use crate::serde_utils;
use alloy_primitives::U256;
use ark_bn254::Bn254;
use circom_types::groth16::Proof;
use serde::{Deserialize, Serialize};
use taceo_oprf::types::api::{CloseFrameMessage, OprfRequestAuthenticatorError};
use crate::{FieldElement, rp::RpId};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OprfPrefix {
Uniqueness = 0x00,
SessionOprfSeed = 0x01,
SessionAction = 0x02,
}
pub trait OprfPrefixedFieldElement {
fn random_with_prefix<R: rand::CryptoRng + rand::RngCore>(
rng: &mut R,
prefix: OprfPrefix,
) -> FieldElement;
fn has_prefix(&self, prefix: OprfPrefix) -> bool;
}
impl OprfPrefixedFieldElement for FieldElement {
fn random_with_prefix<R: rand::CryptoRng + rand::RngCore>(
rng: &mut R,
prefix: OprfPrefix,
) -> FieldElement {
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
bytes[0] = prefix as u8;
Self::from_be_bytes(&bytes).expect(
"should always fit in the field because with 0x02 or lower as the MSB, the field element < babyjubjub modulus",
)
}
fn has_prefix(&self, prefix: OprfPrefix) -> bool {
self.to_be_bytes()[0] == prefix as u8
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum OprfModule {
Nullifier,
CredentialBlindingFactor,
Session,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RpSignatureVerification {
UniquenessAction {
action: FieldElement,
},
}
impl std::fmt::Display for OprfModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Nullifier => write!(f, "nullifier"),
Self::CredentialBlindingFactor => write!(f, "credential_blinding_factor"),
Self::Session => write!(f, "session"),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct NullifierOprfRequestAuthV1 {
pub proof: Proof<Bn254>,
#[serde(with = "ark_serde_compat::field")]
pub action: ark_babyjubjub::Fq,
#[serde(with = "ark_serde_compat::field")]
pub nonce: ark_babyjubjub::Fq,
#[serde(with = "ark_serde_compat::field")]
pub merkle_root: ark_babyjubjub::Fq,
#[serde(alias = "current_time_stamp")]
pub created_at: u64,
#[serde(alias = "expiration_timestamp")]
pub expires_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<alloy_primitives::Signature>,
pub rp_id: RpId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_utils::hex_bytes_opt"
)]
pub wip101_data: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rp_signature_verification: Option<RpSignatureVerification>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CredentialBlindingFactorOprfRequestAuthV1 {
pub proof: Proof<Bn254>,
#[serde(with = "ark_serde_compat::field")]
pub action: ark_babyjubjub::Fq,
#[serde(with = "ark_serde_compat::field")]
pub nonce: ark_babyjubjub::Fq,
#[serde(with = "ark_serde_compat::field")]
pub merkle_root: ark_babyjubjub::Fq,
pub issuer_schema_id: u64,
}
#[derive(Copy, Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorldIdRequestAuthError {
#[error("unknown_rp")]
UnknownRp,
#[error("inactive_rp")]
InactiveRp,
#[error("unknown_schema_issuer_id")]
UnknownSchemaIssuerId,
#[error("timestamp_too_old")]
CreatedAtTooOld,
#[error("timestamp_too_far_in_future")]
CreatedAtTooFarInFuture,
#[error("expires_at_too_far_in_future")]
ExpiresAtTooFarInFuture,
#[error("invalid_timestamp")]
InvalidTimestamp,
#[error("rp_signature_expired")]
RpSignatureExpired,
#[error("invalid_rp_signature")]
InvalidRpSignature,
#[error("rp_signature_missing")]
RpSignatureMissing,
#[error("wip101_aux_data_on_eoa")]
Wip101AuxDataOnEoa,
#[error("duplicate_nonce")]
DuplicateNonce,
#[error("invalid_merkle_root")]
InvalidMerkleRoot,
#[error("invalid_query_proof")]
InvalidQueryProof,
#[error("invalid_action_for_blinding_factor")]
InvalidActionSchemaIssuer,
#[error("invalid_action_for_nullifier")]
InvalidActionNullifier,
#[error("invalid_action_for_session")]
InvalidActionSession,
#[error("invalid_rp_signature_verification")]
InvalidRpSignatureVerification,
#[error("wip101_incompatible_rp_signer")]
Wip101IncompatibleRpSigner,
#[error("wip101_verification_failed")]
Wip101VerificationFailed(Option<U256>),
#[error("wip101_custom_revert")]
Wip101CustomRevert,
#[error("wip101_aux_data_too_large")]
Wip101AuxDataTooLarge,
#[error("wip101_verification_timeout")]
Wip101VerificationTimeout,
#[error("wip101_account_check_timeout")]
Wip101AccountCheckTimeout,
#[error("internal_server_error")]
Internal,
#[error("unknown_error_{0}")]
Unknown(u16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorActor {
Rp,
Issuer,
Authenticator,
OprfNode,
}
impl WorldIdRequestAuthError {
#[must_use]
pub const fn as_actor(&self) -> ErrorActor {
match self {
Self::UnknownRp
| Self::InactiveRp
| Self::CreatedAtTooOld
| Self::CreatedAtTooFarInFuture
| Self::ExpiresAtTooFarInFuture
| Self::InvalidTimestamp
| Self::RpSignatureExpired
| Self::InvalidRpSignature
| Self::DuplicateNonce
| Self::InvalidActionNullifier
| Self::Wip101IncompatibleRpSigner
| Self::Wip101VerificationFailed(_)
| Self::Wip101CustomRevert
| Self::Wip101VerificationTimeout
| Self::Wip101AuxDataOnEoa
| Self::Wip101AuxDataTooLarge
| Self::Wip101AccountCheckTimeout => ErrorActor::Rp,
Self::UnknownSchemaIssuerId => ErrorActor::Issuer,
Self::InvalidMerkleRoot
| Self::InvalidQueryProof
| Self::InvalidActionSchemaIssuer
| Self::InvalidActionSession
| Self::InvalidRpSignatureVerification
| Self::RpSignatureMissing => ErrorActor::Authenticator,
Self::Internal | Self::Unknown(_) => ErrorActor::OprfNode,
}
}
}
impl From<u16> for WorldIdRequestAuthError {
fn from(value: u16) -> Self {
match value {
error_codes::UNKNOWN_RP => Self::UnknownRp,
error_codes::INACTIVE_RP => Self::InactiveRp,
error_codes::CREATED_AT_TOO_OLD => Self::CreatedAtTooOld,
error_codes::INVALID_RP_SIGNATURE => Self::InvalidRpSignature,
error_codes::DUPLICATE_NONCE => Self::DuplicateNonce,
error_codes::INVALID_MERKLE_ROOT => Self::InvalidMerkleRoot,
error_codes::INVALID_QUERY_PROOF => Self::InvalidQueryProof,
error_codes::INVALID_ACTION_SCHEMA_ISSUER => Self::InvalidActionSchemaIssuer,
error_codes::UNKNOWN_SCHEMA_ISSUER => Self::UnknownSchemaIssuerId,
error_codes::INVALID_ACTION_NULLIFIER => Self::InvalidActionNullifier,
error_codes::INVALID_ACTION_SESSION => Self::InvalidActionSession,
error_codes::INVALID_RP_SIGNATURE_VERIFICATION => Self::InvalidRpSignatureVerification,
error_codes::RP_SIGNATURE_EXPIRED => Self::RpSignatureExpired,
error_codes::RP_SIGNATURE_MISSING => Self::RpSignatureMissing,
error_codes::INVALID_TIMESTAMP => Self::InvalidTimestamp,
error_codes::CREATED_AT_TOO_FAR_IN_FUTURE => Self::CreatedAtTooFarInFuture,
error_codes::EXPIRES_AT_TOO_FAR_IN_FUTURE => Self::ExpiresAtTooFarInFuture,
error_codes::WIP101_INCOMPATIBLE_RP_SIGNER => Self::Wip101IncompatibleRpSigner,
error_codes::WIP101_VERIFICATION_TIMEOUT => Self::Wip101VerificationTimeout,
error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT => Self::Wip101AccountCheckTimeout,
error_codes::WIP101_VERIFICATION_FAILED => Self::Wip101VerificationFailed(None),
error_codes::WIP101_CUSTOM_REVERT => Self::Wip101CustomRevert,
error_codes::INTERNAL => Self::Internal,
other => Self::Unknown(other),
}
}
}
impl From<WorldIdRequestAuthError> for u16 {
fn from(value: WorldIdRequestAuthError) -> Self {
match value {
WorldIdRequestAuthError::UnknownRp => error_codes::UNKNOWN_RP,
WorldIdRequestAuthError::InactiveRp => error_codes::INACTIVE_RP,
WorldIdRequestAuthError::CreatedAtTooOld => error_codes::CREATED_AT_TOO_OLD,
WorldIdRequestAuthError::ExpiresAtTooFarInFuture => {
error_codes::EXPIRES_AT_TOO_FAR_IN_FUTURE
}
WorldIdRequestAuthError::InvalidTimestamp => error_codes::INVALID_TIMESTAMP,
WorldIdRequestAuthError::InvalidRpSignature => error_codes::INVALID_RP_SIGNATURE,
WorldIdRequestAuthError::RpSignatureMissing => error_codes::RP_SIGNATURE_MISSING,
WorldIdRequestAuthError::DuplicateNonce => error_codes::DUPLICATE_NONCE,
WorldIdRequestAuthError::InvalidMerkleRoot => error_codes::INVALID_MERKLE_ROOT,
WorldIdRequestAuthError::InvalidQueryProof => error_codes::INVALID_QUERY_PROOF,
WorldIdRequestAuthError::InvalidActionSchemaIssuer => {
error_codes::INVALID_ACTION_SCHEMA_ISSUER
}
WorldIdRequestAuthError::UnknownSchemaIssuerId => error_codes::UNKNOWN_SCHEMA_ISSUER,
WorldIdRequestAuthError::InvalidActionNullifier => {
error_codes::INVALID_ACTION_NULLIFIER
}
WorldIdRequestAuthError::InvalidActionSession => error_codes::INVALID_ACTION_SESSION,
WorldIdRequestAuthError::InvalidRpSignatureVerification => {
error_codes::INVALID_RP_SIGNATURE_VERIFICATION
}
WorldIdRequestAuthError::RpSignatureExpired => error_codes::RP_SIGNATURE_EXPIRED,
WorldIdRequestAuthError::CreatedAtTooFarInFuture => {
error_codes::CREATED_AT_TOO_FAR_IN_FUTURE
}
WorldIdRequestAuthError::Wip101IncompatibleRpSigner => {
error_codes::WIP101_INCOMPATIBLE_RP_SIGNER
}
WorldIdRequestAuthError::Wip101VerificationFailed(_) => {
error_codes::WIP101_VERIFICATION_FAILED
}
WorldIdRequestAuthError::Wip101VerificationTimeout => {
error_codes::WIP101_VERIFICATION_TIMEOUT
}
WorldIdRequestAuthError::Wip101CustomRevert => error_codes::WIP101_CUSTOM_REVERT,
WorldIdRequestAuthError::Wip101AuxDataOnEoa => error_codes::WIP101_AUX_DATA_ON_EOA,
WorldIdRequestAuthError::Wip101AuxDataTooLarge => {
error_codes::WIP101_AUX_DATA_TOO_LARGE
}
WorldIdRequestAuthError::Wip101AccountCheckTimeout => {
error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT
}
WorldIdRequestAuthError::Internal => error_codes::INTERNAL,
WorldIdRequestAuthError::Unknown(other) => other,
}
}
}
pub mod error_codes {
pub const UNKNOWN_RP: u16 = 4500;
pub const CREATED_AT_TOO_OLD: u16 = 4501;
pub const INVALID_RP_SIGNATURE: u16 = 4502;
pub const DUPLICATE_NONCE: u16 = 4503;
pub const INVALID_MERKLE_ROOT: u16 = 4504;
pub const INVALID_QUERY_PROOF: u16 = 4505;
pub const INVALID_ACTION_SCHEMA_ISSUER: u16 = 4506;
pub const UNKNOWN_SCHEMA_ISSUER: u16 = 4507;
pub const INVALID_ACTION_NULLIFIER: u16 = 4508;
pub const INVALID_ACTION_SESSION: u16 = 4509;
pub const INACTIVE_RP: u16 = 4510;
pub const RP_SIGNATURE_EXPIRED: u16 = 4511;
pub const INVALID_TIMESTAMP: u16 = 4512;
pub const CREATED_AT_TOO_FAR_IN_FUTURE: u16 = 4513;
pub const WIP101_INCOMPATIBLE_RP_SIGNER: u16 = 4514;
pub const WIP101_VERIFICATION_FAILED: u16 = 4515;
pub const WIP101_CUSTOM_REVERT: u16 = 4516;
pub const WIP101_AUX_DATA_TOO_LARGE: u16 = 4517;
pub const RP_SIGNATURE_MISSING: u16 = 4518;
pub const WIP101_AUX_DATA_ON_EOA: u16 = 4519;
pub const WIP101_VERIFICATION_TIMEOUT: u16 = 4520;
pub const WIP101_ACCOUNT_CHECK_TIMEOUT: u16 = 4521;
pub const EXPIRES_AT_TOO_FAR_IN_FUTURE: u16 = 4523;
pub const INVALID_RP_SIGNATURE_VERIFICATION: u16 = 4524;
pub const INTERNAL: u16 = 1011;
}
impl From<WorldIdRequestAuthError> for OprfRequestAuthenticatorError {
fn from(value: WorldIdRequestAuthError) -> Self {
let code = u16::from(value);
let msg = match value {
WorldIdRequestAuthError::UnknownRp => {
taceo_oprf::types::close_frame_message!("unknown RP")
}
WorldIdRequestAuthError::CreatedAtTooOld => {
taceo_oprf::types::close_frame_message!("created_at too old")
}
WorldIdRequestAuthError::CreatedAtTooFarInFuture => {
taceo_oprf::types::close_frame_message!("created_at too far in future")
}
WorldIdRequestAuthError::ExpiresAtTooFarInFuture => {
taceo_oprf::types::close_frame_message!("expires_at too far in the future")
}
WorldIdRequestAuthError::InvalidRpSignature => {
taceo_oprf::types::close_frame_message!("signature from RP cannot be verified")
}
WorldIdRequestAuthError::RpSignatureMissing => {
taceo_oprf::types::close_frame_message!("RP signature missing but signer is an EOA")
}
WorldIdRequestAuthError::DuplicateNonce => {
taceo_oprf::types::close_frame_message!("signature nonce already used")
}
WorldIdRequestAuthError::InvalidMerkleRoot => {
taceo_oprf::types::close_frame_message!("invalid merkle root")
}
WorldIdRequestAuthError::InvalidQueryProof => {
taceo_oprf::types::close_frame_message!("cannot verify query proof")
}
WorldIdRequestAuthError::InvalidActionSchemaIssuer => {
taceo_oprf::types::close_frame_message!(
"invalid action for credential sub blinding factor"
)
}
WorldIdRequestAuthError::UnknownSchemaIssuerId => {
taceo_oprf::types::close_frame_message!("unknown schema issuer id")
}
WorldIdRequestAuthError::InvalidActionNullifier => {
taceo_oprf::types::close_frame_message!("invalid action for nullifier")
}
WorldIdRequestAuthError::InvalidActionSession => {
taceo_oprf::types::close_frame_message!("invalid action for session proofs")
}
WorldIdRequestAuthError::InactiveRp => {
taceo_oprf::types::close_frame_message!("inactive RP")
}
WorldIdRequestAuthError::RpSignatureExpired => {
taceo_oprf::types::close_frame_message!("RP signature expired")
}
WorldIdRequestAuthError::InvalidTimestamp => {
taceo_oprf::types::close_frame_message!("cannot parse timestamp on request")
}
WorldIdRequestAuthError::Wip101IncompatibleRpSigner => {
taceo_oprf::types::close_frame_message!(
"RP has a contract backed signer but doesn't conform to WIP101"
)
}
WorldIdRequestAuthError::Wip101CustomRevert => {
taceo_oprf::types::close_frame_message!(
"RP signer contract reverted with custom error (and not error RpInvalidRequest(uint256 code);)"
)
}
WorldIdRequestAuthError::Wip101VerificationFailed(None) => {
taceo_oprf::types::close_frame_message!("")
}
WorldIdRequestAuthError::Wip101VerificationTimeout => {
taceo_oprf::types::close_frame_message!("WIP101 verification ran into timeout")
}
WorldIdRequestAuthError::Wip101VerificationFailed(Some(code)) => {
CloseFrameMessage::new_truncate(format!("{:#x}", code))
}
WorldIdRequestAuthError::InvalidRpSignatureVerification => {
taceo_oprf::types::close_frame_message!("Invalid RP signature verification data")
}
WorldIdRequestAuthError::Wip101AuxDataOnEoa => taceo_oprf::types::close_frame_message!(
"Auxiliary data must be empty with EOA backed signer"
),
WorldIdRequestAuthError::Wip101AuxDataTooLarge => {
taceo_oprf::types::close_frame_message!(
"Auxiliary data for WIP101 contract too large - max 1024 bytes"
)
}
WorldIdRequestAuthError::Wip101AccountCheckTimeout => {
taceo_oprf::types::close_frame_message!(
"Ran into timeout while doing WIP101/ERC165 check on RP's signer"
)
}
WorldIdRequestAuthError::Internal => {
taceo_oprf::types::close_frame_message!("internal server error")
}
WorldIdRequestAuthError::Unknown(_) => {
taceo_oprf::types::close_frame_message!("unknown")
}
};
Self::with_message(code, msg)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_PREFIXES: [OprfPrefix; 3] = [
OprfPrefix::Uniqueness,
OprfPrefix::SessionOprfSeed,
OprfPrefix::SessionAction,
];
#[test]
fn random_with_prefix_is_recognized_only_by_its_own_prefix() {
let mut rng = rand::rngs::OsRng;
for prefix in ALL_PREFIXES {
let field_element = FieldElement::random_with_prefix(&mut rng, prefix);
assert_eq!(field_element.to_be_bytes()[0], prefix as u8);
for other in ALL_PREFIXES {
assert_eq!(field_element.has_prefix(other), other == prefix);
}
}
}
fn test_proof() -> Proof<Bn254> {
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(
rp_signature_verification: Option<RpSignatureVerification>,
) -> 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,
rp_signature_verification,
}
}
#[test]
fn nullifier_auth_rp_signature_verification_json_roundtrip() {
let verification = RpSignatureVerification::UniquenessAction {
action: FieldElement::from(42u64),
};
let auth = test_auth(Some(verification));
let json = serde_json::to_string(&auth).unwrap();
let parsed: NullifierOprfRequestAuthV1 = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.rp_signature_verification, Some(verification));
}
#[test]
fn nullifier_auth_rp_signature_verification_cbor_roundtrip() {
let verification = RpSignatureVerification::UniquenessAction {
action: FieldElement::from(42u64),
};
let auth = test_auth(Some(verification));
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.rp_signature_verification, Some(verification));
}
#[test]
fn error_code_roundtrip() {
let codes: &[u16] = &[
error_codes::UNKNOWN_RP,
error_codes::CREATED_AT_TOO_OLD,
error_codes::CREATED_AT_TOO_FAR_IN_FUTURE,
error_codes::INVALID_RP_SIGNATURE,
error_codes::DUPLICATE_NONCE,
error_codes::INVALID_MERKLE_ROOT,
error_codes::INVALID_QUERY_PROOF,
error_codes::INVALID_ACTION_SCHEMA_ISSUER,
error_codes::UNKNOWN_SCHEMA_ISSUER,
error_codes::INVALID_ACTION_NULLIFIER,
error_codes::INVALID_ACTION_SESSION,
error_codes::INVALID_RP_SIGNATURE_VERIFICATION,
error_codes::INACTIVE_RP,
error_codes::RP_SIGNATURE_EXPIRED,
error_codes::INVALID_TIMESTAMP,
error_codes::WIP101_INCOMPATIBLE_RP_SIGNER,
error_codes::WIP101_VERIFICATION_FAILED,
error_codes::WIP101_CUSTOM_REVERT,
error_codes::WIP101_AUX_DATA_TOO_LARGE,
error_codes::RP_SIGNATURE_MISSING,
error_codes::WIP101_AUX_DATA_ON_EOA,
error_codes::WIP101_VERIFICATION_TIMEOUT,
error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT,
error_codes::INTERNAL,
];
for &code in codes {
let error = WorldIdRequestAuthError::from(code);
let back: u16 = error.into();
assert_eq!(code, back, "roundtrip failed for code {code}");
}
}
}