From e4c99d6bd87d666fa140a5f50371864bfd74bbf0 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Thu, 11 Jun 2026 12:40:33 -0500 Subject: [PATCH 01/13] Pin voprf version to fix compilation errors --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f84f88b..cd244f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ subtle = "2" thiserror = "2" tls_codec = { version = "0.4.2" } tls_codec_derive = "0.4.2" -voprf = { version = "0.6.0-pre.0", features = ["serde"] } +voprf = { version = "=0.6.0-pre.0", features = ["serde"] } p384 = { version = "0.13.0", default-features = false, features = [ "hash2curve", "voprf", From 21f735312491981db6b5042bafd73ca38e77ca15 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Wed, 3 Jun 2026 14:26:50 -0500 Subject: [PATCH 02/13] Implement draft-ietf-privacypass-auth-scheme-extensions --- src/auth/authorize.rs | 245 +++++++++++++++++++++++++++++++++------ src/auth/mod.rs | 8 +- src/common/extensions.rs | 120 +++++++++++++++++++ src/common/mod.rs | 1 + 4 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 src/common/extensions.rs diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 78ab70e..2408723 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -1,11 +1,15 @@ //! This module contains the authorization logic for redemption phase of the //! protocol. -use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use base64::{ + Engine as _, alphabet, + engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, general_purpose::URL_SAFE}, +}; use generic_array::{ArrayLength, GenericArray}; use http::{HeaderValue, header::HeaderName}; use nom::{ IResult, Parser, + branch::alt, bytes::complete::{tag, tag_no_case}, multi::{many1, separated_list1}, }; @@ -13,9 +17,22 @@ use std::io::Write; use thiserror::Error; use tls_codec::{Deserialize, Error, Serialize, Size}; -use crate::{ChallengeDigest, Nonce, TokenKeyId, TokenType}; +use crate::{ChallengeDigest, Nonce, TokenKeyId, TokenType, common::extensions::Extensions}; + +use super::{key_name, opt_spaces, space, unquote}; -use super::{base64_char, key_name, opt_spaces, space}; +// Previous versions of the Token Extensions draft (`draft-ietf-privacypass-auth-scheme-extensions`) +// didn't specify whether the token should be encoded with padding, leading some implementations to +// always encode without padding. Thus, we need to decode extensions with this engine in order to +// support those implementations. +// +// However, the latest version states that "the base64url value MUST include padding", so when +// generating the header in `build_authorization_header_ext`, URL_SAFE should be used instead of +// this engine. +const URL_SAFE_INDIFFERENT: GeneralPurpose = GeneralPurpose::new( + &alphabet::URL_SAFE, + GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent), +); /// A Token as defined in The Privacy Pass HTTP Authentication Scheme: /// @@ -148,12 +165,48 @@ pub fn build_authorization_header( Ok((header_name, header_value)) } +/// Builds an `Authorize` header according to the following scheme: +/// +/// PrivateToken token="abc...", extensions="def..." +/// +/// # Errors +/// Returns an error if the token or extensions are invalid. +pub fn build_authorization_header_ext( + token: &Token, + extensions: &Extensions, +) -> Result<(HeaderName, HeaderValue), BuildError> { + let value = format!( + // format specified by draft-ietf-privacypass-auth-scheme-extensions + // draft requires that the parameters must be enclosed in double quotes + "PrivateToken token=\"{}\", extensions=\"{}\"", + URL_SAFE.encode( + token + .tls_serialize_detached() + .map_err(|_| BuildError::InvalidToken)? + ), + // See comment above URL_SAFE_INDIFFERENT + URL_SAFE.encode( + extensions + .tls_serialize_detached() + .map_err(|_| BuildError::InvalidExtensions)? + ) + ); + + let header_name = http::header::AUTHORIZATION; + let header_value = HeaderValue::from_str(&value).map_err(|_| BuildError::InvalidToken)?; + + Ok((header_name, header_value)) +} + /// Building error for the `Authorization` header values #[derive(PartialEq, Eq, Error, Debug)] pub enum BuildError { #[error("Invalid token")] /// Invalid token InvalidToken, + #[error("Invalid extensions")] + /// Invalid extensions + InvalidExtensions, } /// Parses an `Authorization` header according to the following scheme: @@ -167,10 +220,24 @@ pub fn parse_authorization_header( ) -> Result, ParseError> { let s = value.to_str().map_err(|_| ParseError::InvalidInput)?; let tokens = parse_header_value(s)?; - let token = tokens[0].clone(); + let token = tokens[0].0.clone(); Ok(token) } +/// Parses an `Authorization` header according to the following scheme: +/// +/// `PrivateToken token=... [, extensions=...]` +/// +/// # Errors +/// Returns an error if the header value is not valid. +pub fn parse_authorization_header_ext( + value: &HeaderValue, +) -> Result<(Token, Option), ParseError> { + let s = value.to_str().map_err(|_| ParseError::InvalidInput)?; + let mut tokens = parse_header_value(s)?; + Ok(tokens.pop().unwrap()) +} + /// Parsing error for the `WWW-Authenticate` header values #[derive(PartialEq, Eq, Error, Debug)] pub enum ParseError { @@ -180,6 +247,9 @@ pub enum ParseError { #[error("Invalid input string")] /// Invalid input string InvalidInput, + #[error("Invalid extensions")] + /// Invalid extensions + InvalidExtensions, } fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { @@ -189,7 +259,7 @@ fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { let (input, _) = tag("=").parse(input)?; let (input, _) = opt_spaces(input)?; let (input, value) = match key.to_lowercase().as_str() { - "token" => base64_char(input)?, + "token" | "extensions" => unquote(input)?, _ => { return Err(nom::Err::Failure(nom::error::make_error( input, @@ -200,13 +270,19 @@ fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { Ok((input, (key, value))) } -fn parse_private_token(input: &str) -> IResult<&str, &str> { +fn parse_private_token(input: &str) -> IResult<&str, (&str, Option<&str>)> { let (input, _) = opt_spaces(input)?; let (input, _) = tag_no_case("PrivateToken").parse(input)?; let (input, _) = many1(space).parse(input)?; - let (input, key_values) = separated_list1(tag(","), parse_key_value).parse(input)?; + let (input, key_values) = separated_list1( + alt((tag(","), tag(" "))), // header could be separated by a space in older specs, so we + // need to support it + parse_key_value, + ) + .parse(input)?; let mut token = None; + let mut extensions = None; let err = nom::Err::Failure(nom::error::make_error(input, nom::error::ErrorKind::Tag)); for (key, value) in key_values { @@ -217,61 +293,162 @@ fn parse_private_token(input: &str) -> IResult<&str, &str> { } token = Some(value) } + "extensions" => { + if extensions.is_some() { + return Err(err); + } + + extensions = Some(value); + } _ => return Err(err), } } let token = token.ok_or(err)?; - Ok((input, token)) + Ok((input, (token, extensions))) } -fn parse_private_tokens(input: &str) -> IResult<&str, Vec<&str>> { +fn parse_private_tokens(input: &str) -> IResult<&str, Vec<(&str, Option<&str>)>> { separated_list1(tag(","), parse_private_token).parse(input) } -fn parse_header_value(input: &str) -> Result>, ParseError> { +fn parse_header_value( + input: &str, +) -> Result, Option)>, ParseError> { let (output, tokens) = parse_private_tokens(input).map_err(|_| ParseError::InvalidInput)?; if !output.is_empty() { return Err(ParseError::InvalidInput); } let tokens = tokens .into_iter() - .map(|token_value| { - Token::tls_deserialize( + .map(|(token_value, extensions_value)| { + let ext = extensions_value + .map(|x| { + let decoded = URL_SAFE_INDIFFERENT + .decode(x) + .map_err(|_| ParseError::InvalidExtensions)?; + Extensions::tls_deserialize(&mut decoded.as_slice()) + .map_err(|_| ParseError::InvalidExtensions) + }) + .transpose()?; + + let token = Token::tls_deserialize( &mut URL_SAFE .decode(token_value) .map_err(|_| ParseError::InvalidToken)? .as_slice(), ) - .map_err(|_| ParseError::InvalidToken) + .map_err(|_| ParseError::InvalidToken)?; + + Ok((token, ext)) }) .collect::, _>>()?; Ok(tokens) } -#[test] -fn builder_parser_test() { +#[cfg(test)] +mod tests { + use crate::TokenType; + use crate::auth::authorize::{ + Token, build_authorization_header, build_authorization_header_ext, + parse_authorization_header, parse_authorization_header_ext, + }; + use crate::common::extensions::{Extension, ExtensionType, Extensions}; + use generic_array::GenericArray; use generic_array::typenum::U32; + use http::HeaderValue; - let nonce = [1u8; 32]; - let challenge_digest = [2u8; 32]; - let token_key_id = [3u8; 32]; - let authenticator = [4u8; 32]; - let token = Token::::new( - TokenType::PrivateP384, - nonce, - challenge_digest, - token_key_id, - *GenericArray::from_slice(&authenticator), - ); - let (header_name, header_value) = build_authorization_header(&token).unwrap(); + #[test] + fn builder_parser_test() { + let nonce = [1u8; 32]; + let challenge_digest = [2u8; 32]; + let token_key_id = [3u8; 32]; + let authenticator = [4u8; 32]; + let token = Token::::new( + TokenType::PrivateP384, + nonce, + challenge_digest, + token_key_id, + *GenericArray::from_slice(&authenticator), + ); - assert_eq!(header_name, http::header::AUTHORIZATION); + let (header_name, header_value) = build_authorization_header(&token).unwrap(); - let token = parse_authorization_header::(&header_value).unwrap(); - assert_eq!(token.token_type(), TokenType::PrivateP384); - assert_eq!(token.nonce(), nonce); - assert_eq!(token.challenge_digest(), &challenge_digest); - assert_eq!(token.token_key_id(), &token_key_id); - assert_eq!(token.authenticator(), &authenticator); + assert_eq!(header_name, http::header::AUTHORIZATION); + + let token = parse_authorization_header::(&header_value).unwrap(); + assert_eq!(token.token_type(), TokenType::PrivateP384); + assert_eq!(token.nonce(), nonce); + assert_eq!(token.challenge_digest(), &challenge_digest); + assert_eq!(token.token_key_id(), &token_key_id); + assert_eq!(token.authenticator(), &authenticator); + } + + #[test] + fn builder_parser_extensions_test() { + let nonce = [1u8; 32]; + let challenge_digest = [2u8; 32]; + let token_key_id = [3u8; 32]; + let authenticator = [4u8; 32]; + let token = Token::::new( + TokenType::PrivateP384, + nonce, + challenge_digest, + token_key_id, + *GenericArray::from_slice(&authenticator), + ); + + let extension = Extension::new(ExtensionType(5), b"hello world".to_vec()); + let extensions = Extensions::new(vec![extension]); + let (header_name, header_value) = + build_authorization_header_ext(&token, &extensions).unwrap(); + + assert_eq!(header_name, http::header::AUTHORIZATION); + + let (token, maybe_extensions) = + parse_authorization_header_ext::(&header_value).unwrap(); + assert_eq!(token.token_type(), TokenType::PrivateP384); + assert_eq!(token.nonce(), nonce); + assert_eq!(token.challenge_digest(), &challenge_digest); + assert_eq!(token.token_key_id(), &token_key_id); + assert_eq!(token.authenticator(), &authenticator); + assert_eq!(maybe_extensions, Some(extensions)); + } + + /// This is the same test as `builder_parser_extensions_test`, but we replace the `, ` + /// separator with ` ` (single space) to make sure we can handle tokens generated by clients + /// using an older version of the TOKEN-EXTENSION spec. + #[test] + fn rfc_9110_regression_test() { + let nonce = [1u8; 32]; + let challenge_digest = [2u8; 32]; + let token_key_id = [3u8; 32]; + let authenticator = [4u8; 32]; + let token = Token::::new( + TokenType::PrivateP384, + nonce, + challenge_digest, + token_key_id, + *GenericArray::from_slice(&authenticator), + ); + + let extension = Extension::new(ExtensionType(5), b"hello world".to_vec()); + let extensions = Extensions::new(vec![extension]); + let (header_name, header_value) = + build_authorization_header_ext(&token, &extensions).unwrap(); + + let header_value = + HeaderValue::from_str(&header_value.to_str().unwrap().replace(", ", " ")).unwrap(); + + assert_eq!(header_name, http::header::AUTHORIZATION); + + let (token, maybe_extensions) = + parse_authorization_header_ext::(&header_value).unwrap(); + assert_eq!(token.token_type(), TokenType::PrivateP384); + assert_eq!(token.nonce(), nonce); + assert_eq!(token.challenge_digest(), &challenge_digest); + assert_eq!(token.token_key_id(), &token_key_id); + assert_eq!(token.authenticator(), &authenticator); + assert_eq!(maybe_extensions, Some(extensions)); + } } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 920f1dc..fceefa0 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -2,9 +2,11 @@ use nom::{ IResult, Parser, - bytes::complete::{is_a, take_while1}, + branch::alt, + bytes::complete::{is_a, tag, take_while1}, combinator::verify, multi::many0, + sequence::delimited, }; use std::str::FromStr; @@ -52,3 +54,7 @@ pub(crate) fn surrounded_by_alphanumeric(input: &str) -> bool { true } + +pub(crate) fn unquote(input: &str) -> IResult<&str, &str> { + alt((delimited(tag("\""), base64_char, tag("\"")), base64_char)).parse(input) +} diff --git a/src/common/extensions.rs b/src/common/extensions.rs new file mode 100644 index 0000000..aec7f44 --- /dev/null +++ b/src/common/extensions.rs @@ -0,0 +1,120 @@ +//! Types and functions related to the Extensions parameter. +//! +//! Specified in `draft-ietf-privacypass-auth-scheme-extensions`. + +use std::io::{Read, Write}; +use tls_codec::{Deserialize, Serialize, Size, TlsByteVecU16, TlsVecU16}; +use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; + +/// Type of extension. +/// +/// Extension types are to be defined by the client, not by this crate +#[derive(Clone, Copy, Debug, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserialize)] +pub struct ExtensionType(pub u16); + +impl ExtensionType { + /// Reserved by `draft-ietf-privacypass-auth-scheme-extensions` + pub const RESERVED: ExtensionType = ExtensionType(0); +} + +/// A single extension. +/// +/// Contains opaque byte data whose semantics are determined by the type. +#[derive(Clone, Debug, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserialize)] +pub struct Extension { + extension_type: ExtensionType, + extension_data: TlsByteVecU16, +} + +impl Extension { + /// Create a new Extension. + /// + /// `data` should be byte data whose semantics are determined by `ext_type`. + pub fn new(ext_type: ExtensionType, data: Vec) -> Extension { + Extension { + extension_type: ext_type, + extension_data: TlsByteVecU16::new(data), + } + } +} + +/// A set of extensions. +/// +/// Contains a list of Extension values. +#[derive(Clone, Debug, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserialize)] +pub struct Extensions { + extensions: TlsVecU16, +} + +impl Extensions { + /// Create a new `Extensions`. + pub fn new(extensions: Vec) -> Extensions { + Extensions { + extensions: TlsVecU16::new(extensions), + } + } +} + +/// Denotes whether a certain extension type is required or optional. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtensionEntry { + is_required: bool, + extension_type: ExtensionType, +} + +// we need to implement these by hand because tls_codec doesn't ship an impl of these for `bool` +impl Size for ExtensionEntry { + fn tls_serialized_len(&self) -> usize { + (self.is_required as u8).tls_serialized_len() + self.extension_type.tls_serialized_len() + } +} + +impl Serialize for ExtensionEntry { + fn tls_serialize(&self, writer: &mut W) -> Result { + Ok((self.is_required as u8).tls_serialize(writer)? + + self.extension_type.tls_serialize(writer)?) + } +} + +impl Deserialize for ExtensionEntry { + fn tls_deserialize(bytes: &mut R) -> Result { + // extensions spec requires that bools are either 0 or 1 + let is_required = match u8::tls_deserialize(bytes)? { + 0 => false, + 1 => true, + _ => return Err(tls_codec::Error::InvalidInput), + }; + + let extension_type = ExtensionType::tls_deserialize(bytes)?; + + Ok(Self { + is_required, + extension_type, + }) + } +} + +impl ExtensionEntry { + /// Creates a new `ExtensionEntry`. + pub fn new(is_required: bool, extension_type: ExtensionType) -> ExtensionEntry { + Self { + is_required, + extension_type, + } + } +} + +/// A set of extension entries. +#[derive(Clone, Debug, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserialize)] +pub struct ExtensionSet { + extension_types: TlsVecU16, +} + +impl ExtensionSet { + /// Creates a new `ExtensionSet` + pub fn new(extension_types: Vec) -> ExtensionSet { + ExtensionSet { + extension_types: TlsVecU16::new(extension_types), + } + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index a564117..9c44d6e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -2,5 +2,6 @@ //! tokens pub mod errors; +pub mod extensions; pub mod private; pub mod store; From 42ee3b46f2a2ddf5486d37a7a5e74d4783c1e57f Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Fri, 5 Jun 2026 16:18:18 -0500 Subject: [PATCH 03/13] Implement the public metadata issuance protocol --- src/generic_tokens/request.rs | 4 +- src/lib.rs | 4 + src/public_tokens/mod.rs | 30 +++++- src/public_tokens/request.rs | 70 +++++++++++--- src/public_tokens/response.rs | 39 +++++--- src/public_tokens/server.rs | 175 +++++++++++++++++++++++++++++----- tests/public_tokens.rs | 109 +++++++++++++++++++++ 7 files changed, 380 insertions(+), 51 deletions(-) diff --git a/src/generic_tokens/request.rs b/src/generic_tokens/request.rs index b5c5dda..cf48a2b 100644 --- a/src/generic_tokens/request.rs +++ b/src/generic_tokens/request.rs @@ -183,12 +183,12 @@ impl Deserialize for GenericTokenRequest { let mut all_bytes = (peeked).chain(bytes); match token_type { - TokenType::PrivateP384 => { + TokenType::PrivateP384 | TokenType::PrivateMetadata => { let token_request = crate::private_tokens::TokenRequest::tls_deserialize(&mut all_bytes)?; Ok(GenericTokenRequest::PrivateP384(Box::new(token_request))) } - TokenType::Public => { + TokenType::Public | TokenType::PublicMetadata => { let token_request = crate::public_tokens::TokenRequest::tls_deserialize(&mut all_bytes)?; Ok(GenericTokenRequest::Public(Box::new(token_request))) diff --git a/src/lib.rs b/src/lib.rs index 5b874f9..060912f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,10 @@ pub enum TokenType { Public = 2, /// Private ristretto255 token PrivateRistretto255 = 5, + /// Publicly verifiable token with public metadata + PublicMetadata = 0xDA7A, + /// Privately verifiable token with public metadata + PrivateMetadata = 0xDA7B, } /// Token key ID diff --git a/src/public_tokens/mod.rs b/src/public_tokens/mod.rs index 75c19b7..c8e265e 100644 --- a/src/public_tokens/mod.rs +++ b/src/public_tokens/mod.rs @@ -6,7 +6,9 @@ use typenum::U256; use blind_rsa_signatures::Error as BlindRsaError; -use crate::{TokenKeyId, TruncatedTokenKeyId, auth::authorize::Token, truncate_token_key_id}; +use crate::{ + TokenKeyId, TokenType, TruncatedTokenKeyId, auth::authorize::Token, truncate_token_key_id, +}; pub mod request; pub mod response; @@ -45,3 +47,29 @@ fn public_key_to_token_key_id(public_key: &PublicKey) -> Result { + /// Privacy Pass issuance protocol + #[default] + Basic, + /// Privacy Pass issuance with Public Metadata + /// + /// Specified by `draft-ietf-privacypass-public-metadata-issuance`. + PublicMetadata { + /// A reference to the public metadata, cryptographically bound to + /// the generated token. + metadata: &'a [u8], + }, +} + +impl<'a> TokenProtocol<'a> { + /// Returns the token type associated with this protocol + pub fn token_type(&self) -> TokenType { + match self { + TokenProtocol::Basic => TokenType::Public, + TokenProtocol::PublicMetadata { .. } => TokenType::PublicMetadata, + } + } +} diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index f6970e9..d981c09 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -1,15 +1,16 @@ //! Request implementation of the Publicly Verifiable Token protocol. -use blind_rsa_signatures::BlindingResult; use blind_rsa_signatures::reexports::rand::CryptoRng; +use blind_rsa_signatures::{BlindingResult, pbrsa::PartiallyBlindPublicKey}; use log::warn; use super::PublicKey; use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; +use crate::public_tokens::server::PbrsaPublicKey; use crate::{ ChallengeDigest, Nonce, TokenInput, TokenType, auth::authenticate::TokenChallenge, - common::errors::IssueTokenRequestError, truncate_token_key_id, + common::errors::IssueTokenRequestError, public_tokens::TokenProtocol, truncate_token_key_id, }; use super::{NK, public_key_to_token_key_id}; @@ -21,6 +22,8 @@ pub struct TokenState { pub(crate) challenge_digest: ChallengeDigest, pub(crate) blinding_result: BlindingResult, pub(crate) public_key: PublicKey, + pub(crate) metadata: Option>, + pub(crate) derived_pk: Option, } /// Token request as specified in the spec: @@ -40,14 +43,15 @@ pub struct TokenRequest { } impl TokenRequest { - /// Issue a new token request. + /// Issue a new token request using the given protocol. /// /// # Errors - /// Returns an error if the challenge is invalid. - pub fn new( + /// Returns an error if the challenge is invalid or if blinding the token input fails.. + pub fn new_with_protocol( rng: &mut R, public_key: PublicKey, challenge: &TokenChallenge, + protocol: TokenProtocol, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { let mut nonce: Nonce = [0u8; 32]; rng.fill_bytes(&mut nonce); @@ -63,26 +67,52 @@ impl TokenRequest { } })?; + let token_input = + TokenInput::new(protocol.token_type(), nonce, challenge_digest, token_key_id); + // nonce = random(32) // challenge_digest = SHA256(challenge) // token_input = concat(0x0002, nonce, challenge_digest, token_key_id) // blinded_msg, blind_inv = rsabssa_blind(pkI, token_input) - let token_input = TokenInput::new(TokenType::Public, nonce, challenge_digest, token_key_id); + let mut m: Option<_> = None; + let mut dpk: Option<_> = None; - let blinding_result = public_key - .blind(rng, token_input.serialize()) - .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; + let blinding_result = match protocol { + TokenProtocol::Basic => public_key + .blind(rng, token_input.serialize()) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?, + TokenProtocol::PublicMetadata { metadata } => { + let pbrsa_pk: PbrsaPublicKey = + PartiallyBlindPublicKey::new(public_key.as_ref().clone()); + let derived_pk = pbrsa_pk + .derive_public_key_for_metadata(metadata) + .inspect_err(|e| warn!(error:% = e; "Failed to derive metadata public key")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + m = Some(metadata.to_vec()); + dpk = Some(derived_pk.clone()); + + derived_pk + .blind(rng, token_input.serialize(), Some(metadata)) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })? + } + }; debug_assert!(blinding_result.blind_message.len() == NK); let mut blinded_msg = [0u8; NK]; blinded_msg.copy_from_slice(blinding_result.blind_message.as_slice()); let token_request = TokenRequest { - token_type: TokenType::Public, + token_type: protocol.token_type(), truncated_token_key_id: truncate_token_key_id(&token_key_id), blinded_msg, }; @@ -92,7 +122,21 @@ impl TokenRequest { token_input, challenge_digest, public_key, + metadata: m, + derived_pk: dpk, }; Ok((token_request, token_state)) } + + /// Issue a new token request. + /// + /// # Errors + /// Returns an error if the challenge is invalid. + pub fn new( + rng: &mut R, + public_key: PublicKey, + challenge: &TokenChallenge, + ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { + Self::new_with_protocol(rng, public_key, challenge, TokenProtocol::Basic) + } } diff --git a/src/public_tokens/response.rs b/src/public_tokens/response.rs index 7f9fb7c..730783d 100644 --- a/src/public_tokens/response.rs +++ b/src/public_tokens/response.rs @@ -5,7 +5,7 @@ use generic_array::{GenericArray, typenum::U256}; use log::warn; use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; -use crate::{TokenType, auth::authorize::Token, common::errors::IssueTokenError}; +use crate::{auth::authorize::Token, common::errors::IssueTokenError}; use super::{NK, PublicToken, TokenState}; @@ -29,19 +29,36 @@ impl TokenResponse { pub fn issue_token(self, token_state: &TokenState) -> Result { // authenticator = rsabssa_finalize(pkI, nonce, blind_sig, blind_inv) let token_input = token_state.token_input.serialize(); - let token_type = TokenType::Public; + let token_type = token_state.token_input.token_type; let blind_sig = BlindSignature(self.blind_sig.to_vec()); - let signature = token_state - .public_key - .finalize(&blind_sig, &token_state.blinding_result, token_input) - .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) - .map_err(|source| IssueTokenError::SignatureFinalizationFailed { - token_type, - source, - })?; + + let signature = if let Some(derived_pk) = &token_state.derived_pk { + derived_pk + .finalize( + &blind_sig, + &token_state.blinding_result, + token_input, + token_state.metadata.as_deref(), + ) + .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) + .map_err(|source| IssueTokenError::SignatureFinalizationFailed { + token_type, + source, + })? + } else { + token_state + .public_key + .finalize(&blind_sig, &token_state.blinding_result, token_input) + .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) + .map_err(|source| IssueTokenError::SignatureFinalizationFailed { + token_type, + source, + })? + }; + let authenticator: GenericArray = *GenericArray::from_slice(&signature[0..256]); Ok(Token::new( - TokenType::Public, + token_type, token_state.token_input.nonce, token_state.challenge_digest, token_state.token_input.token_key_id, diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index 79eca61..685fced 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -1,9 +1,13 @@ //! Server-side implementation of Publicly Verifiable Token protocol. use async_trait::async_trait; +use blind_rsa_signatures::pbrsa::{ + PartiallyBlindKeyPair, PartiallyBlindPublicKey, PartiallyBlindSecretKey, +}; use blind_rsa_signatures::reexports::rand::CryptoRng; use blind_rsa_signatures::{ - Deterministic, KeyPair as GenericKeyPair, PSS, PublicKey as GenericPublicKey, Sha384, Signature, + Deterministic, KeyPair as GenericKeyPair, PSS, PublicKey as GenericPublicKey, SecretKey, + Sha384, Signature, }; use generic_array::ArrayLength; use log::{debug, warn}; @@ -11,8 +15,12 @@ use log::{debug, warn}; type KeyPair = GenericKeyPair; type PublicKey = GenericPublicKey; +pub(crate) type PbrsaKeyPair = PartiallyBlindKeyPair; +pub(crate) type PbrsaPublicKey = PartiallyBlindPublicKey; + +use crate::public_tokens::TokenProtocol; use crate::{ - COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TokenType, TruncatedTokenKeyId, + COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TruncatedTokenKeyId, auth::authorize::Token, common::errors::{CreateKeypairError, IssueTokenResponseError, RedeemTokenError}, }; @@ -95,6 +103,23 @@ pub fn serialize_public_key( public_key.to_spki() } +fn pbrsa_to_keypair(pbrsa: &PbrsaKeyPair) -> Result { + let sk_der = pbrsa.sk.to_der()?; + let sk = SecretKey::from_der(&sk_der)?; + let pk = sk.public_key()?; + + Ok(KeyPair { sk, pk }) +} + +fn keypair_to_pbrsa(keypair: &KeyPair) -> Result { + let sk_der = keypair.sk.to_der()?; + // validates that the keypair has safe primes, e.g. was generated as a PBRSA keypair + let sk = PartiallyBlindSecretKey::from_der(&sk_der)?; + let pk = sk.public_key()?; + + Ok(PbrsaKeyPair { sk, pk }) +} + const KEYSIZE_IN_BITS: usize = 2048; const KEYSIZE_IN_BYTES: usize = KEYSIZE_IN_BITS / 8; @@ -141,32 +166,90 @@ impl IssuerServer { Err(CreateKeypairError::CollisionExhausted) } - /// Issues a new token response. + /// Creates a new partially-blinded keypair and inserts it into the key store. /// /// # Errors - /// Returns an error if the token request is invalid. - pub async fn issue_token_response( + /// Returns an error if creating the keypair or converting it to plain RSA fails. + pub async fn create_keypair_pbrsa( + &self, + rng: &mut R, + key_store: &IKS, + ) -> Result { + for _ in 0..COLLISION_AVOIDANCE_ATTEMPTS { + let pbrsa_key_pair = PbrsaKeyPair::generate(rng, KEYSIZE_IN_BITS) + .inspect_err(|e| debug!(error:% = e; "Failed to generate RSA keypair")) + .map_err(|source| CreateKeypairError::KeyGenerationFailed { source })?; + + let key_pair = pbrsa_to_keypair(&pbrsa_key_pair) + .inspect_err(|e| debug!(error:% = e; "Key Conversion Failed")) + .map_err(|source| CreateKeypairError::KeyGenerationFailed { source })?; + + let truncated_token_key_id = truncate_token_key_id( + &public_key_to_token_key_id(&key_pair.pk) + .map_err(|source| CreateKeypairError::KeySerializationFailed { source })?, + ); + + if key_store.get(&truncated_token_key_id).await.is_some() { + continue; + } + + let public_key = key_pair.pk.clone(); + + if key_store.insert(truncated_token_key_id, key_pair).await { + return Ok(public_key); + } + } + Err(CreateKeypairError::CollisionExhausted) + } + + /// Issues a new token response using the given protocol. + /// + /// # Errors + /// Returns an error if the token request is invalid, or if protocol == PublicMetadata and the + /// key is not PBRSA compatible. + pub async fn issue_token_response_protocol( &self, key_store: &IKS, token_request: TokenRequest, + protocol: TokenProtocol<'_>, ) -> Result { - if token_request.token_type != TokenType::Public { + if token_request.token_type != protocol.token_type() { return Err(IssueTokenResponseError::InvalidTokenType { - expected: TokenType::Public, + expected: protocol.token_type(), found: token_request.token_type, }); } + let key_pair = key_store .get(&token_request.truncated_token_key_id) .await .ok_or(IssueTokenResponseError::KeyIdNotFound)?; // blind_sig = rsabssa_blind_sign(skI, TokenRequest.blinded_msg) - let blind_signature = key_pair - .sk - .blind_sign(token_request.blinded_msg) - .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) - .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?; + let blind_signature = match protocol { + TokenProtocol::Basic => key_pair + .sk + .blind_sign(token_request.blinded_msg) + .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) + .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?, + TokenProtocol::PublicMetadata { metadata } => { + let pbrsa = keypair_to_pbrsa(&key_pair) + .inspect_err( + |e| warn!(error:% = e; "Stored key not PBRSA compatible (no safe primes)"), + ) + .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?; + + let derived_sk = pbrsa + .derive_secret_key_for_metadata(metadata) + .inspect_err(|e| warn!(error:% = e; "Failed to derive augmented secret key")) + .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?; + + derived_sk + .blind_sign(token_request.blinded_msg) + .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) + .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })? + } + }; debug_assert!(blind_signature.len() == NK); let mut blind_sig = [0u8; NK]; @@ -175,6 +258,19 @@ impl IssuerServer { Ok(TokenResponse { blind_sig }) } + /// Issues a new token response. + /// + /// # Errors + /// Returns an error if the token request is invalid. + pub async fn issue_token_response( + &self, + key_store: &IKS, + token_request: TokenRequest, + ) -> Result { + self.issue_token_response_protocol(key_store, token_request, TokenProtocol::Basic) + .await + } + /// Sets the given keypair. /// /// # Errors @@ -205,23 +301,24 @@ impl OriginServer { Self {} } - /// Redeems a token. + /// Redeems a token using the given protocol. /// /// # Errors - /// Returns an error if the token is invalid. - pub async fn redeem_token( + /// Returns an error if the token is invalid or deriving the key for the metadata fails. + pub async fn redeem_token_protocol( &self, key_store: &OKS, nonce_store: &NS, token: Token, + protocol: TokenProtocol<'_>, ) -> Result<(), RedeemTokenError> { - let token_type = token.token_type(); - if token_type != TokenType::Public { + if token.token_type() != protocol.token_type() { return Err(RedeemTokenError::TokenTypeMismatch { - expected: TokenType::Public, - found: token_type, + expected: protocol.token_type(), + found: token.token_type(), }); } + let authenticator_len = token.authenticator().len(); if authenticator_len != KEYSIZE_IN_BYTES { return Err(RedeemTokenError::InvalidAuthenticatorLength { @@ -229,9 +326,10 @@ impl OriginServer { found: authenticator_len, }); } + let nonce = token.nonce(); let token_input = TokenInput::new( - token_type, + token.token_type(), nonce, *token.challenge_digest(), *token.token_key_id(), @@ -251,15 +349,30 @@ impl OriginServer { let signature = Signature(token.authenticator().to_vec()); let token_input_bytes = token_input.serialize(); - let verified = public_keys.iter().any(|public_key| { - public_key + let verified = public_keys.iter().any(|public_key| match protocol { + TokenProtocol::Basic => public_key .verify(&signature, None, &token_input_bytes) .inspect_err(|e| warn!(error:% = e; "Verify failed")) - .is_ok() + .is_ok(), + TokenProtocol::PublicMetadata { metadata } => { + let pbrsa_pk = PbrsaPublicKey::new(public_key.as_ref().clone()); + match pbrsa_pk.derive_public_key_for_metadata(metadata) { + Ok(derived) => derived + .verify(&signature, None, &token_input_bytes, Some(metadata)) + .inspect_err(|e| warn!(error:% = e; "Verify failed")) + .is_ok(), + Err(e) => { + warn!(error:% = e; "Key derivation failed"); + false + } + } + } }); if !verified { - return Err(RedeemTokenError::InvalidSignature { token_type }); + return Err(RedeemTokenError::InvalidSignature { + token_type: token.token_type(), + }); } Ok(()) } @@ -276,4 +389,18 @@ impl OriginServer { } } } + + /// Redeems a token. + /// + /// # Errors + /// Returns an error if the token is invalid. + pub async fn redeem_token( + &self, + key_store: &OKS, + nonce_store: &NS, + token: Token, + ) -> Result<(), RedeemTokenError> { + self.redeem_token_protocol(key_store, nonce_store, token, TokenProtocol::Basic) + .await + } } diff --git a/tests/public_tokens.rs b/tests/public_tokens.rs index 3aa5a6d..8bf26ca 100644 --- a/tests/public_tokens.rs +++ b/tests/public_tokens.rs @@ -1,5 +1,8 @@ +use blind_rsa_signatures::reexports::crypto_bigint::BoxedUint; use blind_rsa_signatures::reexports::rand::rng; +use blind_rsa_signatures::reexports::rsa::RsaPrivateKey; use blind_rsa_signatures::{Deterministic, KeyPair, PSS, PublicKey, SecretKey, Sha384}; +use privacypass::public_tokens::TokenProtocol; use privacypass::{ TokenType, auth::authenticate::TokenChallenge, @@ -170,3 +173,109 @@ async fn redeem_token_supports_multiple_public_keys_per_truncated_id() { .await .unwrap(); } + +fn hex_to_boxed_uint(h: &[u8]) -> BoxedUint { + let bytes: Vec = h + .chunks(2) + .map(|x| u8::from_str_radix(std::str::from_utf8(x).unwrap(), 16).unwrap()) + .collect(); + let bits_precision = bytes.len() as u32 * 8; + BoxedUint::from_be_slice(&bytes, bits_precision).unwrap() +} + +#[tokio::test] +async fn public_metadata_tokens() { + // https://gist.github.com/chris-wood/b77536febb25a5a11af428afff77820a + const P_ENC: &[u8] = b"dcd90af1be463632c0d5ea555256a20605af3db667475e190e3af12a34a3324c46a3094062c59fb4b249e0ee6afba8bee14e0276d126c99f4784b23009bf6168ff628ac1486e5ae8e23ce4d362889de4df63109cbd90ef93db5ae64372bfe1c55f832766f21e94ea3322eb2182f10a891546536ba907ad74b8d72469bea396f3"; + const Q_ENC: &[u8] = b"f8ba5c89bd068f57234a3cf54a1c89d5b4cd0194f2633ca7c60b91a795a56fa8c8686c0e37b1c4498b851e3420d08bea29f71d195cfbd3671c6ddc49cf4c1db5b478231ea9d91377ffa98fe95685fca20ba4623212b2f2def4da5b281ed0100b651f6db32112e4017d831c0da668768afa7141d45bbc279f1e0f8735d74395b3"; + const N_ENC: &[u8] = b"d6930820f71fe517bf3259d14d40209b02a5c0d3d61991c731dd7da39f8d69821552e2318d6c9ad897e603887a476ea3162c1205da9ac96f02edf31df049bd55f142134c17d4382a0e78e275345f165fbe8e49cdca6cf5c726c599dd39e09e75e0f330a33121e73976e4facba9cfa001c28b7c96f8134f9981db6750b43a41710f51da4240fe03106c12acb1e7bb53d75ec7256da3fddd0718b89c365410fce61bc7c99b115fb4c3c318081fa7e1b65a37774e8e50c96e8ce2b2cc6b3b367982366a2bf9924c4bafdb3ff5e722258ab705c76d43e5f1f121b984814e98ea2b2b8725cd9bc905c0bc3d75c2a8db70a7153213c39ae371b2b5dc1dafcb19d6fae9"; + const E_ENC: &[u8] = b"010001"; + const D_ENC: &[u8] = b"4e21356983722aa1adedb084a483401c1127b781aac89eab103e1cfc52215494981d18dd8028566d9d499469c25476358de23821c78a6ae43005e26b394e3051b5ca206aa9968d68cae23b5affd9cbb4cb16d64ac7754b3cdba241b72ad6ddfc000facdb0f0dd03abd4efcfee1730748fcc47b7621182ef8af2eeb7c985349f62ce96ab373d2689baeaea0e28ea7d45f2d605451920ca4ea1f0c08b0f1f6711eaa4b7cca66d58a6b916f9985480f90aca97210685ac7b12d2ec3e30a1c7b97b65a18d38a93189258aa346bf2bc572cd7e7359605c20221b8909d599ed9d38164c9c4abf396f897b9993c1e805e574d704649985b600fa0ced8e5427071d7049d"; + let sk = SecretKey::new( + RsaPrivateKey::from_components( + hex_to_boxed_uint(N_ENC), + hex_to_boxed_uint(E_ENC), + hex_to_boxed_uint(D_ENC), + vec![hex_to_boxed_uint(P_ENC), hex_to_boxed_uint(Q_ENC)], + ) + .unwrap(), + ); + + let public_key = sk.public_key().unwrap(); + + let rng = &mut rng(); + + // Server: Instantiate in-memory keystore and nonce store. + let issuer_key_store = IssuerMemoryKeyStore::default(); + let origin_key_store = OriginMemoryKeyStore::default(); + let nonce_store = MemoryNonceStore::default(); + + // Server: Create servers for issuer and origin + let issuer_server = IssuerServer::new(); + let origin_server = OriginServer::new(); + + // Issuer server: Create a new keypair + let key_pair = KeyPair { + sk, + pk: public_key.clone(), + }; + let token_key_id = public_key_to_truncated_token_key_id(&key_pair.pk).unwrap(); + + issuer_key_store + .insert(token_key_id, key_pair.clone()) + .await; + + origin_key_store + .insert( + public_key_to_truncated_token_key_id(&public_key).unwrap(), + public_key.clone(), + ) + .await; + + // Generate a challenge + let token_challenge = TokenChallenge::new( + TokenType::PublicMetadata, + "example.com", + None, + &["example.com".to_string()], + ); + let challenge_digest = token_challenge.digest().unwrap(); + let metadata = b"Hello world"; + let protocol = TokenProtocol::PublicMetadata { metadata }; + + // Client: Prepare a TokenRequest after having received a challenge + let (token_request, token_state) = + TokenRequest::new_with_protocol(rng, public_key, &token_challenge, protocol.clone()) + .unwrap(); + + // Issuer server: Issue a TokenResponse + let token_response = issuer_server + .issue_token_response_protocol(&issuer_key_store, token_request, protocol.clone()) + .await + .unwrap(); + + // Client: Turn the TokenResponse into a Token + let token = token_response.issue_token(&token_state).unwrap(); + + // Origin server: Compare the challenge digest + assert_eq!(token.challenge_digest(), &challenge_digest); + + // Origin server: Redeem the token + origin_server + .redeem_token_protocol( + &origin_key_store, + &nonce_store, + token.clone(), + protocol.clone(), + ) + .await + .unwrap(); + + // Origin server: Test double spend protection + assert_eq!( + origin_server + .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol.clone()) + .await, + Err(RedeemTokenError::DoubleSpending) + ); +} From d77836e7c9c4b76a7757b0480600611e0b9b7277 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Thu, 11 Jun 2026 12:34:23 -0500 Subject: [PATCH 04/13] Cache validated PBRSA keypairs --- src/public_tokens/server.rs | 59 ++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index 685fced..1451ea8 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -1,5 +1,8 @@ //! Server-side implementation of Publicly Verifiable Token protocol. +use std::collections::HashMap; +use std::sync::{PoisonError, RwLock}; + use async_trait::async_trait; use blind_rsa_signatures::pbrsa::{ PartiallyBlindKeyPair, PartiallyBlindPublicKey, PartiallyBlindSecretKey, @@ -18,6 +21,7 @@ type PublicKey = GenericPublicKey; pub(crate) type PbrsaKeyPair = PartiallyBlindKeyPair; pub(crate) type PbrsaPublicKey = PartiallyBlindPublicKey; +use crate::TokenKeyId; use crate::public_tokens::TokenProtocol; use crate::{ COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TruncatedTokenKeyId, @@ -126,13 +130,46 @@ const KEYSIZE_IN_BYTES: usize = KEYSIZE_IN_BITS / 8; /// Server-side implementation of Publicly Verifiable Token protocol for /// issuers. #[derive(Default, Debug)] -pub struct IssuerServer {} +pub struct IssuerServer { + /// Caches the validated PBRSA conversion so we don't re-validate on every token response. + /// Keyed by the full key id so the map stays stable across rotation + pbrsa_cache: RwLock>, +} impl IssuerServer { /// Creates a new server. #[must_use] - pub const fn new() -> Self { - Self {} + pub fn new() -> Self { + Self { + pbrsa_cache: RwLock::new(HashMap::new()), + } + } + + /// Gets the PBRSA keypair associated with the given Blind-RSA keypair. + /// + /// The given keypair MUST have been generated as a PBRSA keypair, + /// i.e. using safe primes (p and q where (p-1)/2 and (q-1)/2 are also prime) + fn get_pbrsa_pair( + &self, + key_pair: &KeyPair, + ) -> Result { + let key_id = public_key_to_token_key_id(&key_pair.pk)?; + + if let Some(pbrsa) = self + .pbrsa_cache + .read() + .unwrap_or_else(PoisonError::into_inner) + .get(&key_id) + { + Ok(pbrsa.clone()) + } else { + let pbrsa = keypair_to_pbrsa(key_pair)?; + self.pbrsa_cache + .write() + .unwrap_or_else(PoisonError::into_inner) + .insert(key_id, pbrsa.clone()); + Ok(pbrsa) + } } /// Creates a new keypair and inserts it into the key store. @@ -184,10 +221,10 @@ impl IssuerServer { .inspect_err(|e| debug!(error:% = e; "Key Conversion Failed")) .map_err(|source| CreateKeypairError::KeyGenerationFailed { source })?; - let truncated_token_key_id = truncate_token_key_id( - &public_key_to_token_key_id(&key_pair.pk) - .map_err(|source| CreateKeypairError::KeySerializationFailed { source })?, - ); + let token_key_id = public_key_to_token_key_id(&key_pair.pk) + .map_err(|source| CreateKeypairError::KeySerializationFailed { source })?; + + let truncated_token_key_id = truncate_token_key_id(&token_key_id); if key_store.get(&truncated_token_key_id).await.is_some() { continue; @@ -196,6 +233,11 @@ impl IssuerServer { let public_key = key_pair.pk.clone(); if key_store.insert(truncated_token_key_id, key_pair).await { + self.pbrsa_cache + .write() + .unwrap_or_else(PoisonError::into_inner) + .insert(token_key_id, pbrsa_key_pair); + return Ok(public_key); } } @@ -233,7 +275,8 @@ impl IssuerServer { .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?, TokenProtocol::PublicMetadata { metadata } => { - let pbrsa = keypair_to_pbrsa(&key_pair) + let pbrsa = self + .get_pbrsa_pair(&key_pair) .inspect_err( |e| warn!(error:% = e; "Stored key not PBRSA compatible (no safe primes)"), ) From 5c715f2b58d6720721c3228ff9a3c303f4826f29 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Fri, 12 Jun 2026 10:35:43 -0500 Subject: [PATCH 05/13] Document compatibility fixes --- Cargo.toml | 2 +- src/auth/mod.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cd244f6..f1cd045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ subtle = "2" thiserror = "2" tls_codec = { version = "0.4.2" } tls_codec_derive = "0.4.2" -voprf = { version = "=0.6.0-pre.0", features = ["serde"] } +voprf = { version = "=0.6.0-pre.0", features = ["serde"] } # version pinned to avoid incompatibility with rand_core in 0.6.0-pre.1 p384 = { version = "0.13.0", default-features = false, features = [ "hash2curve", "voprf", diff --git a/src/auth/mod.rs b/src/auth/mod.rs index fceefa0..0b0bf94 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -56,5 +56,8 @@ pub(crate) fn surrounded_by_alphanumeric(input: &str) -> bool { } pub(crate) fn unquote(input: &str) -> IResult<&str, &str> { + // older versions of the extensions draft specified that values should not be surrounded with double + // quotes, but the newest version of the draft specifies that values should be quoted. + // The use of alt() lets us support both versions alt((delimited(tag("\""), base64_char, tag("\"")), base64_char)).parse(input) } From 9b1b14314dc175d3d90cfbd15326417ade0ea2e0 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Mon, 8 Jun 2026 11:33:48 -0500 Subject: [PATCH 06/13] Add parse_authorization_str and verify_token, minor cleanups + fixes --- README.md | 13 ++--- src/auth/authorize.rs | 36 +++++++++++-- src/public_tokens/mod.rs | 2 +- src/public_tokens/request.rs | 14 ++--- src/public_tokens/server.rs | 100 ++++++++++++++++++++++++----------- tests/public_tokens.rs | 14 ++--- 6 files changed, 120 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index bc9b9d6..0f39591 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,13 @@ with an async API. ## Protocols -| Token type | Spec | Primitive | Cipher suites | -|---|---|---|---| -| Privately Verifiable | [RFC 9578 §5](https://www.rfc-editor.org/rfc/rfc9578.html#section-5) | VOPRF | P384-SHA384, Ristretto255-SHA512 | -| Publicly Verifiable | [RFC 9578 §6](https://www.rfc-editor.org/rfc/rfc9578.html#section-6) | Blind RSA | RSA-2048, SHA-384, PSS | -| Amortized (Batch VOPRF) | [draft-ietf-privacypass-batched-tokens §5](https://www.ietf.org/archive/id/draft-ietf-privacypass-batched-tokens-04.html#section-5) | VOPRF | P384-SHA384, Ristretto255-SHA512 | -| Generic Batch | [draft-ietf-privacypass-batched-tokens §6](https://www.ietf.org/archive/id/draft-ietf-privacypass-batched-tokens-04.html#section-6) | Mixed | All of the above | +| Token type | Spec | Primitive | Cipher suites | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -------------------------------- | +| Privately Verifiable | [RFC 9578 §5](https://www.rfc-editor.org/rfc/rfc9578.html#section-5) | VOPRF | P384-SHA384, Ristretto255-SHA512 | +| Publicly Verifiable | [RFC 9578 §6](https://www.rfc-editor.org/rfc/rfc9578.html#section-6) | Blind RSA | RSA-2048, SHA-384, PSS | +| Publicly Verifiable with Public Metadata | [draft-ietf-privacypass-public-metadata-issuance §6](https://www.ietf.org/archive/id/draft-ietf-privacypass-public-metadata-issuance-03.html#section-6) | Partially-Blind RSA | RSA-2048, SHA-384, PSS | +| Amortized (Batch VOPRF) | [draft-ietf-privacypass-batched-tokens §5](https://www.ietf.org/archive/id/draft-ietf-privacypass-batched-tokens-04.html#section-5) | VOPRF | P384-SHA384, Ristretto255-SHA512 | +| Generic Batch | [draft-ietf-privacypass-batched-tokens §6](https://www.ietf.org/archive/id/draft-ietf-privacypass-batched-tokens-04.html#section-6) | Mixed | All of the above | The `auth` module provides construction and parsing of the HTTP `WWW-Authenticate` / `Authorization` headers used in the Privacy Pass diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 2408723..34ea573 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -219,14 +219,25 @@ pub fn parse_authorization_header( value: &HeaderValue, ) -> Result, ParseError> { let s = value.to_str().map_err(|_| ParseError::InvalidInput)?; - let tokens = parse_header_value(s)?; - let token = tokens[0].0.clone(); + + parse_authorization_str(s) +} + +/// Parses an `Authorization` string according to the following scheme: +/// +/// `PrivateToken token=...` +/// +/// # Errors +/// Returns an error if the header value is not valid. +pub fn parse_authorization_str(s: &str) -> Result, ParseError> { + let token = parse_authorization_str_ext(s)?.0; + Ok(token) } /// Parses an `Authorization` header according to the following scheme: /// -/// `PrivateToken token=... [, extensions=...]` +/// `PrivateToken token="..." [, extensions="..."]` /// /// # Errors /// Returns an error if the header value is not valid. @@ -234,8 +245,22 @@ pub fn parse_authorization_header_ext( value: &HeaderValue, ) -> Result<(Token, Option), ParseError> { let s = value.to_str().map_err(|_| ParseError::InvalidInput)?; - let mut tokens = parse_header_value(s)?; - Ok(tokens.pop().unwrap()) + + parse_authorization_str_ext(s) +} + +/// Parses an `Authorization` string according to the following scheme: +/// +/// `PrivateToken token="..." [, extensions="..."]` +/// +/// # Errors +/// Returns an error if the header value is not valid. +pub fn parse_authorization_str_ext( + s: &str, +) -> Result<(Token, Option), ParseError> { + let tokens = parse_header_value(s)?; + + tokens.into_iter().next().ok_or(ParseError::InvalidInput) } /// Parsing error for the `WWW-Authenticate` header values @@ -312,6 +337,7 @@ fn parse_private_tokens(input: &str) -> IResult<&str, Vec<(&str, Option<&str>)>> separated_list1(tag(","), parse_private_token).parse(input) } +#[allow(clippy::type_complexity)] fn parse_header_value( input: &str, ) -> Result, Option)>, ParseError> { diff --git a/src/public_tokens/mod.rs b/src/public_tokens/mod.rs index c8e265e..d621dd4 100644 --- a/src/public_tokens/mod.rs +++ b/src/public_tokens/mod.rs @@ -49,7 +49,7 @@ fn public_key_to_token_key_id(public_key: &PublicKey) -> Result { /// Privacy Pass issuance protocol #[default] diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index d981c09..d12c67a 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -46,7 +46,7 @@ impl TokenRequest { /// Issue a new token request using the given protocol. /// /// # Errors - /// Returns an error if the challenge is invalid or if blinding the token input fails.. + /// Returns an error if the challenge is invalid or if blinding the token input fails. pub fn new_with_protocol( rng: &mut R, public_key: PublicKey, @@ -95,15 +95,17 @@ impl TokenRequest { source: source.into(), })?; - m = Some(metadata.to_vec()); - dpk = Some(derived_pk.clone()); - - derived_pk + let result = derived_pk .blind(rng, token_input.serialize(), Some(metadata)) .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) .map_err(|source| IssueTokenRequestError::BlindingError { source: source.into(), - })? + })?; + + m = Some(metadata.to_vec()); + dpk = Some(derived_pk); + + result } }; diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index 1451ea8..e8835c3 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -333,6 +333,70 @@ impl IssuerServer { } } +/// Verifies a token without performing nonce validation +/// +/// Useful for debug CLIs and configurations where nonce validation is separate from validating the +/// tokens themselves. +/// +/// # Errors +/// Returns an error if the token is invalid. +pub fn verify_token( + public_key: &PublicKey, + token: &Token, + protocol: TokenProtocol<'_>, +) -> Result<(), RedeemTokenError> { + if token.token_type() != protocol.token_type() { + return Err(RedeemTokenError::TokenTypeMismatch { + expected: protocol.token_type(), + found: token.token_type(), + }); + } + + let authenticator_len = token.authenticator().len(); + if authenticator_len != KEYSIZE_IN_BYTES { + return Err(RedeemTokenError::InvalidAuthenticatorLength { + expected: KEYSIZE_IN_BYTES, + found: authenticator_len, + }); + } + + let token_input = TokenInput::new( + token.token_type(), + token.nonce(), + *token.challenge_digest(), + *token.token_key_id(), + ); + + let signature = Signature(token.authenticator().to_vec()); + let token_input_bytes = token_input.serialize(); + + let verified = match protocol { + TokenProtocol::Basic => public_key + .verify(&signature, None, &token_input_bytes) + .inspect_err(|e| warn!(error:% = e; "Verify failed")) + .is_ok(), + TokenProtocol::PublicMetadata { metadata } => { + let pbrsa_pk = PbrsaPublicKey::new(public_key.as_ref().clone()); + match pbrsa_pk.derive_public_key_for_metadata(metadata) { + Ok(derived) => derived + .verify(&signature, None, &token_input_bytes, Some(metadata)) + .inspect_err(|e| warn!(error:% = e; "Verify failed")) + .is_ok(), + Err(e) => { + warn!(error:% = e; "Key derivation failed"); + false + } + } + } + }; + if !verified { + return Err(RedeemTokenError::InvalidSignature { + token_type: token.token_type(), + }); + } + Ok(()) +} + /// Server-side implementation of Publicly Verifiable Token protocol for /// origins. #[derive(Default, Debug)] @@ -362,21 +426,14 @@ impl OriginServer { }); } - let authenticator_len = token.authenticator().len(); - if authenticator_len != KEYSIZE_IN_BYTES { + if token.authenticator().len() != KEYSIZE_IN_BYTES { return Err(RedeemTokenError::InvalidAuthenticatorLength { expected: KEYSIZE_IN_BYTES, - found: authenticator_len, + found: token.authenticator().len(), }); } let nonce = token.nonce(); - let token_input = TokenInput::new( - token.token_type(), - nonce, - *token.challenge_digest(), - *token.token_key_id(), - ); if !nonce_store.reserve(&nonce).await { return Err(RedeemTokenError::DoubleSpending); @@ -389,28 +446,9 @@ impl OriginServer { return Err(RedeemTokenError::KeyIdNotFound); } - let signature = Signature(token.authenticator().to_vec()); - let token_input_bytes = token_input.serialize(); - - let verified = public_keys.iter().any(|public_key| match protocol { - TokenProtocol::Basic => public_key - .verify(&signature, None, &token_input_bytes) - .inspect_err(|e| warn!(error:% = e; "Verify failed")) - .is_ok(), - TokenProtocol::PublicMetadata { metadata } => { - let pbrsa_pk = PbrsaPublicKey::new(public_key.as_ref().clone()); - match pbrsa_pk.derive_public_key_for_metadata(metadata) { - Ok(derived) => derived - .verify(&signature, None, &token_input_bytes, Some(metadata)) - .inspect_err(|e| warn!(error:% = e; "Verify failed")) - .is_ok(), - Err(e) => { - warn!(error:% = e; "Key derivation failed"); - false - } - } - } - }); + let verified = public_keys + .iter() + .any(|public_key| verify_token(public_key, &token, protocol).is_ok()); if !verified { return Err(RedeemTokenError::InvalidSignature { diff --git a/tests/public_tokens.rs b/tests/public_tokens.rs index 8bf26ca..5b092c6 100644 --- a/tests/public_tokens.rs +++ b/tests/public_tokens.rs @@ -245,12 +245,11 @@ async fn public_metadata_tokens() { // Client: Prepare a TokenRequest after having received a challenge let (token_request, token_state) = - TokenRequest::new_with_protocol(rng, public_key, &token_challenge, protocol.clone()) - .unwrap(); + TokenRequest::new_with_protocol(rng, public_key, &token_challenge, protocol).unwrap(); // Issuer server: Issue a TokenResponse let token_response = issuer_server - .issue_token_response_protocol(&issuer_key_store, token_request, protocol.clone()) + .issue_token_response_protocol(&issuer_key_store, token_request, protocol) .await .unwrap(); @@ -262,19 +261,14 @@ async fn public_metadata_tokens() { // Origin server: Redeem the token origin_server - .redeem_token_protocol( - &origin_key_store, - &nonce_store, - token.clone(), - protocol.clone(), - ) + .redeem_token_protocol(&origin_key_store, &nonce_store, token.clone(), protocol) .await .unwrap(); // Origin server: Test double spend protection assert_eq!( origin_server - .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol.clone()) + .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol) .await, Err(RedeemTokenError::DoubleSpending) ); From 66b64337dfcbdd7f502d10b80d9d4ad11a622164 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Wed, 17 Jun 2026 14:28:23 -0500 Subject: [PATCH 07/13] Add cross-compat tests --- src/auth/authorize.rs | 108 ++++++++++++++++- tests/kat_public_metadata.rs | 139 ++++++++++++++++++++++ tests/kat_vectors/public_metadata_ts.json | 14 +++ 3 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 tests/kat_public_metadata.rs create mode 100644 tests/kat_vectors/public_metadata_ts.json diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 34ea573..2a70aa0 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -417,7 +417,7 @@ mod tests { let token_key_id = [3u8; 32]; let authenticator = [4u8; 32]; let token = Token::::new( - TokenType::PrivateP384, + TokenType::PublicMetadata, nonce, challenge_digest, token_key_id, @@ -433,7 +433,7 @@ mod tests { let (token, maybe_extensions) = parse_authorization_header_ext::(&header_value).unwrap(); - assert_eq!(token.token_type(), TokenType::PrivateP384); + assert_eq!(token.token_type(), TokenType::PublicMetadata); assert_eq!(token.nonce(), nonce); assert_eq!(token.challenge_digest(), &challenge_digest); assert_eq!(token.token_key_id(), &token_key_id); @@ -441,6 +441,110 @@ mod tests { assert_eq!(maybe_extensions, Some(extensions)); } + struct Vector { + nonce: [u8; 32], + challenge_digest: [u8; 32], + token_key_id: [u8; 32], + } + + fn hex_to_bytes(s: &str) -> Vec { + let mut output = vec![]; + let chars: Vec = s.chars().collect(); + + for pair in chars.chunks(2) { + let string_pair: String = pair.iter().collect(); + output.push(u8::from_str_radix(&string_pair, 16).unwrap()); + } + + output + } + + fn hex32(s: &str) -> [u8; 32] { + hex_to_bytes(s).try_into().unwrap() + } + + #[test] + fn builder_parser_extensions_test_cross_compat() { + // test vectors taken from + // https://github.com/cloudflare/privacypass-ts/blob/main/test/test_data/auth_scheme_token_with_extensions_v1.json + // challenge digests were extracted from the token_authenticator_input field, located after + // 2-byte token input and 32-byte nonce + let vectors = [ + Vector { + nonce: hex32("e01978182c469e5e026d66558ee186568614f235e41ef7e2378e6f202688abab"), + challenge_digest: hex32( + "d95573d45e84e65d5ce4adaff401040b823a5586c30855580bff3ea0118f8192", + ), + token_key_id: hex32( + "ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708", + ), + }, + Vector { + nonce: hex32("e01978182c469e5e026d66558ee186568614f235e41ef7e2378e6f202688abab"), + challenge_digest: hex32( + "0021e3fccff3e175dabdef586fafdbb26fc0a869ee29d0229b592729fa6b1289", + ), + token_key_id: hex32( + "ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708", + ), + }, + Vector { + nonce: hex32("e01978182c469e5e026d66558ee186568614f235e41ef7e2378e6f202688abab"), + challenge_digest: hex32( + "e67c893c237722726064008792670a0368dbe8fcfd47c8233613bee3e1ee52ff", + ), + token_key_id: hex32( + "ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708", + ), + }, + Vector { + nonce: hex32("e01978182c469e5e026d66558ee186568614f235e41ef7e2378e6f202688abab"), + challenge_digest: hex32( + "69a780715896548c5eecaee2be452eac6bb001078a057993f665c653133b7ffc", + ), + token_key_id: hex32( + "ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708", + ), + }, + Vector { + nonce: hex32("e01978182c469e5e026d66558ee186568614f235e41ef7e2378e6f202688abab"), + challenge_digest: hex32( + "6d3d849f2508b23721cedafbbff8183c1d7f2ed2c586f3641b8131f5ed719ebf", + ), + token_key_id: hex32( + "ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708", + ), + }, + ]; + + // all vectors use extension(0x0000, uint8[01, 02, 03]) + let extension = Extension::new(ExtensionType(0), vec![1, 2, 3]); + let extensions = Extensions::new(vec![extension]); + + for vector in &vectors { + let token = Token::::new( + TokenType::PublicMetadata, + vector.nonce, + vector.challenge_digest, + vector.token_key_id, + *GenericArray::from_slice(&[0u8; 32]), + ); + + let (header_name, header_value) = + build_authorization_header_ext(&token, &extensions).unwrap(); + + assert_eq!(header_name, http::header::AUTHORIZATION); + + let (token, maybe_extensions) = + parse_authorization_header_ext::(&header_value).unwrap(); + assert_eq!(token.token_type(), TokenType::PublicMetadata); + assert_eq!(token.nonce(), vector.nonce); + assert_eq!(token.challenge_digest(), &vector.challenge_digest); + assert_eq!(token.token_key_id(), &vector.token_key_id); + assert_eq!(maybe_extensions, Some(extensions.clone())); + } + } + /// This is the same test as `builder_parser_extensions_test`, but we replace the `, ` /// separator with ` ` (single space) to make sure we can handle tokens generated by clients /// using an older version of the TOKEN-EXTENSION spec. diff --git a/tests/kat_public_metadata.rs b/tests/kat_public_metadata.rs new file mode 100644 index 0000000..44c99a5 --- /dev/null +++ b/tests/kat_public_metadata.rs @@ -0,0 +1,139 @@ +use blind_rsa_signatures::{Deterministic, KeyPair, PSS, PublicKey, SecretKey, Sha384}; +use serde::{Deserialize, Serialize}; +use tls_codec::Serialize as TlsSerializeTrait; + +use privacypass::{ + TokenType, + auth::authenticate::TokenChallenge, + public_tokens::{ + TokenProtocol, TokenRequest, det_rng::DeterministicRng, + public_key_to_truncated_token_key_id, server::*, + }, + test_utils::{ + nonce_store::MemoryNonceStore, + public_memory_store::{IssuerMemoryKeyStore, OriginMemoryKeyStore}, + }, +}; + +#[derive(Serialize, Deserialize)] +struct PublicMetadataTokenTestVector { + #[serde(with = "hex", rename = "skS")] + sk_s: Vec, + #[serde(with = "hex", rename = "pkS")] + pk_s: Vec, + #[serde(with = "hex")] + token_challenge: Vec, + #[serde(with = "hex")] + extensions: Vec, + #[serde(with = "hex")] + nonce: Vec, + #[serde(with = "hex")] + blind: Vec, + #[serde(with = "hex")] + salt: Vec, + #[serde(with = "hex")] + token_request: Vec, + #[serde(with = "hex")] + token_response: Vec, + #[serde(with = "hex")] + token: Vec, +} + +#[tokio::test] +async fn read_kat_public_metadata_token() { + // Test vectors from + // https://github.com/cloudflare/privacypass-ts/blob/main/test/test_data/pub_verif_with_metadata_v3.json + let list: Vec = + serde_json::from_str(include_str!("kat_vectors/public_metadata_ts.json").trim()).unwrap(); + + for vector in list { + evaluate_vector(vector).await; + } +} + +async fn evaluate_vector(vector: PublicMetadataTokenTestVector) { + let issuer_key_store = IssuerMemoryKeyStore::default(); + let origin_key_store = OriginMemoryKeyStore::default(); + let nonce_store = MemoryNonceStore::default(); + + let issuer_server = IssuerServer::new(); + let origin_server = OriginServer::new(); + + let sec_key = + SecretKey::::from_pem(&String::from_utf8_lossy(&vector.sk_s)) + .unwrap(); + let pub_key = PublicKey::::from_spki(&vector.pk_s).unwrap(); + + let keypair = KeyPair { + sk: sec_key, + pk: pub_key.clone(), + }; + + issuer_server + .set_keypair(&issuer_key_store, keypair) + .await + .unwrap(); + + origin_key_store + .insert( + public_key_to_truncated_token_key_id(&pub_key).unwrap(), + pub_key.clone(), + ) + .await; + + // The extensions field is the raw metadata bytes passed to TokenProtocol::PublicMetadata + let metadata = vector.extensions.clone(); + let protocol = TokenProtocol::PublicMetadata { + metadata: &metadata, + }; + + let mut blind = vector.blind.clone(); + blind.reverse(); + + let det_rng = &mut DeterministicRng::new(vector.nonce.clone(), vector.salt.clone(), blind); + + let token_challenge = TokenChallenge::deserialize(vector.token_challenge.as_slice()).unwrap(); + let challenge_digest: [u8; 32] = token_challenge.digest().unwrap(); + + // KAT: Check token challenge type + assert_eq!(token_challenge.token_type(), TokenType::PublicMetadata); + + let (token_request, token_state) = + TokenRequest::new_with_protocol(det_rng, pub_key, &token_challenge, protocol).unwrap(); + + // TODO: this is due to wrong api + // will fix in next commit + let mut token_request_bytes = token_request.tls_serialize_detached().unwrap(); + token_request_bytes.extend_from_slice(&vector.extensions); + + // KAT: Check token request + // assert_eq!( + // token_request.tls_serialize_detached().unwrap(), + // vector.token_request + // ); + assert_eq!(token_request_bytes, vector.token_request); + + let token_response = issuer_server + .issue_token_response_protocol(&issuer_key_store, token_request, protocol) + .await + .unwrap(); + + // KAT: Check token response + assert_eq!( + token_response.tls_serialize_detached().unwrap(), + vector.token_response + ); + + let token = token_response.issue_token(&token_state).unwrap(); + + assert_eq!(token.challenge_digest(), &challenge_digest); + + // KAT: Check token + assert_eq!(token.tls_serialize_detached().unwrap(), vector.token); + + // Origin: Redeem the token + origin_server + .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol) + .await + .unwrap(); +} diff --git a/tests/kat_vectors/public_metadata_ts.json b/tests/kat_vectors/public_metadata_ts.json new file mode 100644 index 0000000..594934a --- /dev/null +++ b/tests/kat_vectors/public_metadata_ts.json @@ -0,0 +1,14 @@ +[ + { + "skS": "2d2d2d2d2d424547494e2050524956415445204b45592d2d2d2d2d0a4d494945765149424144414e42676b71686b6947397730424151454641415343424b63776767536a41674541416f4942415144576b77676739782f6c463738790a5764464e51434362417158413039595a6b6363783358326a6e34317067685653346a474e624a72596c2b59446948704862714d574c4249463270724a62774c740a387833775362315638554954544266554f436f4f654f4a314e4638575837364f5363334b625058484a73575a33546e676e6e5867387a436a4d53486e4f58626b0a2b7375707a364142776f74386c76675454356d42323264517444704263513952326b4a412f674d5162424b737365653755396465787956746f2f3364427869340a6e445a5545507a6d4738664a6d784666744d504447416766702b4732576a6433546f35517957364d34724c4d617a733265594932616976356b6b784c7239732f0a396563694a59713342636474512b5878385347356849464f6d4f6f724b34636c7a5a764a4263433850585843714e74777078557945384f6134334779746477640a7238735a3176727041674d4241414543676745415469453161594e794b7147743762434570494e414842456e74344771794a3672454434632f464968564a53590a48526a6467436857625a314a6c476e43564859316a6549344963654b6175517742654a724f5534775562584b494771706c6f316f7975493757762f5a7937544c0a46745a4b7833564c504e7569516263713174333841412b733277384e3044713954767a2b34584d4853507a45653359684743373472793772664a6854536659730a3657717a63394a6f6d3636756f4f4b4f703952664c574255555a494d704f6f664441697738665a784871704c664d706d315970726b572b5a685567506b4b79700a6368426f577365784c53374434776f636535653257686a5469704d596b6c69714e47767976466373312b63316c675843416947346b4a315a6e746e546757544a0a784b767a6c766958755a6b38486f426556303177526b6d59573241506f4d375935554a77636463456e514b426751446332517278766b59324d734456366c56530a5671494742613839746d644858686b4f4f7645714e4b4d795445616a43554269785a2b30736b6e67376d7237714c376854674a323053624a6e306545736a414a0a7632466f2f324b4b7755687557756a69504f5454596f6964354e396a454a79396b4f2b543231726d51334b2f346356666779646d38683655366a4d69367947430a3851714a46555a5461366b48725853343179527076714f5738774b4267514434756c794a7651615056794e4b5050564b48496e56744d30426c504a6a504b66470a4335476e6c615676714d686f624134337363524a693455654e434451692b6f703978305a585076545a78787433456e50544232317448676a48716e5a4533662f0a71592f70566f58386f67756b596a495373764c65394e70624b4237514541746c4832327a49524c6b415832444841326d6148614b2b6e4642314675384a3538650a4434633131304f5673774b426751434a6a4a7a6d5a2b33752f376c66416d355674434f42774c5365715361324b6f4e4d6536574d39652f572b653763794878540a69476170386f6c56454f6d4e43464b716e52326e7879687a56304d43414d4575735159496b56646375486c57556b544e38384e4a6251744e33436a4e324b446d0a364968752f4b596d324564312f7a5968414e7a666e316b777770564b3445756b39462f74645653787a46496775415945776571702b64534678514b42674362490a6b474e64325363437446734146464e6844644548357975472b53436d4e5a62342b4e5a447a4538543532503437574b62306e715253636d433456634461686a760a456c6542477a64456a6264487353366b744d452f696267594d4f334c546c764d4b6364396d566a69503652374e306d5a493676475445494969483179387750710a6d61566a30396b2f726a324359314a2b5758576b52594b35525443616b51495438385a656359686e416f4741563774614e4b377a592b66496345557765616d6f0a524432316434413030692f72755a7331566f307846304d6a46692f715964653562356739466352687a4a686b5051573555683671323073342f6a4155697459360a4e574b6e4476506864357139736f64724e6d48415239582f73396f6347796e784e74554b467a4845757241765a43636e662f7563724f4c6869302f53707848570a56554f564545656765726d534f58796b4a6e32775975633d0a2d2d2d2d2d454e442050524956415445204b45592d2d2d2d2d", + "pkS": "30820152303d06092a864886f70d01010a3030a00d300b0609608648016503040202a11a301806092a864886f70d010108300b0609608648016503040202a2030201300382010f003082010a0282010100d6930820f71fe517bf3259d14d40209b02a5c0d3d61991c731dd7da39f8d69821552e2318d6c9ad897e603887a476ea3162c1205da9ac96f02edf31df049bd55f142134c17d4382a0e78e275345f165fbe8e49cdca6cf5c726c599dd39e09e75e0f330a33121e73976e4facba9cfa001c28b7c96f8134f9981db6750b43a41710f51da4240fe03106c12acb1e7bb53d75ec7256da3fddd0718b89c365410fce61bc7c99b115fb4c3c318081fa7e1b65a37774e8e50c96e8ce2b2cc6b3b367982366a2bf9924c4bafdb3ff5e722258ab705c76d43e5f1f121b984814e98ea2b2b8725cd9bc905c0bc3d75c2a8db70a7153213c39ae371b2b5dc1dafcb19d6fae90203010001", + "token_challenge": "da7a000e6973737565722e6578616d706c65208e7acc900e393381e8810b7c9e4a68b5163f1f880ab6688a6ffe780923609e88000e6f726967696e2e6578616d706c65", + "extensions": "000700010003010203", + "nonce": "aa72019d1f951df197021ce63876fe8b0a02dc1c31a12b0a2dd1508d07827f05", + "blind": "425421de54c7381864ce36473abfb988c454fe6c27de863de702a6a2adca153fa2de47bd8fcd62734caa8ce1f920b77d980ab58c32d16dde54873f28ca968e8c125b8363514be68972f553655bcc7f80a284cc327e47e804a47333c5b3cdf773312cc7ad9fda748aed0baa7e19c5a2d1dafda718f086d7fc0a4bc02d488e0f20812daee335af7177b7a8369bd617066aed7a58f659f295c36b418827f679725b81ca14ea16fb82df21ad76da1ac38dcf24bf6252f8510e2308608ac9197f6cb54fdcb19db17837302a2b87d659c5605f35f3709a130f0c3d50e172f0cae36cbc9467f9914895a215a9e32443bcafff795273ccf8965a7eaa8c0b2184763e3e5c", + "salt": "648ea74482fbab69876817ee3c2055a6921a458648c802c09a23f8825b259724e41c960ef29febe16a04e120c8b1cc1a", + "token_request": "da7a599e2d94eac16333e515b65cd621d4556fd4c74beb868955207423b72abe411b0094f5ac83ae37c9bdac6014ddc95fffe5fda475cb4705e0c4304eed23c3c7f1c6793a163f961c69e17670ad52cf729e274f160405b3f5477761796e53fb2b3281ee4c7cbbb0b918f187dde8bad6481ba31b9f6b010a783caa5eb535c78905c3c687d625ee2aceb0eee4d95b4e5702def4d20db8c30ac1376b8d0fedae7a81c5605c59944857baa3610f480f068868dd4027eee25824542a4272363cd497ddb2fce9ed51dbbb08765376a18434e4f0984cb4414dfb9e4fc0e7087e4cafeaaf46db72f31e6516e8acd2974a2953050e21f8de4d1f7131a222690be43f66d3ef4f8a000700010003010203", + "token_response": "8ee14c73cce7100a745673cac4d52f0123bd568b3d92c74bca96da61fda85498dc5c2e48dc7563d034e48784dc3e6bb58847d6e3f02d2fa21935fae3c99d592437954b38ee6f4c1c19c6d22cb89a0165764207d2c2484c825b0ad2be06e9acaacfda839bc45e6d7b21bdd8d52fc80f95cc4b652fc90e385f0adb70b0137dd32a8d77145ab1bcf0c3d6fa90765633a3b02e9838a5f4a293c6d688c4ecb4134eea2f7838b37a283b706a7f21170897f2f75cd4bd7b816f4ed41dbbc13319de5f7673bc941a42c82be1a14422c5c187eb6a50bba0943031e4378dcc8a08e9b6c2ded9e754b38e834433f0b49ef13995e08f636aee135b5e370226bff4a0adddba10", + "token": "da7aaa72019d1f951df197021ce63876fe8b0a02dc1c31a12b0a2dd1508d07827f052d10a392ba075b3098d3c6b651277229f278e5a1bbcf6366caeeb208e8f8dff710175f0d285597841ffdcef8ff5ed6c2e5c95953bc3c645e006c757f3d7aa159ad59e77d7227935ac0ec3bbbfcca3699d79c59ffdde259e3ba1df5a2c061dfb6f96d8cc2185b781dce94912f2925e01283ae93b4e321d2f7edd1a53735ed8ae72ee248531e484c2c7f4bf32c5943cb4a043f032aca4ad78416f300bcacfff44a3c578f2d2d910ad96fc68957a84ef46a34f82ad3b79a22c21d81c3fc4242c69c26afc2b41fe13db09458a0af2cd1ed1ee244a0165ed10ba15e2e901fc32f8821fd366fabefc414003aae9590ba7814c626529d42e613f7df5e02755b37991fb80efab09d116f02cab892de4249c71458846198f582339b85e054dbb75de3a47c34c60881918b1590b2a15f33b08baed7b51322fc6a960a3c668a944306a968e8" + } +] From 59747822b1f171285c530a9f395605d4d68dd5d7 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Wed, 17 Jun 2026 16:32:35 -0500 Subject: [PATCH 08/13] Remove TokenProtocol and switch to using Extensions types in functions --- src/common/errors.rs | 21 ++++ src/public_tokens/mod.rs | 30 +----- src/public_tokens/request.rs | 193 ++++++++++++++++++++++++++--------- src/public_tokens/server.rs | 162 ++++++++++++++++++++--------- tests/kat_public_metadata.rs | 33 +++--- tests/public_tokens.rs | 15 +-- 6 files changed, 300 insertions(+), 154 deletions(-) diff --git a/src/common/errors.rs b/src/common/errors.rs index 64e4a33..3335055 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -67,6 +67,13 @@ pub enum IssueTokenRequestError { #[source] source: TokenChallengeSerializationError, }, + #[error("Extension serialization error")] + /// Error when serializing Extensions fails + ExtensionSerializationError { + /// Underlying TLS error that triggered the failure + #[source] + source: TlsCodecError, + }, } /// Source errors for blinding failures. @@ -123,6 +130,13 @@ pub enum IssueTokenResponseError { /// Actual batch size in the request. size: usize, }, + #[error("Extension serialization error")] + /// Error when serializing Extensions fails + ExtensionSerializationError { + /// Underlying TLS error that triggered the failure + #[source] + source: TlsCodecError, + }, } /// Errors that can occur when issuing tokens. @@ -229,4 +243,11 @@ pub enum RedeemTokenError { /// Token type that was being redeemed. token_type: TokenType, }, + #[error("Extension serialization error")] + /// Error when serializing Extensions fails + ExtensionSerializationError { + /// Underlying TLS error that triggered the failure + #[source] + source: TlsCodecError, + }, } diff --git a/src/public_tokens/mod.rs b/src/public_tokens/mod.rs index d621dd4..75c19b7 100644 --- a/src/public_tokens/mod.rs +++ b/src/public_tokens/mod.rs @@ -6,9 +6,7 @@ use typenum::U256; use blind_rsa_signatures::Error as BlindRsaError; -use crate::{ - TokenKeyId, TokenType, TruncatedTokenKeyId, auth::authorize::Token, truncate_token_key_id, -}; +use crate::{TokenKeyId, TruncatedTokenKeyId, auth::authorize::Token, truncate_token_key_id}; pub mod request; pub mod response; @@ -47,29 +45,3 @@ fn public_key_to_token_key_id(public_key: &PublicKey) -> Result { - /// Privacy Pass issuance protocol - #[default] - Basic, - /// Privacy Pass issuance with Public Metadata - /// - /// Specified by `draft-ietf-privacypass-public-metadata-issuance`. - PublicMetadata { - /// A reference to the public metadata, cryptographically bound to - /// the generated token. - metadata: &'a [u8], - }, -} - -impl<'a> TokenProtocol<'a> { - /// Returns the token type associated with this protocol - pub fn token_type(&self) -> TokenType { - match self { - TokenProtocol::Basic => TokenType::Public, - TokenProtocol::PublicMetadata { .. } => TokenType::PublicMetadata, - } - } -} diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index d12c67a..e9dc5af 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -1,16 +1,19 @@ //! Request implementation of the Publicly Verifiable Token protocol. +use std::io::{Read, Write}; + use blind_rsa_signatures::reexports::rand::CryptoRng; use blind_rsa_signatures::{BlindingResult, pbrsa::PartiallyBlindPublicKey}; use log::warn; +use tls_codec::{Deserialize, Serialize, Size}; use super::PublicKey; -use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; +use crate::common::extensions::Extensions; use crate::public_tokens::server::PbrsaPublicKey; use crate::{ ChallengeDigest, Nonce, TokenInput, TokenType, auth::authenticate::TokenChallenge, - common::errors::IssueTokenRequestError, public_tokens::TokenProtocol, truncate_token_key_id, + common::errors::IssueTokenRequestError, truncate_token_key_id, }; use super::{NK, public_key_to_token_key_id}; @@ -35,23 +38,71 @@ pub struct TokenState { /// uint8_t blinded_msg[Nk]; /// } TokenRequest; /// ``` -#[derive(Debug, Clone, PartialEq, TlsDeserialize, TlsSerialize, TlsSize)] +#[derive(Debug, Clone, PartialEq)] pub struct TokenRequest { pub(crate) token_type: TokenType, pub(crate) truncated_token_key_id: u8, pub(crate) blinded_msg: [u8; NK], + pub(crate) extensions: Option, +} + +// We have to implement these manually in order to avoid having the Option serialization byte +impl Size for TokenRequest { + fn tls_serialized_len(&self) -> usize { + self.token_type.tls_serialized_len() + + self.truncated_token_key_id.tls_serialized_len() + + self.blinded_msg.tls_serialized_len() + + match &self.extensions { + Some(e) => e.tls_serialized_len(), + None => 0, + } + } +} + +impl Serialize for TokenRequest { + fn tls_serialize(&self, writer: &mut W) -> Result { + let mut written = 0; + written += self.token_type.tls_serialize(writer)?; + written += self.truncated_token_key_id.tls_serialize(writer)?; + written += self.blinded_msg.tls_serialize(writer)?; + if let Some(extensions) = &self.extensions { + written += extensions.tls_serialize(writer)?; + } + + Ok(written) + } +} + +impl Deserialize for TokenRequest { + fn tls_deserialize(bytes: &mut R) -> Result { + let token_type = TokenType::tls_deserialize(bytes)?; + let truncated_token_key_id = u8::tls_deserialize(bytes)?; + let blinded_msg = <[u8; NK]>::tls_deserialize(bytes)?; + + let extensions = if token_type == TokenType::PublicMetadata { + Some(Extensions::tls_deserialize(bytes)?) + } else { + None + }; + + Ok(Self { + token_type, + truncated_token_key_id, + blinded_msg, + extensions, + }) + } } impl TokenRequest { - /// Issue a new token request using the given protocol. + /// Issue a new token request. /// /// # Errors - /// Returns an error if the challenge is invalid or if blinding the token input fails. - pub fn new_with_protocol( + /// Returns an error if the challenge is invalid. + pub fn new( rng: &mut R, public_key: PublicKey, challenge: &TokenChallenge, - protocol: TokenProtocol, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { let mut nonce: Nonce = [0u8; 32]; rng.fill_bytes(&mut nonce); @@ -67,56 +118,29 @@ impl TokenRequest { } })?; - let token_input = - TokenInput::new(protocol.token_type(), nonce, challenge_digest, token_key_id); + let token_input = TokenInput::new(TokenType::Public, nonce, challenge_digest, token_key_id); // nonce = random(32) // challenge_digest = SHA256(challenge) // token_input = concat(0x0002, nonce, challenge_digest, token_key_id) // blinded_msg, blind_inv = rsabssa_blind(pkI, token_input) - let mut m: Option<_> = None; - let mut dpk: Option<_> = None; - - let blinding_result = match protocol { - TokenProtocol::Basic => public_key - .blind(rng, token_input.serialize()) - .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?, - TokenProtocol::PublicMetadata { metadata } => { - let pbrsa_pk: PbrsaPublicKey = - PartiallyBlindPublicKey::new(public_key.as_ref().clone()); - let derived_pk = pbrsa_pk - .derive_public_key_for_metadata(metadata) - .inspect_err(|e| warn!(error:% = e; "Failed to derive metadata public key")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; - - let result = derived_pk - .blind(rng, token_input.serialize(), Some(metadata)) - .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; - - m = Some(metadata.to_vec()); - dpk = Some(derived_pk); - - result - } - }; + let blinding_result = public_key + .blind(rng, token_input.serialize()) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; debug_assert!(blinding_result.blind_message.len() == NK); let mut blinded_msg = [0u8; NK]; blinded_msg.copy_from_slice(blinding_result.blind_message.as_slice()); let token_request = TokenRequest { - token_type: protocol.token_type(), + token_type: TokenType::Public, truncated_token_key_id: truncate_token_key_id(&token_key_id), blinded_msg, + extensions: None, }; let token_state = TokenState { @@ -124,21 +148,92 @@ impl TokenRequest { token_input, challenge_digest, public_key, - metadata: m, - derived_pk: dpk, + metadata: None, + derived_pk: None, }; + Ok((token_request, token_state)) } - /// Issue a new token request. + /// Issue a new token request using the given extensions /// /// # Errors - /// Returns an error if the challenge is invalid. - pub fn new( + /// Returns an error if the challenge is invalid or if blinding the token input fails. + pub fn new_with_extensions( rng: &mut R, public_key: PublicKey, challenge: &TokenChallenge, + extensions: Extensions, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { - Self::new_with_protocol(rng, public_key, challenge, TokenProtocol::Basic) + let mut nonce: Nonce = [0u8; 32]; + rng.fill_bytes(&mut nonce); + + let challenge_digest = challenge + .digest() + .inspect_err(|e| warn!(error:% = e; "Failed to create challenge digest")) + .map_err(|source| IssueTokenRequestError::InvalidTokenChallenge { source })?; + + let token_key_id = public_key_to_token_key_id(&public_key).map_err(|source| { + IssueTokenRequestError::BlindingError { + source: source.into(), + } + })?; + + let token_input = TokenInput::new( + TokenType::PublicMetadata, + nonce, + challenge_digest, + token_key_id, + ); + + // nonce = random(32) + // challenge_digest = SHA256(challenge) + // token_input = concat(0x0002, nonce, challenge_digest, token_key_id) + // blinded_msg, blind_inv = rsabssa_blind(pkI, token_input) + + let metadata = extensions + .tls_serialize_detached() + .inspect_err(|e| warn!(error:% = e; "Failed to serialize extensions")) + .map_err(|source| IssueTokenRequestError::ExtensionSerializationError { source })?; + + let pbrsa_pk: PbrsaPublicKey = PartiallyBlindPublicKey::new(public_key.as_ref().clone()); + let derived_pk = pbrsa_pk + .derive_public_key_for_metadata(&metadata) + .inspect_err(|e| warn!(error:% = e; "Failed to derive metadata public key")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + let blinding_result = derived_pk + .blind(rng, token_input.serialize(), Some(&metadata)) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + let m = Some(metadata.to_vec()); + let dpk = Some(derived_pk); + + debug_assert!(blinding_result.blind_message.len() == NK); + let mut blinded_msg = [0u8; NK]; + blinded_msg.copy_from_slice(blinding_result.blind_message.as_slice()); + + let token_request = TokenRequest { + token_type: TokenType::PublicMetadata, + truncated_token_key_id: truncate_token_key_id(&token_key_id), + blinded_msg, + extensions: Some(extensions), + }; + + let token_state = TokenState { + blinding_result, + token_input, + challenge_digest, + public_key, + metadata: m, + derived_pk: dpk, + }; + + Ok((token_request, token_state)) } } diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index e8835c3..dcffc41 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -14,6 +14,7 @@ use blind_rsa_signatures::{ }; use generic_array::ArrayLength; use log::{debug, warn}; +use tls_codec::Serialize; type KeyPair = GenericKeyPair; type PublicKey = GenericPublicKey; @@ -21,13 +22,13 @@ type PublicKey = GenericPublicKey; pub(crate) type PbrsaKeyPair = PartiallyBlindKeyPair; pub(crate) type PbrsaPublicKey = PartiallyBlindPublicKey; -use crate::TokenKeyId; -use crate::public_tokens::TokenProtocol; +use crate::common::extensions::Extensions; use crate::{ COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TruncatedTokenKeyId, auth::authorize::Token, common::errors::{CreateKeypairError, IssueTokenResponseError, RedeemTokenError}, }; +use crate::{TokenKeyId, TokenType}; use super::{NK, TokenRequest, TokenResponse, public_key_to_token_key_id, truncate_token_key_id}; @@ -244,20 +245,26 @@ impl IssuerServer { Err(CreateKeypairError::CollisionExhausted) } - /// Issues a new token response using the given protocol. + /// Issues a new token response. /// /// # Errors - /// Returns an error if the token request is invalid, or if protocol == PublicMetadata and the + /// Returns an error if the token request is invalid, or if token_type == PublicMetadata and the /// key is not PBRSA compatible. - pub async fn issue_token_response_protocol( + pub async fn issue_token_response( &self, key_store: &IKS, token_request: TokenRequest, - protocol: TokenProtocol<'_>, ) -> Result { - if token_request.token_type != protocol.token_type() { + if token_request.extensions.is_some() { + if token_request.token_type != TokenType::PublicMetadata { + return Err(IssueTokenResponseError::InvalidTokenType { + expected: TokenType::PublicMetadata, + found: token_request.token_type, + }); + } + } else if token_request.token_type != TokenType::Public { return Err(IssueTokenResponseError::InvalidTokenType { - expected: protocol.token_type(), + expected: TokenType::Public, found: token_request.token_type, }); } @@ -268,13 +275,16 @@ impl IssuerServer { .ok_or(IssueTokenResponseError::KeyIdNotFound)?; // blind_sig = rsabssa_blind_sign(skI, TokenRequest.blinded_msg) - let blind_signature = match protocol { - TokenProtocol::Basic => key_pair - .sk - .blind_sign(token_request.blinded_msg) - .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) - .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?, - TokenProtocol::PublicMetadata { metadata } => { + + let blind_signature = match token_request.extensions { + Some(extensions) => { + let metadata = extensions + .tls_serialize_detached() + .inspect_err(|e| warn!(error:% = e; "Failed to serialize extensions")) + .map_err( + |source| IssueTokenResponseError::ExtensionSerializationError { source }, + )?; + let pbrsa = self .get_pbrsa_pair(&key_pair) .inspect_err( @@ -283,7 +293,7 @@ impl IssuerServer { .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?; let derived_sk = pbrsa - .derive_secret_key_for_metadata(metadata) + .derive_secret_key_for_metadata(&metadata) .inspect_err(|e| warn!(error:% = e; "Failed to derive augmented secret key")) .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?; @@ -292,6 +302,11 @@ impl IssuerServer { .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })? } + None => key_pair + .sk + .blind_sign(token_request.blinded_msg) + .inspect_err(|e| warn!(error:% = e; "Failed to blind_sign token")) + .map_err(|source| IssueTokenResponseError::BlindSignatureFailed { source })?, }; debug_assert!(blind_signature.len() == NK); @@ -301,19 +316,6 @@ impl IssuerServer { Ok(TokenResponse { blind_sig }) } - /// Issues a new token response. - /// - /// # Errors - /// Returns an error if the token request is invalid. - pub async fn issue_token_response( - &self, - key_store: &IKS, - token_request: TokenRequest, - ) -> Result { - self.issue_token_response_protocol(key_store, token_request, TokenProtocol::Basic) - .await - } - /// Sets the given keypair. /// /// # Errors @@ -343,11 +345,18 @@ impl IssuerServer { pub fn verify_token( public_key: &PublicKey, token: &Token, - protocol: TokenProtocol<'_>, + extensions: Option<&Extensions>, ) -> Result<(), RedeemTokenError> { - if token.token_type() != protocol.token_type() { + if extensions.is_some() { + if token.token_type() != TokenType::PublicMetadata { + return Err(RedeemTokenError::TokenTypeMismatch { + expected: TokenType::PublicMetadata, + found: token.token_type(), + }); + } + } else if token.token_type() != TokenType::Public { return Err(RedeemTokenError::TokenTypeMismatch { - expected: protocol.token_type(), + expected: TokenType::Public, found: token.token_type(), }); } @@ -370,16 +379,17 @@ pub fn verify_token( let signature = Signature(token.authenticator().to_vec()); let token_input_bytes = token_input.serialize(); - let verified = match protocol { - TokenProtocol::Basic => public_key - .verify(&signature, None, &token_input_bytes) - .inspect_err(|e| warn!(error:% = e; "Verify failed")) - .is_ok(), - TokenProtocol::PublicMetadata { metadata } => { + let verified = match extensions { + Some(extensions) => { + let metadata = extensions + .tls_serialize_detached() + .inspect_err(|e| warn!(error:% = e; "Extension serialization failed")) + .map_err(|source| RedeemTokenError::ExtensionSerializationError { source })?; + let pbrsa_pk = PbrsaPublicKey::new(public_key.as_ref().clone()); - match pbrsa_pk.derive_public_key_for_metadata(metadata) { + match pbrsa_pk.derive_public_key_for_metadata(&metadata) { Ok(derived) => derived - .verify(&signature, None, &token_input_bytes, Some(metadata)) + .verify(&signature, None, &token_input_bytes, Some(&metadata)) .inspect_err(|e| warn!(error:% = e; "Verify failed")) .is_ok(), Err(e) => { @@ -388,6 +398,10 @@ pub fn verify_token( } } } + None => public_key + .verify(&signature, None, &token_input_bytes) + .inspect_err(|e| warn!(error:% = e; "Verify failed")) + .is_ok(), }; if !verified { return Err(RedeemTokenError::InvalidSignature { @@ -408,20 +422,24 @@ impl OriginServer { Self {} } - /// Redeems a token using the given protocol. + /// Redeems a token using the given extensions. /// /// # Errors /// Returns an error if the token is invalid or deriving the key for the metadata fails. - pub async fn redeem_token_protocol( + pub async fn redeem_token_with_extensions< + OKS: OriginKeyStore, + NS: NonceStore, + Nk: ArrayLength, + >( &self, key_store: &OKS, nonce_store: &NS, token: Token, - protocol: TokenProtocol<'_>, + extensions: &Extensions, ) -> Result<(), RedeemTokenError> { - if token.token_type() != protocol.token_type() { + if token.token_type() != TokenType::PublicMetadata { return Err(RedeemTokenError::TokenTypeMismatch { - expected: protocol.token_type(), + expected: TokenType::PublicMetadata, found: token.token_type(), }); } @@ -448,7 +466,7 @@ impl OriginServer { let verified = public_keys .iter() - .any(|public_key| verify_token(public_key, &token, protocol).is_ok()); + .any(|public_key| verify_token(public_key, &token, Some(extensions)).is_ok()); if !verified { return Err(RedeemTokenError::InvalidSignature { @@ -481,7 +499,55 @@ impl OriginServer { nonce_store: &NS, token: Token, ) -> Result<(), RedeemTokenError> { - self.redeem_token_protocol(key_store, nonce_store, token, TokenProtocol::Basic) - .await + if token.token_type() != TokenType::Public { + return Err(RedeemTokenError::TokenTypeMismatch { + expected: TokenType::Public, + found: token.token_type(), + }); + } + + if token.authenticator().len() != KEYSIZE_IN_BYTES { + return Err(RedeemTokenError::InvalidAuthenticatorLength { + expected: KEYSIZE_IN_BYTES, + found: token.authenticator().len(), + }); + } + + let nonce = token.nonce(); + + if !nonce_store.reserve(&nonce).await { + return Err(RedeemTokenError::DoubleSpending); + } + + let crypto_result = async { + let truncated_token_key_id = truncate_token_key_id(token.token_key_id()); + let public_keys = key_store.get(&truncated_token_key_id).await; + if public_keys.is_empty() { + return Err(RedeemTokenError::KeyIdNotFound); + } + + let verified = public_keys + .iter() + .any(|public_key| verify_token(public_key, &token, None).is_ok()); + + if !verified { + return Err(RedeemTokenError::InvalidSignature { + token_type: token.token_type(), + }); + } + Ok(()) + } + .await; + + match crypto_result { + Ok(()) => { + nonce_store.commit(&nonce).await; + Ok(()) + } + Err(e) => { + nonce_store.release(&nonce).await; + Err(e) + } + } } } diff --git a/tests/kat_public_metadata.rs b/tests/kat_public_metadata.rs index 44c99a5..b56c718 100644 --- a/tests/kat_public_metadata.rs +++ b/tests/kat_public_metadata.rs @@ -1,13 +1,13 @@ use blind_rsa_signatures::{Deterministic, KeyPair, PSS, PublicKey, SecretKey, Sha384}; use serde::{Deserialize, Serialize}; -use tls_codec::Serialize as TlsSerializeTrait; +use tls_codec::{Deserialize as _, Serialize as _}; use privacypass::{ TokenType, auth::authenticate::TokenChallenge, + common::extensions::Extensions, public_tokens::{ - TokenProtocol, TokenRequest, det_rng::DeterministicRng, - public_key_to_truncated_token_key_id, server::*, + TokenRequest, det_rng::DeterministicRng, public_key_to_truncated_token_key_id, server::*, }, test_utils::{ nonce_store::MemoryNonceStore, @@ -81,11 +81,7 @@ async fn evaluate_vector(vector: PublicMetadataTokenTestVector) { ) .await; - // The extensions field is the raw metadata bytes passed to TokenProtocol::PublicMetadata - let metadata = vector.extensions.clone(); - let protocol = TokenProtocol::PublicMetadata { - metadata: &metadata, - }; + let extensions: Extensions = Extensions::tls_deserialize_exact(vector.extensions).unwrap(); let mut blind = vector.blind.clone(); blind.reverse(); @@ -99,22 +95,17 @@ async fn evaluate_vector(vector: PublicMetadataTokenTestVector) { assert_eq!(token_challenge.token_type(), TokenType::PublicMetadata); let (token_request, token_state) = - TokenRequest::new_with_protocol(det_rng, pub_key, &token_challenge, protocol).unwrap(); - - // TODO: this is due to wrong api - // will fix in next commit - let mut token_request_bytes = token_request.tls_serialize_detached().unwrap(); - token_request_bytes.extend_from_slice(&vector.extensions); + TokenRequest::new_with_extensions(det_rng, pub_key, &token_challenge, extensions.clone()) + .unwrap(); // KAT: Check token request - // assert_eq!( - // token_request.tls_serialize_detached().unwrap(), - // vector.token_request - // ); - assert_eq!(token_request_bytes, vector.token_request); + assert_eq!( + token_request.tls_serialize_detached().unwrap(), + vector.token_request + ); let token_response = issuer_server - .issue_token_response_protocol(&issuer_key_store, token_request, protocol) + .issue_token_response(&issuer_key_store, token_request) .await .unwrap(); @@ -133,7 +124,7 @@ async fn evaluate_vector(vector: PublicMetadataTokenTestVector) { // Origin: Redeem the token origin_server - .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol) + .redeem_token_with_extensions(&origin_key_store, &nonce_store, token, &extensions) .await .unwrap(); } diff --git a/tests/public_tokens.rs b/tests/public_tokens.rs index 5b092c6..d0be832 100644 --- a/tests/public_tokens.rs +++ b/tests/public_tokens.rs @@ -2,7 +2,7 @@ use blind_rsa_signatures::reexports::crypto_bigint::BoxedUint; use blind_rsa_signatures::reexports::rand::rng; use blind_rsa_signatures::reexports::rsa::RsaPrivateKey; use blind_rsa_signatures::{Deterministic, KeyPair, PSS, PublicKey, SecretKey, Sha384}; -use privacypass::public_tokens::TokenProtocol; +use privacypass::common::extensions::{Extension, ExtensionType, Extensions}; use privacypass::{ TokenType, auth::authenticate::TokenChallenge, @@ -240,16 +240,17 @@ async fn public_metadata_tokens() { &["example.com".to_string()], ); let challenge_digest = token_challenge.digest().unwrap(); - let metadata = b"Hello world"; - let protocol = TokenProtocol::PublicMetadata { metadata }; + let extension = Extension::new(ExtensionType(0), b"Hello world".to_vec()); + let extensions = Extensions::new(vec![extension]); // Client: Prepare a TokenRequest after having received a challenge let (token_request, token_state) = - TokenRequest::new_with_protocol(rng, public_key, &token_challenge, protocol).unwrap(); + TokenRequest::new_with_extensions(rng, public_key, &token_challenge, extensions.clone()) + .unwrap(); // Issuer server: Issue a TokenResponse let token_response = issuer_server - .issue_token_response_protocol(&issuer_key_store, token_request, protocol) + .issue_token_response(&issuer_key_store, token_request) .await .unwrap(); @@ -261,14 +262,14 @@ async fn public_metadata_tokens() { // Origin server: Redeem the token origin_server - .redeem_token_protocol(&origin_key_store, &nonce_store, token.clone(), protocol) + .redeem_token_with_extensions(&origin_key_store, &nonce_store, token.clone(), &extensions) .await .unwrap(); // Origin server: Test double spend protection assert_eq!( origin_server - .redeem_token_protocol(&origin_key_store, &nonce_store, token, protocol) + .redeem_token_with_extensions(&origin_key_store, &nonce_store, token, &extensions) .await, Err(RedeemTokenError::DoubleSpending) ); From b66fe4cf321e7e5010b64330e10be6a9307c91bf Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Thu, 18 Jun 2026 12:01:24 -0500 Subject: [PATCH 09/13] Address thibmeu review comments --- src/auth/authorize.rs | 12 +++++++--- src/common/errors.rs | 9 ++++++++ src/common/extensions.rs | 5 +++-- src/generic_tokens/request.rs | 2 +- src/lib.rs | 2 -- src/public_tokens/request.rs | 12 ++++------ src/public_tokens/response.rs | 42 +++++++++++++++++++++-------------- 7 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 2a70aa0..0b1d1b1 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -165,7 +165,9 @@ pub fn build_authorization_header( Ok((header_name, header_value)) } -/// Builds an `Authorize` header according to the following scheme: +/// Builds an `Authorize` header according to the following scheme, +/// specified in +/// [`draft-ietf-privacypass-auth-scheme-extensions-03`](https://datatracker.ietf.org/doc/html/draft-ietf-privacypass-auth-scheme-extensions-03): /// /// PrivateToken token="abc...", extensions="def..." /// @@ -235,7 +237,9 @@ pub fn parse_authorization_str(s: &str) -> Result, Pa Ok(token) } -/// Parses an `Authorization` header according to the following scheme: +/// Parses an `Authorization` header according to the following scheme, +/// specified in +/// [`draft-ietf-privacypass-auth-scheme-extensions-03`](https://datatracker.ietf.org/doc/html/draft-ietf-privacypass-auth-scheme-extensions-03): /// /// `PrivateToken token="..." [, extensions="..."]` /// @@ -249,7 +253,9 @@ pub fn parse_authorization_header_ext( parse_authorization_str_ext(s) } -/// Parses an `Authorization` string according to the following scheme: +/// Parses an `Authorization` string according to the following scheme, +/// specified in +/// [`draft-ietf-privacypass-auth-scheme-extensions-03`](https://datatracker.ietf.org/doc/html/draft-ietf-privacypass-auth-scheme-extensions-03): /// /// `PrivateToken token="..." [, extensions="..."]` /// diff --git a/src/common/errors.rs b/src/common/errors.rs index 3335055..a729194 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -195,6 +195,15 @@ pub enum IssueTokenError { #[source] source: BlindRsaError, }, + #[error("Invalid token type: {token_type:?}")] + /// Error when the token type is not supported (e.g. using private token in public issue_token) + InvalidTokenType { + /// Token type found in the token. + token_type: TokenType, + }, + #[error("Expected to have a PBRSA state but there was none")] + /// Error when there is no PBRSA state when using `TokenType::PublicMetadata` + NoPbrsaState, } /// Errors that can occur when redeeming the token. diff --git a/src/common/extensions.rs b/src/common/extensions.rs index aec7f44..08ab90e 100644 --- a/src/common/extensions.rs +++ b/src/common/extensions.rs @@ -1,6 +1,6 @@ //! Types and functions related to the Extensions parameter. //! -//! Specified in `draft-ietf-privacypass-auth-scheme-extensions`. +//! Specified in `draft-ietf-privacypass-auth-scheme-extensions-03`. use std::io::{Read, Write}; use tls_codec::{Deserialize, Serialize, Size, TlsByteVecU16, TlsVecU16}; @@ -13,7 +13,8 @@ use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; pub struct ExtensionType(pub u16); impl ExtensionType { - /// Reserved by `draft-ietf-privacypass-auth-scheme-extensions` + /// Reserved in + /// [`draft-ietf-privacypass-auth-scheme-extensions-03` §3](https://datatracker.ietf.org/doc/html/draft-ietf-privacypass-auth-scheme-extensions-03#section-3) pub const RESERVED: ExtensionType = ExtensionType(0); } diff --git a/src/generic_tokens/request.rs b/src/generic_tokens/request.rs index cf48a2b..6340f27 100644 --- a/src/generic_tokens/request.rs +++ b/src/generic_tokens/request.rs @@ -183,7 +183,7 @@ impl Deserialize for GenericTokenRequest { let mut all_bytes = (peeked).chain(bytes); match token_type { - TokenType::PrivateP384 | TokenType::PrivateMetadata => { + TokenType::PrivateP384 => { let token_request = crate::private_tokens::TokenRequest::tls_deserialize(&mut all_bytes)?; Ok(GenericTokenRequest::PrivateP384(Box::new(token_request))) diff --git a/src/lib.rs b/src/lib.rs index 060912f..509c849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,6 @@ pub enum TokenType { PrivateRistretto255 = 5, /// Publicly verifiable token with public metadata PublicMetadata = 0xDA7A, - /// Privately verifiable token with public metadata - PrivateMetadata = 0xDA7B, } /// Token key ID diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index e9dc5af..2143d97 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -25,8 +25,7 @@ pub struct TokenState { pub(crate) challenge_digest: ChallengeDigest, pub(crate) blinding_result: BlindingResult, pub(crate) public_key: PublicKey, - pub(crate) metadata: Option>, - pub(crate) derived_pk: Option, + pub(crate) pbrsa_state: Option<(Vec, PbrsaPublicKey)>, } /// Token request as specified in the spec: @@ -148,8 +147,7 @@ impl TokenRequest { token_input, challenge_digest, public_key, - metadata: None, - derived_pk: None, + pbrsa_state: None, }; Ok((token_request, token_state)) @@ -211,8 +209,7 @@ impl TokenRequest { source: source.into(), })?; - let m = Some(metadata.to_vec()); - let dpk = Some(derived_pk); + let pbrsa_state = Some((metadata.to_vec(), derived_pk)); debug_assert!(blinding_result.blind_message.len() == NK); let mut blinded_msg = [0u8; NK]; @@ -230,8 +227,7 @@ impl TokenRequest { token_input, challenge_digest, public_key, - metadata: m, - derived_pk: dpk, + pbrsa_state, }; Ok((token_request, token_state)) diff --git a/src/public_tokens/response.rs b/src/public_tokens/response.rs index 730783d..d3d83be 100644 --- a/src/public_tokens/response.rs +++ b/src/public_tokens/response.rs @@ -5,7 +5,7 @@ use generic_array::{GenericArray, typenum::U256}; use log::warn; use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; -use crate::{auth::authorize::Token, common::errors::IssueTokenError}; +use crate::{TokenType, auth::authorize::Token, common::errors::IssueTokenError}; use super::{NK, PublicToken, TokenState}; @@ -32,28 +32,36 @@ impl TokenResponse { let token_type = token_state.token_input.token_type; let blind_sig = BlindSignature(self.blind_sig.to_vec()); - let signature = if let Some(derived_pk) = &token_state.derived_pk { - derived_pk - .finalize( - &blind_sig, - &token_state.blinding_result, - token_input, - token_state.metadata.as_deref(), - ) - .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) - .map_err(|source| IssueTokenError::SignatureFinalizationFailed { - token_type, - source, - })? - } else { - token_state + let signature = match token_type { + TokenType::Public => token_state .public_key .finalize(&blind_sig, &token_state.blinding_result, token_input) .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) .map_err(|source| IssueTokenError::SignatureFinalizationFailed { token_type, source, - })? + })?, + TokenType::PublicMetadata => { + let (metadata, derived_pk) = token_state + .pbrsa_state + .as_ref() + .ok_or(IssueTokenError::NoPbrsaState) + .inspect_err(|e| warn!(error:% = e; "No PBRSA state found"))?; + + derived_pk + .finalize( + &blind_sig, + &token_state.blinding_result, + token_input, + Some(metadata), + ) + .inspect_err(|e| warn!(error:% = e; "Failed to finalize blind signature")) + .map_err(|source| IssueTokenError::SignatureFinalizationFailed { + token_type, + source, + })? + } + _ => return Err(IssueTokenError::InvalidTokenType { token_type }), }; let authenticator: GenericArray = *GenericArray::from_slice(&signature[0..256]); From 75ada93ed19ccf4ed57543b08ce3bacf07b38a76 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Mon, 22 Jun 2026 15:21:34 -0500 Subject: [PATCH 10/13] Simplify TokenRequest::new_*, redeem_token_*, update ExtensionType doc comment --- src/common/extensions.rs | 2 +- src/public_tokens/request.rs | 131 +++++++++++++++-------------------- src/public_tokens/server.rs | 28 -------- 3 files changed, 55 insertions(+), 106 deletions(-) diff --git a/src/common/extensions.rs b/src/common/extensions.rs index 08ab90e..a53983d 100644 --- a/src/common/extensions.rs +++ b/src/common/extensions.rs @@ -13,7 +13,7 @@ use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; pub struct ExtensionType(pub u16); impl ExtensionType { - /// Reserved in + /// Defined in /// [`draft-ietf-privacypass-auth-scheme-extensions-03` §3](https://datatracker.ietf.org/doc/html/draft-ietf-privacypass-auth-scheme-extensions-03#section-3) pub const RESERVED: ExtensionType = ExtensionType(0); } diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index 2143d97..16f571a 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -103,54 +103,7 @@ impl TokenRequest { public_key: PublicKey, challenge: &TokenChallenge, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { - let mut nonce: Nonce = [0u8; 32]; - rng.fill_bytes(&mut nonce); - - let challenge_digest = challenge - .digest() - .inspect_err(|e| warn!(error:% = e; "Failed to create challenge digest")) - .map_err(|source| IssueTokenRequestError::InvalidTokenChallenge { source })?; - - let token_key_id = public_key_to_token_key_id(&public_key).map_err(|source| { - IssueTokenRequestError::BlindingError { - source: source.into(), - } - })?; - - let token_input = TokenInput::new(TokenType::Public, nonce, challenge_digest, token_key_id); - - // nonce = random(32) - // challenge_digest = SHA256(challenge) - // token_input = concat(0x0002, nonce, challenge_digest, token_key_id) - // blinded_msg, blind_inv = rsabssa_blind(pkI, token_input) - - let blinding_result = public_key - .blind(rng, token_input.serialize()) - .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; - - debug_assert!(blinding_result.blind_message.len() == NK); - let mut blinded_msg = [0u8; NK]; - blinded_msg.copy_from_slice(blinding_result.blind_message.as_slice()); - - let token_request = TokenRequest { - token_type: TokenType::Public, - truncated_token_key_id: truncate_token_key_id(&token_key_id), - blinded_msg, - extensions: None, - }; - - let token_state = TokenState { - blinding_result, - token_input, - challenge_digest, - public_key, - pbrsa_state: None, - }; - - Ok((token_request, token_state)) + Self::new_maybe_extensions(rng, public_key, challenge, None) } /// Issue a new token request using the given extensions @@ -162,6 +115,15 @@ impl TokenRequest { public_key: PublicKey, challenge: &TokenChallenge, extensions: Extensions, + ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { + Self::new_maybe_extensions(rng, public_key, challenge, Some(extensions)) + } + + fn new_maybe_extensions( + rng: &mut R, + public_key: PublicKey, + challenge: &TokenChallenge, + extensions: Option, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { let mut nonce: Nonce = [0u8; 32]; rng.fill_bytes(&mut nonce); @@ -177,49 +139,64 @@ impl TokenRequest { } })?; - let token_input = TokenInput::new( - TokenType::PublicMetadata, - nonce, - challenge_digest, - token_key_id, - ); + let token_type = if extensions.is_some() { + TokenType::PublicMetadata + } else { + TokenType::Public + }; + + let token_input = TokenInput::new(token_type, nonce, challenge_digest, token_key_id); // nonce = random(32) // challenge_digest = SHA256(challenge) // token_input = concat(0x0002, nonce, challenge_digest, token_key_id) // blinded_msg, blind_inv = rsabssa_blind(pkI, token_input) - let metadata = extensions - .tls_serialize_detached() - .inspect_err(|e| warn!(error:% = e; "Failed to serialize extensions")) - .map_err(|source| IssueTokenRequestError::ExtensionSerializationError { source })?; - - let pbrsa_pk: PbrsaPublicKey = PartiallyBlindPublicKey::new(public_key.as_ref().clone()); - let derived_pk = pbrsa_pk - .derive_public_key_for_metadata(&metadata) - .inspect_err(|e| warn!(error:% = e; "Failed to derive metadata public key")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; - - let blinding_result = derived_pk - .blind(rng, token_input.serialize(), Some(&metadata)) - .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) - .map_err(|source| IssueTokenRequestError::BlindingError { - source: source.into(), - })?; - - let pbrsa_state = Some((metadata.to_vec(), derived_pk)); + let (blinding_result, pbrsa_state) = if let Some(ref extensions) = extensions { + let metadata = extensions + .tls_serialize_detached() + .inspect_err(|e| warn!(error:% = e; "Failed to serialize extensions")) + .map_err(|source| IssueTokenRequestError::ExtensionSerializationError { source })?; + + let pbrsa_pk: PbrsaPublicKey = + PartiallyBlindPublicKey::new(public_key.as_ref().clone()); + let derived_pk = pbrsa_pk + .derive_public_key_for_metadata(&metadata) + .inspect_err(|e| warn!(error:% = e; "Failed to derive metadata public key")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + let blinding_result = derived_pk + .blind(rng, token_input.serialize(), Some(&metadata)) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + let pbrsa_state = Some((metadata, derived_pk)); + + (blinding_result, pbrsa_state) + } else { + let blinding_result = public_key + .blind(rng, token_input.serialize()) + .inspect_err(|e| warn!(error:% = e; "Failed to blind token input")) + .map_err(|source| IssueTokenRequestError::BlindingError { + source: source.into(), + })?; + + (blinding_result, None) + }; debug_assert!(blinding_result.blind_message.len() == NK); let mut blinded_msg = [0u8; NK]; blinded_msg.copy_from_slice(blinding_result.blind_message.as_slice()); let token_request = TokenRequest { - token_type: TokenType::PublicMetadata, + token_type, truncated_token_key_id: truncate_token_key_id(&token_key_id), blinded_msg, - extensions: Some(extensions), + extensions, }; let token_state = TokenState { diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index dcffc41..8f6d4da 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -437,20 +437,6 @@ impl OriginServer { token: Token, extensions: &Extensions, ) -> Result<(), RedeemTokenError> { - if token.token_type() != TokenType::PublicMetadata { - return Err(RedeemTokenError::TokenTypeMismatch { - expected: TokenType::PublicMetadata, - found: token.token_type(), - }); - } - - if token.authenticator().len() != KEYSIZE_IN_BYTES { - return Err(RedeemTokenError::InvalidAuthenticatorLength { - expected: KEYSIZE_IN_BYTES, - found: token.authenticator().len(), - }); - } - let nonce = token.nonce(); if !nonce_store.reserve(&nonce).await { @@ -499,20 +485,6 @@ impl OriginServer { nonce_store: &NS, token: Token, ) -> Result<(), RedeemTokenError> { - if token.token_type() != TokenType::Public { - return Err(RedeemTokenError::TokenTypeMismatch { - expected: TokenType::Public, - found: token.token_type(), - }); - } - - if token.authenticator().len() != KEYSIZE_IN_BYTES { - return Err(RedeemTokenError::InvalidAuthenticatorLength { - expected: KEYSIZE_IN_BYTES, - found: token.authenticator().len(), - }); - } - let nonce = token.nonce(); if !nonce_store.reserve(&nonce).await { From a003bc7602aa9041e029c7e196258e165c0f03a3 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Tue, 23 Jun 2026 10:23:02 -0500 Subject: [PATCH 11/13] Don't support `TokenType::PublicMetadata` when deserializing generic requests --- src/generic_tokens/request.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/generic_tokens/request.rs b/src/generic_tokens/request.rs index 6340f27..c4cb6de 100644 --- a/src/generic_tokens/request.rs +++ b/src/generic_tokens/request.rs @@ -188,7 +188,7 @@ impl Deserialize for GenericTokenRequest { crate::private_tokens::TokenRequest::tls_deserialize(&mut all_bytes)?; Ok(GenericTokenRequest::PrivateP384(Box::new(token_request))) } - TokenType::Public | TokenType::PublicMetadata => { + TokenType::Public => { let token_request = crate::public_tokens::TokenRequest::tls_deserialize(&mut all_bytes)?; Ok(GenericTokenRequest::Public(Box::new(token_request))) @@ -200,6 +200,10 @@ impl Deserialize for GenericTokenRequest { token_request, ))) } + _ => Err(tls_codec::Error::DecodingError(format!( + "Unsupported token type: {:?}", + token_type + ))), } } } From 895e6ab861bd2d7583234c560e551374bb63688a Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Wed, 24 Jun 2026 11:50:01 -0500 Subject: [PATCH 12/13] Add comment about validation + update TokenRequest docs --- src/public_tokens/request.rs | 10 ++++++++++ src/public_tokens/server.rs | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index 16f571a..cde8130 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -37,6 +37,16 @@ pub struct TokenState { /// uint8_t blinded_msg[Nk]; /// } TokenRequest; /// ``` +/// +/// or, if created with extensions: +/// ```c +/// struct { +/// TokenRequest request; +/// Extensions extensions; +/// } ExtendedTokenRequest; +/// ``` +/// As specified in +/// [`draft-ietf-privacypass-public-metadata-issuance-03 ยง6.1`](https://www.ietf.org/archive/id/draft-ietf-privacypass-public-metadata-issuance-03.html#section-6.1). #[derive(Debug, Clone, PartialEq)] pub struct TokenRequest { pub(crate) token_type: TokenType, diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index 8f6d4da..f769bac 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -247,6 +247,11 @@ impl IssuerServer { /// Issues a new token response. /// + /// Note that this method does not validate the extensions provided in the token request. If + /// such validation is required (for example, validating that the provided extension types are + /// allowed by this issuer), issuers are expected to perform this validation before calling + /// this method. + /// /// # Errors /// Returns an error if the token request is invalid, or if token_type == PublicMetadata and the /// key is not PBRSA compatible. From 7dea44665da1c45a4ca58f0e509fe458c56afd27 Mon Sep 17 00:00:00 2001 From: Yash Karandikar Date: Thu, 25 Jun 2026 15:08:29 -0500 Subject: [PATCH 13/13] Adhere strictly to draft-ietf-privacypass-auth-scheme-extensions-03 and RFC 9577 --- src/auth/authorize.rs | 115 +++++++++++++++--------------------------- src/auth/mod.rs | 9 ++-- 2 files changed, 45 insertions(+), 79 deletions(-) diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 0b1d1b1..798705b 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -1,15 +1,11 @@ //! This module contains the authorization logic for redemption phase of the //! protocol. -use base64::{ - Engine as _, alphabet, - engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, general_purpose::URL_SAFE}, -}; +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use generic_array::{ArrayLength, GenericArray}; use http::{HeaderValue, header::HeaderName}; use nom::{ IResult, Parser, - branch::alt, bytes::complete::{tag, tag_no_case}, multi::{many1, separated_list1}, }; @@ -17,23 +13,13 @@ use std::io::Write; use thiserror::Error; use tls_codec::{Deserialize, Error, Serialize, Size}; -use crate::{ChallengeDigest, Nonce, TokenKeyId, TokenType, common::extensions::Extensions}; +use crate::{ + ChallengeDigest, Nonce, TokenKeyId, TokenType, auth::maybe_unquote, + common::extensions::Extensions, +}; use super::{key_name, opt_spaces, space, unquote}; -// Previous versions of the Token Extensions draft (`draft-ietf-privacypass-auth-scheme-extensions`) -// didn't specify whether the token should be encoded with padding, leading some implementations to -// always encode without padding. Thus, we need to decode extensions with this engine in order to -// support those implementations. -// -// However, the latest version states that "the base64url value MUST include padding", so when -// generating the header in `build_authorization_header_ext`, URL_SAFE should be used instead of -// this engine. -const URL_SAFE_INDIFFERENT: GeneralPurpose = GeneralPurpose::new( - &alphabet::URL_SAFE, - GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent), -); - /// A Token as defined in The Privacy Pass HTTP Authentication Scheme: /// /// ```text @@ -178,7 +164,7 @@ pub fn build_authorization_header_ext( extensions: &Extensions, ) -> Result<(HeaderName, HeaderValue), BuildError> { let value = format!( - // format specified by draft-ietf-privacypass-auth-scheme-extensions + // format specified by draft-ietf-privacypass-auth-scheme-extensions-03 // draft requires that the parameters must be enclosed in double quotes "PrivateToken token=\"{}\", extensions=\"{}\"", URL_SAFE.encode( @@ -186,7 +172,6 @@ pub fn build_authorization_header_ext( .tls_serialize_detached() .map_err(|_| BuildError::InvalidToken)? ), - // See comment above URL_SAFE_INDIFFERENT URL_SAFE.encode( extensions .tls_serialize_detached() @@ -232,9 +217,15 @@ pub fn parse_authorization_header( /// # Errors /// Returns an error if the header value is not valid. pub fn parse_authorization_str(s: &str) -> Result, ParseError> { - let token = parse_authorization_str_ext(s)?.0; + // in RFC 9577 section 2.2.2, it says the token field might be a quoted string, so when + // parsing just a token, we need to accept values that are not quoted as well. + let tokens = parse_header_value(s, false)?; - Ok(token) + tokens + .into_iter() + .next() + .ok_or(ParseError::InvalidInput) + .map(|(token, _)| token) } /// Parses an `Authorization` header according to the following scheme, @@ -264,7 +255,10 @@ pub fn parse_authorization_header_ext( pub fn parse_authorization_str_ext( s: &str, ) -> Result<(Token, Option), ParseError> { - let tokens = parse_header_value(s)?; + // In draft-ietf-privacypass-auth-scheme-extensions-03, it says token and extensions MUST be + // enclosed in double-quotes. Thus, when parsing both tokens and extensions, we should + // strictly accept only quoted values. + let tokens = parse_header_value(s, true)?; tokens.into_iter().next().ok_or(ParseError::InvalidInput) } @@ -283,14 +277,22 @@ pub enum ParseError { InvalidExtensions, } -fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { +fn parse_key_value(input: &str, strict_quotes: bool) -> IResult<&str, (&str, &str)> { let (input, _) = opt_spaces(input)?; let (input, key) = key_name(input)?; let (input, _) = opt_spaces(input)?; let (input, _) = tag("=").parse(input)?; let (input, _) = opt_spaces(input)?; let (input, value) = match key.to_lowercase().as_str() { - "token" | "extensions" => unquote(input)?, + // see comments in parse_authorization_str and parse_authorization_str_ext + "token" => { + if strict_quotes { + unquote(input)? + } else { + maybe_unquote(input)? + } + } + "extensions" => unquote(input)?, _ => { return Err(nom::Err::Failure(nom::error::make_error( input, @@ -301,16 +303,12 @@ fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { Ok((input, (key, value))) } -fn parse_private_token(input: &str) -> IResult<&str, (&str, Option<&str>)> { +fn parse_private_token(input: &str, strict_quotes: bool) -> IResult<&str, (&str, Option<&str>)> { let (input, _) = opt_spaces(input)?; let (input, _) = tag_no_case("PrivateToken").parse(input)?; let (input, _) = many1(space).parse(input)?; - let (input, key_values) = separated_list1( - alt((tag(","), tag(" "))), // header could be separated by a space in older specs, so we - // need to support it - parse_key_value, - ) - .parse(input)?; + let (input, key_values) = + separated_list1(tag(","), |i| parse_key_value(i, strict_quotes)).parse(input)?; let mut token = None; let mut extensions = None; @@ -339,15 +337,20 @@ fn parse_private_token(input: &str) -> IResult<&str, (&str, Option<&str>)> { Ok((input, (token, extensions))) } -fn parse_private_tokens(input: &str) -> IResult<&str, Vec<(&str, Option<&str>)>> { - separated_list1(tag(","), parse_private_token).parse(input) +fn parse_private_tokens( + input: &str, + strict_quotes: bool, +) -> IResult<&str, Vec<(&str, Option<&str>)>> { + separated_list1(tag(","), |i| parse_private_token(i, strict_quotes)).parse(input) } #[allow(clippy::type_complexity)] fn parse_header_value( input: &str, + strict_quotes: bool, ) -> Result, Option)>, ParseError> { - let (output, tokens) = parse_private_tokens(input).map_err(|_| ParseError::InvalidInput)?; + let (output, tokens) = + parse_private_tokens(input, strict_quotes).map_err(|_| ParseError::InvalidInput)?; if !output.is_empty() { return Err(ParseError::InvalidInput); } @@ -356,7 +359,7 @@ fn parse_header_value( .map(|(token_value, extensions_value)| { let ext = extensions_value .map(|x| { - let decoded = URL_SAFE_INDIFFERENT + let decoded = URL_SAFE .decode(x) .map_err(|_| ParseError::InvalidExtensions)?; Extensions::tls_deserialize(&mut decoded.as_slice()) @@ -388,7 +391,6 @@ mod tests { use crate::common::extensions::{Extension, ExtensionType, Extensions}; use generic_array::GenericArray; use generic_array::typenum::U32; - use http::HeaderValue; #[test] fn builder_parser_test() { @@ -550,41 +552,4 @@ mod tests { assert_eq!(maybe_extensions, Some(extensions.clone())); } } - - /// This is the same test as `builder_parser_extensions_test`, but we replace the `, ` - /// separator with ` ` (single space) to make sure we can handle tokens generated by clients - /// using an older version of the TOKEN-EXTENSION spec. - #[test] - fn rfc_9110_regression_test() { - let nonce = [1u8; 32]; - let challenge_digest = [2u8; 32]; - let token_key_id = [3u8; 32]; - let authenticator = [4u8; 32]; - let token = Token::::new( - TokenType::PrivateP384, - nonce, - challenge_digest, - token_key_id, - *GenericArray::from_slice(&authenticator), - ); - - let extension = Extension::new(ExtensionType(5), b"hello world".to_vec()); - let extensions = Extensions::new(vec![extension]); - let (header_name, header_value) = - build_authorization_header_ext(&token, &extensions).unwrap(); - - let header_value = - HeaderValue::from_str(&header_value.to_str().unwrap().replace(", ", " ")).unwrap(); - - assert_eq!(header_name, http::header::AUTHORIZATION); - - let (token, maybe_extensions) = - parse_authorization_header_ext::(&header_value).unwrap(); - assert_eq!(token.token_type(), TokenType::PrivateP384); - assert_eq!(token.nonce(), nonce); - assert_eq!(token.challenge_digest(), &challenge_digest); - assert_eq!(token.token_key_id(), &token_key_id); - assert_eq!(token.authenticator(), &authenticator); - assert_eq!(maybe_extensions, Some(extensions)); - } } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 0b0bf94..f7ffc5b 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -56,8 +56,9 @@ pub(crate) fn surrounded_by_alphanumeric(input: &str) -> bool { } pub(crate) fn unquote(input: &str) -> IResult<&str, &str> { - // older versions of the extensions draft specified that values should not be surrounded with double - // quotes, but the newest version of the draft specifies that values should be quoted. - // The use of alt() lets us support both versions - alt((delimited(tag("\""), base64_char, tag("\"")), base64_char)).parse(input) + delimited(tag("\""), base64_char, tag("\"")).parse(input) +} + +pub(crate) fn maybe_unquote(input: &str) -> IResult<&str, &str> { + alt((unquote, base64_char)).parse(input) }