From 1a8518402a3435d2b2c3b343e12284e89efd18a2 Mon Sep 17 00:00:00 2001 From: kilianglas Date: Tue, 14 Jul 2026 10:42:23 +0200 Subject: [PATCH] feat: verify rp signature over signed action on session seed queries --- crates/primitives/src/oprf.rs | 117 +++++++++++++- crates/proof/src/oprf_query.rs | 2 + .../src/bin/world-id-dev-client-rp.rs | 1 + services/oprf-node/src/auth/rp_module.rs | 50 +++++- .../oprf-node/src/auth/rp_module/tests.rs | 146 ++++++++++++++++++ services/oprf-node/src/metrics.rs | 11 ++ 6 files changed, 323 insertions(+), 4 deletions(-) diff --git a/crates/primitives/src/oprf.rs b/crates/primitives/src/oprf.rs index b883f27a1..01cdf2824 100644 --- a/crates/primitives/src/oprf.rs +++ b/crates/primitives/src/oprf.rs @@ -5,7 +5,7 @@ use circom_types::groth16::Proof; use serde::{Deserialize, Serialize}; use taceo_oprf::types::api::{CloseFrameMessage, OprfRequestAuthenticatorError}; -use crate::rp::RpId; +use crate::{FieldElement, rp::RpId}; #[expect(unused_imports, reason = "used in doc comments")] use crate::SessionFeType; @@ -69,6 +69,14 @@ pub struct NullifierOprfRequestAuthV1 { with = "serde_utils::hex_bytes_opt" )] pub wip101_data: Option>, + /// The RP-signed uniqueness action (MSB `0x00`) for create-and-bind session-seed queries. + /// + /// Only valid on session-seed queries (see [`SessionFeType::OprfSeed`]) from EOA-backed RPs. + /// When present, the OPRF node verifies the RP signature over the action-inclusive message + /// (see `compute_rp_signature_msg`) instead of the action-less one, so a single RP signature + /// can authorize creating a session and binding a Uniqueness Proof to it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signed_action: Option, } /// A request sent by a client for OPRF credential blinding factor authentication. @@ -171,6 +179,14 @@ pub enum WorldIdRequestAuthError { /// prefixes. #[error("invalid_action_for_session")] InvalidActionSession, + /// The provided signed action is not a valid nullifier action. Signed actions must + /// start with `0x00` (MSB). + #[error("invalid_signed_action")] + InvalidSignedAction, + /// A signed action was provided on a request that does not support one. Signed + /// actions are only allowed on session-seed queries from EOA-backed RPs. + #[error("signed_action_not_allowed")] + SignedActionNotAllowed, /// The RP signer is a contract but does not implement the WIP101 interface. #[error("wip101_incompatible_rp_signer")] Wip101IncompatibleRpSigner, @@ -235,6 +251,7 @@ impl WorldIdRequestAuthError { | Self::InvalidRpSignature | Self::DuplicateNonce | Self::InvalidActionNullifier + | Self::InvalidSignedAction | Self::Wip101IncompatibleRpSigner | Self::Wip101VerificationFailed(_) | Self::Wip101CustomRevert @@ -247,6 +264,7 @@ impl WorldIdRequestAuthError { | Self::InvalidQueryProof | Self::InvalidActionSchemaIssuer | Self::InvalidActionSession + | Self::SignedActionNotAllowed | Self::RpSignatureMissing => ErrorActor::Authenticator, Self::Internal | Self::Unknown(_) => ErrorActor::OprfNode, } @@ -268,6 +286,8 @@ impl From for WorldIdRequestAuthError { error_codes::UNKNOWN_SCHEMA_ISSUER => Self::UnknownSchemaIssuerId, error_codes::INVALID_ACTION_NULLIFIER => Self::InvalidActionNullifier, error_codes::INVALID_ACTION_SESSION => Self::InvalidActionSession, + error_codes::INVALID_SIGNED_ACTION => Self::InvalidSignedAction, + error_codes::SIGNED_ACTION_NOT_ALLOWED => Self::SignedActionNotAllowed, error_codes::RP_SIGNATURE_EXPIRED => Self::RpSignatureExpired, error_codes::RP_SIGNATURE_MISSING => Self::RpSignatureMissing, error_codes::INVALID_TIMESTAMP => Self::InvalidTimestamp, @@ -309,6 +329,10 @@ impl From for u16 { error_codes::INVALID_ACTION_NULLIFIER } WorldIdRequestAuthError::InvalidActionSession => error_codes::INVALID_ACTION_SESSION, + WorldIdRequestAuthError::InvalidSignedAction => error_codes::INVALID_SIGNED_ACTION, + WorldIdRequestAuthError::SignedActionNotAllowed => { + error_codes::SIGNED_ACTION_NOT_ALLOWED + } WorldIdRequestAuthError::RpSignatureExpired => error_codes::RP_SIGNATURE_EXPIRED, WorldIdRequestAuthError::CreatedAtTooFarInFuture => { error_codes::CREATED_AT_TOO_FAR_IN_FUTURE @@ -386,6 +410,10 @@ pub mod error_codes { pub const BLOCKED_RP: u16 = 4522; /// Error code for [`super::WorldIdRequestAuthError::ExpiresAtTooFarInFuture`]. pub const EXPIRES_AT_TOO_FAR_IN_FUTURE: u16 = 4523; + /// Error code for [`super::WorldIdRequestAuthError::InvalidSignedAction`]. + pub const INVALID_SIGNED_ACTION: u16 = 4524; + /// Error code for [`super::WorldIdRequestAuthError::SignedActionNotAllowed`]. + pub const SIGNED_ACTION_NOT_ALLOWED: u16 = 4525; /// Error code for [`super::WorldIdRequestAuthError::Internal`]. pub const INTERNAL: u16 = 1011; } @@ -468,6 +496,16 @@ impl From for OprfRequestAuthenticatorError { // this should never truncate as code is a U256 encoded as hex CloseFrameMessage::new_truncate(format!("{:#x}", code)) } + WorldIdRequestAuthError::InvalidSignedAction => { + taceo_oprf::types::close_frame_message!( + "Invalid signed action - must be a valid nullifier action (MSB 0x00)" + ) + } + WorldIdRequestAuthError::SignedActionNotAllowed => { + taceo_oprf::types::close_frame_message!( + "Signed actions are only allowed on session-seed queries from EOA-backed RPs" + ) + } WorldIdRequestAuthError::Wip101AuxDataOnEoa => taceo_oprf::types::close_frame_message!( "Auxiliary data must be empty with EOA backed signer" ), @@ -496,6 +534,81 @@ impl From for OprfRequestAuthenticatorError { mod tests { use super::*; + /// A structurally valid Groth16 proof (BN254 generator points) for serde tests. + fn test_proof() -> Proof { + serde_json::from_value(serde_json::json!({ + "pi_a": ["1", "2", "1"], + "pi_b": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + ["1", "0"] + ], + "pi_c": ["1", "2", "1"], + "protocol": "groth16", + "curve": "bn128" + })) + .expect("valid test proof") + } + + fn test_auth(signed_action: Option) -> NullifierOprfRequestAuthV1 { + NullifierOprfRequestAuthV1 { + proof: test_proof(), + action: ark_babyjubjub::Fq::from(1u64), + nonce: ark_babyjubjub::Fq::from(2u64), + merkle_root: ark_babyjubjub::Fq::from(3u64), + created_at: 4, + expires_at: 5, + signature: None, + rp_id: RpId::new(6), + wip101_data: None, + signed_action, + } + } + + #[test] + fn nullifier_auth_signed_action_none_is_omitted() { + let value = serde_json::to_value(test_auth(None)).unwrap(); + // Forward compat: unused, the field never appears on the wire. + assert!(value.get("signed_action").is_none()); + // Backward compat: payloads without the field deserialize to `None`. + let parsed: NullifierOprfRequestAuthV1 = serde_json::from_value(value).unwrap(); + assert!(parsed.signed_action.is_none()); + } + + #[test] + fn nullifier_auth_signed_action_json_roundtrip() { + let signed_action = FieldElement::from(42u64); + let auth = test_auth(Some(signed_action)); + let json = serde_json::to_string(&auth).unwrap(); + let parsed: NullifierOprfRequestAuthV1 = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.signed_action, Some(signed_action)); + } + + #[test] + fn nullifier_auth_signed_action_cbor_roundtrip() { + let signed_action = FieldElement::from(42u64); + let auth = test_auth(Some(signed_action)); + let mut bytes = Vec::new(); + ciborium::into_writer(&auth, &mut bytes).unwrap(); + let parsed: NullifierOprfRequestAuthV1 = ciborium::from_reader(bytes.as_slice()).unwrap(); + assert_eq!(parsed.signed_action, Some(signed_action)); + } + + #[test] + fn nullifier_auth_ignores_unknown_fields() { + // Old nodes must ignore fields added later (no `deny_unknown_fields`). + let mut value = serde_json::to_value(test_auth(None)).unwrap(); + value["some_future_field"] = serde_json::json!("ignored"); + let parsed = serde_json::from_value::(value); + assert!(parsed.is_ok()); + } + #[test] fn error_code_roundtrip() { let codes: &[u16] = &[ @@ -511,6 +624,8 @@ mod tests { error_codes::UNKNOWN_SCHEMA_ISSUER, error_codes::INVALID_ACTION_NULLIFIER, error_codes::INVALID_ACTION_SESSION, + error_codes::INVALID_SIGNED_ACTION, + error_codes::SIGNED_ACTION_NOT_ALLOWED, error_codes::INACTIVE_RP, error_codes::RP_SIGNATURE_EXPIRED, error_codes::INVALID_TIMESTAMP, diff --git a/crates/proof/src/oprf_query.rs b/crates/proof/src/oprf_query.rs index 049590d71..91832b7ca 100644 --- a/crates/proof/src/oprf_query.rs +++ b/crates/proof/src/oprf_query.rs @@ -258,6 +258,7 @@ impl<'a> OprfEntrypoint<'a> { signature: Some(proof_request.signature), rp_id: proof_request.rp_id, wip101_data: None, + signed_action: None, }; let verifiable_oprf_output = Self::execute_distributed_oprf( @@ -311,6 +312,7 @@ impl<'a> OprfEntrypoint<'a> { signature: Some(proof_request.signature), rp_id: proof_request.rp_id, wip101_data: None, + signed_action: None, }; let verifiable_oprf_output = Self::execute_distributed_oprf( diff --git a/services/oprf-dev-client/src/bin/world-id-dev-client-rp.rs b/services/oprf-dev-client/src/bin/world-id-dev-client-rp.rs index 4de270ce0..7c00598bc 100644 --- a/services/oprf-dev-client/src/bin/world-id-dev-client-rp.rs +++ b/services/oprf-dev-client/src/bin/world-id-dev-client-rp.rs @@ -360,6 +360,7 @@ fn generate_oprf_auth_request( signature: Some(proof_request.signature), rp_id: proof_request.rp_id, wip101_data: None, + signed_action: None, }; Ok(auth) diff --git a/services/oprf-node/src/auth/rp_module.rs b/services/oprf-node/src/auth/rp_module.rs index 5ec53193b..96565a31a 100644 --- a/services/oprf-node/src/auth/rp_module.rs +++ b/services/oprf-node/src/auth/rp_module.rs @@ -3,11 +3,22 @@ //! Both the session and uniqueness modules share identical struct fields, init //! logic, and query-proof verification. They differ only in: //! - how the action field is validated (`MSB == 0x00` for uniqueness vs `0x01/0x02` for sessions depending on the [`SessionFeType`]) -//! - whether the action is included in the RP signature (`Some` for uniqueness, `None` for session) +//! - whether the action is included in the RP signature (`Some` for uniqueness, `None` for +//! session — unless the request carries a `signed_action`, see below) //! - which [`WorldIdRequestAuthError`] variant is returned for an invalid action //! //! [`RpModuleKind`] captures these differences; [`RpModuleAuth`] holds the shared //! state and branches on the kind at runtime. +//! +//! # Signed actions on session-seed queries (create-and-bind) +//! +//! A session-seed query may carry an optional `signed_action` (a nullifier action, MSB +//! `0x00`). When present, the session module verifies the RP signature over the +//! action-inclusive message instead of the action-less one. This lets a single RP +//! signature authorize both creating a session and binding a Uniqueness Proof to it. +//! `signed_action` is rejected everywhere else: on session-action queries, on the +//! uniqueness module, and for WIP101 contract-backed RPs (which do not support session +//! queries yet). use crate::{ accountant_batcher::AccountantBatcherHandle, @@ -42,7 +53,9 @@ pub(crate) mod wip101; /// Distinguishes the two RP-authenticated OPRF modules. #[derive(Clone)] pub(crate) enum RpModuleKind { - /// Session module: action MSB must be `0x01` (seed) or `0x02` (action); action is NOT signed. + /// Session module: action MSB must be `0x01` (seed) or `0x02` (action); action is NOT + /// signed. Seed queries may carry a `signed_action` (MSB `0x00`), in which case the RP + /// signature is verified over the action-inclusive message (create-and-bind). Session, /// Uniqueness module: action MSB must be `0x00`; action IS signed. Uniqueness(AccountantBatcherHandle), @@ -73,6 +86,10 @@ pub(crate) enum RpModuleError { #[error("Invalid action for uniqueness (action MSB must be 0x00): {action}")] InvalidActionUniqueness { action: FieldElement }, + #[error("Invalid signed action (MSB must be 0x00): {signed_action}")] + InvalidSignedAction { signed_action: FieldElement }, + #[error("Signed action not allowed: {context}")] + SignedActionNotAllowed { context: &'static str }, #[error("Could not verify query proof")] InvalidQueryProof, #[error(transparent)] @@ -131,6 +148,8 @@ impl From<&RpModuleError> for WorldIdRequestAuthError { match value { RpModuleError::InvalidActionSession { .. } => Self::InvalidActionSession, RpModuleError::InvalidActionUniqueness { .. } => Self::InvalidActionNullifier, + RpModuleError::InvalidSignedAction { .. } => Self::InvalidSignedAction, + RpModuleError::SignedActionNotAllowed { .. } => Self::SignedActionNotAllowed, RpModuleError::InvalidQueryProof => Self::InvalidQueryProof, RpModuleError::MerkleWatcher(e) => Self::from(e.as_ref()), RpModuleError::RpRegistry(e) => Self::from(e.as_ref()), @@ -321,12 +340,19 @@ impl RpModuleAuth { tracing::trace!("RP signer is EOA"); let action = match self.kind { RpModuleKind::Uniqueness(_) => Some(action), - RpModuleKind::Session => None, + // Session RP signatures do not include the action, unless the request + // carries a `signed_action` (create-and-bind seed queries). + RpModuleKind::Session => request.auth.signed_action.map(|a| *a), }; rp.verify_eoa(action, request) } RpAccountType::Contract => { // TODO(session-proofs): WIP-101 does not currently support session proofs. + if request.auth.signed_action.is_some() { + return Err(RpModuleError::SignedActionNotAllowed { + context: "not supported for WIP101 contract-backed RPs", + }); + } Ok(rp .verify_wip101( action, @@ -378,12 +404,25 @@ impl RpModuleAuth { let action = FieldElement::from(request.auth.action); // Validate the action per kind and derive the nonce scope it consumes. + // A `signed_action` (a nullifier action the RP signature covers) is only valid on + // session-seed queries; see the module docs for the create-and-bind flow. let nonce_scope = match self.kind { RpModuleKind::Session => { metrics::auth_module::inc_session(); if action.is_valid_for_session(SessionFeType::OprfSeed) { + if let Some(signed_action) = request.auth.signed_action { + if signed_action.to_be_bytes()[0] != 0 { + return Err(RpModuleError::InvalidSignedAction { signed_action }); + } + metrics::auth_module::inc_session_signed_action(); + } NonceScope::SessionOprfSeed } else if action.is_valid_for_session(SessionFeType::Action) { + if request.auth.signed_action.is_some() { + return Err(RpModuleError::SignedActionNotAllowed { + context: "only allowed on session-seed queries", + }); + } NonceScope::SessionAction } else { return Err(RpModuleError::InvalidActionSession { action }); @@ -391,6 +430,11 @@ impl RpModuleAuth { } RpModuleKind::Uniqueness(_) => { metrics::auth_module::inc_nullifier(); + if request.auth.signed_action.is_some() { + return Err(RpModuleError::SignedActionNotAllowed { + context: "only allowed on the session module", + }); + } if action.to_be_bytes()[0] != 0 { return Err(RpModuleError::InvalidActionUniqueness { action }); } diff --git a/services/oprf-node/src/auth/rp_module/tests.rs b/services/oprf-node/src/auth/rp_module/tests.rs index beac1afe7..930fd5a7e 100644 --- a/services/oprf-node/src/auth/rp_module/tests.rs +++ b/services/oprf-node/src/auth/rp_module/tests.rs @@ -76,6 +76,45 @@ impl RpModuleTestSetup { signature: Some(signature), rp_id: infra.setup.rp_fixture.world_rp_id, wip101_data: None, + signed_action: None, + }; + + Ok(Self { + setup: infra.setup, + request_authenticator, + request: OprfRequest { + request_id: Uuid::new_v4(), + blinded_query: bundle.blinded_query, + auth, + }, + }) + } + + /// Constructs a valid session-seed test setup whose RP signature covers the + /// fixture's uniqueness action, carried in `signed_action` (create-and-bind). + pub(crate) async fn new_session_bound_seed() -> eyre::Result { + let mut rng = rand::thread_rng(); + let infra = AuthModulesTestSetup::new(SetupKind::RpModule).await?; + + let request_authenticator = RpModuleAuth::new_session(infra.rp_module_args()); + + let session_action = FieldElement::random_for_session(&mut rng, SessionFeType::OprfSeed); + let bundle = infra + .generate_query_proof(session_action, infra.setup.rp_fixture.world_rp_id.into())?; + + // The fixture signature is computed over the action-inclusive message, matching + // the `signed_action` below. + let auth = NullifierOprfRequestAuthV1 { + proof: bundle.proof, + action: *session_action, + nonce: bundle.nonce, + merkle_root: *infra.setup.merkle_inclusion_proof.root, + created_at: infra.setup.rp_fixture.current_timestamp, + expires_at: infra.setup.rp_fixture.expiration_timestamp, + signature: Some(infra.setup.rp_fixture.signature), + rp_id: infra.setup.rp_fixture.world_rp_id, + wip101_data: None, + signed_action: Some(infra.setup.rp_fixture.action.into()), }; Ok(Self { @@ -111,6 +150,7 @@ impl RpModuleTestSetup { signature: Some(infra.setup.rp_fixture.signature), rp_id: infra.setup.rp_fixture.world_rp_id, wip101_data: None, + signed_action: None, }; Ok(Self { @@ -643,6 +683,112 @@ async fn test_session_invalid_action_random_prefix() -> eyre::Result<()> { .await } +// ── Signed-action (create-and-bind) tests ──────────────────────────────── +// +// A session-seed query may carry a `signed_action` covered by the RP signature. +// The happy path verifies the action-inclusive message; everything else must +// fail loudly — most importantly the two signature/field mismatch directions, +// which are exactly what an old node (ignoring the field) would hit. + +#[tokio::test] +async fn test_session_seed_signed_action_success() -> eyre::Result<()> { + check_success(RpModuleTestSetup::new_session_bound_seed().await?).await +} + +#[tokio::test] +async fn test_session_seed_signed_action_missing_field() -> eyre::Result<()> { + // Old-node simulation: the signature covers the action, but the field is absent, + // so the node reconstructs the action-less message. Must fail closed. + let mut setup = RpModuleTestSetup::new_session_bound_seed().await?; + setup.request.auth.signed_action = None; + setup + .assert_auth_err( + error_codes::INVALID_RP_SIGNATURE, + "signature from RP cannot be verified", + ) + .await +} + +#[tokio::test] +async fn test_session_seed_signed_action_actionless_signature() -> eyre::Result<()> { + // Inverse mismatch: field present, but the signature was made over the + // action-less message. + let mut setup = RpModuleTestSetup::new_session().await?; + setup.request.auth.signed_action = Some(setup.setup.rp_fixture.action.into()); + setup + .assert_auth_err( + error_codes::INVALID_RP_SIGNATURE, + "signature from RP cannot be verified", + ) + .await +} + +#[tokio::test] +async fn test_session_seed_signed_action_tampered() -> eyre::Result<()> { + let mut setup = RpModuleTestSetup::new_session_bound_seed().await?; + setup.request.auth.signed_action = Some(action_with_msb(0x00).into()); + setup + .assert_auth_err( + error_codes::INVALID_RP_SIGNATURE, + "signature from RP cannot be verified", + ) + .await +} + +#[tokio::test] +async fn test_session_seed_signed_action_invalid_prefix() -> eyre::Result<()> { + // A signed action must be a nullifier action (MSB 0x00); session prefixes are invalid. + let mut setup = RpModuleTestSetup::new_session_bound_seed().await?; + setup.request.auth.signed_action = Some(action_with_msb(0x01).into()); + setup + .assert_auth_err( + error_codes::INVALID_SIGNED_ACTION, + "Invalid signed action - must be a valid nullifier action (MSB 0x00)", + ) + .await +} + +#[tokio::test] +async fn test_session_action_query_rejects_signed_action() -> eyre::Result<()> { + // Only seed queries (0x01) may carry a signed action, not session-action queries (0x02). + let mut setup = RpModuleTestSetup::new_session_with_fe_type(SessionFeType::Action).await?; + setup.request.auth.signed_action = Some(setup.setup.rp_fixture.action.into()); + setup + .assert_auth_err( + error_codes::SIGNED_ACTION_NOT_ALLOWED, + "Signed actions are only allowed on session-seed queries from EOA-backed RPs", + ) + .await +} + +#[tokio::test] +async fn test_uniqueness_rejects_signed_action() -> eyre::Result<()> { + // The uniqueness module signs the regular action; a signed_action is meaningless there. + let mut setup = RpModuleTestSetup::new_uniqueness().await?; + setup.request.auth.signed_action = Some(setup.setup.rp_fixture.action.into()); + setup + .assert_auth_err( + error_codes::SIGNED_ACTION_NOT_ALLOWED, + "Signed actions are only allowed on session-seed queries from EOA-backed RPs", + ) + .await +} + +#[tokio::test] +async fn test_session_wip101_rejects_signed_action() -> eyre::Result<()> { + // WIP101 contract-backed RPs do not support session queries with signed actions. + let mut setup = RpModuleTestSetup::new_session_bound_seed().await?; + let addr = deploy!(WIP101Correct, setup); + setup.set_contract_signer(addr, None).await; + setup.request.auth.signed_action = Some(setup.setup.rp_fixture.action.into()); + setup + .assert_auth_err( + error_codes::SIGNED_ACTION_NOT_ALLOWED, + "Signed actions are only allowed on session-seed queries from EOA-backed RPs", + ) + .await +} + // ── Uniqueness-specific tests ──────────────────────────────────────────── #[tokio::test] diff --git a/services/oprf-node/src/metrics.rs b/services/oprf-node/src/metrics.rs index 7feb07fe4..71dd346c3 100644 --- a/services/oprf-node/src/metrics.rs +++ b/services/oprf-node/src/metrics.rs @@ -51,6 +51,8 @@ pub(crate) mod accountant_batcher { pub(crate) mod auth_module { const METRICS_ID_AUTHENTICATION_COUNTER: &str = "taceo.oprf.node.auth"; + const METRICS_ID_SESSION_SIGNED_ACTION_COUNTER: &str = + "taceo.oprf.node.auth.session_signed_action"; const METRICS_ATTRID_AUTH_MODULE: &str = "auth_module"; const METRICS_ATTR_NULLIFIER_MODULE: &str = "nullifier"; const METRICS_ATTR_SESSION_MODULE: &str = "session"; @@ -62,6 +64,11 @@ pub(crate) mod auth_module { metrics::Unit::Count, "Number of times the authentication modules were hit." ); + metrics::describe_counter!( + METRICS_ID_SESSION_SIGNED_ACTION_COUNTER, + metrics::Unit::Count, + "Number of session-seed authentications carrying an RP-signed action (create-and-bind)." + ); } pub(crate) fn inc_nullifier() { @@ -72,6 +79,10 @@ pub(crate) mod auth_module { metrics::counter!(METRICS_ID_AUTHENTICATION_COUNTER, METRICS_ATTRID_AUTH_MODULE => METRICS_ATTR_SESSION_MODULE).increment(1); } + pub(crate) fn inc_session_signed_action() { + metrics::counter!(METRICS_ID_SESSION_SIGNED_ACTION_COUNTER).increment(1); + } + pub(crate) fn inc_issuer_blinding() { metrics::counter!(METRICS_ID_AUTHENTICATION_COUNTER, METRICS_ATTRID_AUTH_MODULE => METRICS_ATTR_CREDENTIAL_BLINDING).increment(1); }