diff --git a/Cargo.toml b/Cargo.toml index f84f88b..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/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 78ab70e..798705b 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -13,9 +13,12 @@ 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, auth::maybe_unquote, + common::extensions::Extensions, +}; -use super::{base64_char, key_name, opt_spaces, space}; +use super::{key_name, opt_spaces, space, unquote}; /// A Token as defined in The Privacy Pass HTTP Authentication Scheme: /// @@ -148,12 +151,49 @@ pub fn build_authorization_header( Ok((header_name, header_value)) } +/// 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..." +/// +/// # 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-03 + // 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)? + ), + 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: @@ -166,9 +206,61 @@ 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].clone(); - Ok(token) + + 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> { + // 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)?; + + tokens + .into_iter() + .next() + .ok_or(ParseError::InvalidInput) + .map(|(token, _)| token) +} + +/// 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="..."]` +/// +/// # 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)?; + + parse_authorization_str_ext(s) +} + +/// 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="..."]` +/// +/// # Errors +/// Returns an error if the header value is not valid. +pub fn parse_authorization_str_ext( + s: &str, +) -> Result<(Token, Option), ParseError> { + // 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) } /// Parsing error for the `WWW-Authenticate` header values @@ -180,16 +272,27 @@ 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)> { +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" => base64_char(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, @@ -200,13 +303,15 @@ 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, 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(tag(","), 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; let err = nom::Err::Failure(nom::error::make_error(input, nom::error::ErrorKind::Tag)); for (key, value) in key_values { @@ -217,61 +322,234 @@ 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>> { - 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) } -fn parse_header_value(input: &str) -> Result>, ParseError> { - let (output, tokens) = parse_private_tokens(input).map_err(|_| ParseError::InvalidInput)?; +#[allow(clippy::type_complexity)] +fn parse_header_value( + input: &str, + strict_quotes: bool, +) -> Result, Option)>, ParseError> { + let (output, tokens) = + parse_private_tokens(input, strict_quotes).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 + .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; - 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), + ); + + let (header_name, header_value) = build_authorization_header(&token).unwrap(); + + 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::PublicMetadata, + 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::PublicMetadata); + 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)); + } + + 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", + ), + }, + ]; - assert_eq!(header_name, http::header::AUTHORIZATION); + // 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]); - 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); + 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())); + } + } } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 920f1dc..f7ffc5b 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,11 @@ pub(crate) fn surrounded_by_alphanumeric(input: &str) -> bool { true } + +pub(crate) fn unquote(input: &str) -> IResult<&str, &str> { + delimited(tag("\""), base64_char, tag("\"")).parse(input) +} + +pub(crate) fn maybe_unquote(input: &str) -> IResult<&str, &str> { + alt((unquote, base64_char)).parse(input) +} diff --git a/src/common/errors.rs b/src/common/errors.rs index 64e4a33..a729194 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. @@ -181,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. @@ -229,4 +252,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/common/extensions.rs b/src/common/extensions.rs new file mode 100644 index 0000000..a53983d --- /dev/null +++ b/src/common/extensions.rs @@ -0,0 +1,121 @@ +//! Types and functions related to the Extensions parameter. +//! +//! Specified in `draft-ietf-privacypass-auth-scheme-extensions-03`. + +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 { + /// 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); +} + +/// 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; diff --git a/src/generic_tokens/request.rs b/src/generic_tokens/request.rs index b5c5dda..c4cb6de 100644 --- a/src/generic_tokens/request.rs +++ b/src/generic_tokens/request.rs @@ -200,6 +200,10 @@ impl Deserialize for GenericTokenRequest { token_request, ))) } + _ => Err(tls_codec::Error::DecodingError(format!( + "Unsupported token type: {:?}", + token_type + ))), } } } diff --git a/src/lib.rs b/src/lib.rs index 5b874f9..509c849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,8 @@ pub enum TokenType { Public = 2, /// Private ristretto255 token PrivateRistretto255 = 5, + /// Publicly verifiable token with public metadata + PublicMetadata = 0xDA7A, } /// Token key ID diff --git a/src/public_tokens/request.rs b/src/public_tokens/request.rs index f6970e9..cde8130 100644 --- a/src/public_tokens/request.rs +++ b/src/public_tokens/request.rs @@ -1,12 +1,16 @@ //! Request implementation of the Publicly Verifiable Token protocol. -use blind_rsa_signatures::BlindingResult; +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, truncate_token_key_id, @@ -21,6 +25,7 @@ pub struct TokenState { pub(crate) challenge_digest: ChallengeDigest, pub(crate) blinding_result: BlindingResult, pub(crate) public_key: PublicKey, + pub(crate) pbrsa_state: Option<(Vec, PbrsaPublicKey)>, } /// Token request as specified in the spec: @@ -32,11 +37,70 @@ pub struct TokenState { /// uint8_t blinded_msg[Nk]; /// } TokenRequest; /// ``` -#[derive(Debug, Clone, PartialEq, TlsDeserialize, TlsSerialize, TlsSize)] +/// +/// 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, 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 { @@ -48,6 +112,28 @@ impl TokenRequest { rng: &mut R, public_key: PublicKey, challenge: &TokenChallenge, + ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { + Self::new_maybe_extensions(rng, public_key, challenge, None) + } + + /// Issue a new token request using the given extensions + /// + /// # Errors + /// 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_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); @@ -63,28 +149,64 @@ impl TokenRequest { } })?; + 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 token_input = TokenInput::new(TokenType::Public, nonce, challenge_digest, token_key_id); + 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 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 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::Public, + token_type, truncated_token_key_id: truncate_token_key_id(&token_key_id), blinded_msg, + extensions, }; let token_state = TokenState { @@ -92,7 +214,9 @@ impl TokenRequest { token_input, challenge_digest, public_key, + pbrsa_state, }; + Ok((token_request, token_state)) } } diff --git a/src/public_tokens/response.rs b/src/public_tokens/response.rs index 7f9fb7c..d3d83be 100644 --- a/src/public_tokens/response.rs +++ b/src/public_tokens/response.rs @@ -29,19 +29,44 @@ 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 = 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]); 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..f769bac 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -1,21 +1,34 @@ //! 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, +}; 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}; +use tls_codec::Serialize; type KeyPair = GenericKeyPair; type PublicKey = GenericPublicKey; +pub(crate) type PbrsaKeyPair = PartiallyBlindKeyPair; +pub(crate) type PbrsaPublicKey = PartiallyBlindPublicKey; + +use crate::common::extensions::Extensions; use crate::{ - COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TokenType, TruncatedTokenKeyId, + 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}; @@ -95,19 +108,69 @@ 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; /// 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. @@ -141,32 +204,115 @@ impl IssuerServer { Err(CreateKeypairError::CollisionExhausted) } + /// Creates a new partially-blinded keypair and inserts it into the key store. + /// + /// # Errors + /// 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 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; + } + + 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); + } + } + Err(CreateKeypairError::CollisionExhausted) + } + /// 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. + /// 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( &self, key_store: &IKS, token_request: TokenRequest, ) -> Result { - if token_request.token_type != TokenType::Public { + 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: TokenType::Public, 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 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( + |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 })? + } + 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); let mut blind_sig = [0u8; NK]; @@ -194,6 +340,82 @@ 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, + extensions: Option<&Extensions>, +) -> Result<(), RedeemTokenError> { + 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: TokenType::Public, + 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 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) { + 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 + } + } + } + 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 { + token_type: token.token_type(), + }); + } + Ok(()) +} + /// Server-side implementation of Publicly Verifiable Token protocol for /// origins. #[derive(Default, Debug)] @@ -205,6 +427,59 @@ impl OriginServer { Self {} } + /// 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_with_extensions< + OKS: OriginKeyStore, + NS: NonceStore, + Nk: ArrayLength, + >( + &self, + key_store: &OKS, + nonce_store: &NS, + token: Token, + extensions: &Extensions, + ) -> Result<(), RedeemTokenError> { + 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, Some(extensions)).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) + } + } + } + /// Redeems a token. /// /// # Errors @@ -215,27 +490,7 @@ impl OriginServer { nonce_store: &NS, token: Token, ) -> Result<(), RedeemTokenError> { - let token_type = token.token_type(); - if token_type != TokenType::Public { - return Err(RedeemTokenError::TokenTypeMismatch { - expected: TokenType::Public, - found: 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 nonce = token.nonce(); - let token_input = TokenInput::new( - token_type, - nonce, - *token.challenge_digest(), - *token.token_key_id(), - ); if !nonce_store.reserve(&nonce).await { return Err(RedeemTokenError::DoubleSpending); @@ -248,18 +503,14 @@ 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| { - public_key - .verify(&signature, None, &token_input_bytes) - .inspect_err(|e| warn!(error:% = e; "Verify failed")) - .is_ok() - }); + let verified = public_keys + .iter() + .any(|public_key| verify_token(public_key, &token, None).is_ok()); if !verified { - return Err(RedeemTokenError::InvalidSignature { token_type }); + return Err(RedeemTokenError::InvalidSignature { + token_type: token.token_type(), + }); } Ok(()) } diff --git a/tests/kat_public_metadata.rs b/tests/kat_public_metadata.rs new file mode 100644 index 0000000..b56c718 --- /dev/null +++ b/tests/kat_public_metadata.rs @@ -0,0 +1,130 @@ +use blind_rsa_signatures::{Deterministic, KeyPair, PSS, PublicKey, SecretKey, Sha384}; +use serde::{Deserialize, Serialize}; +use tls_codec::{Deserialize as _, Serialize as _}; + +use privacypass::{ + TokenType, + auth::authenticate::TokenChallenge, + common::extensions::Extensions, + public_tokens::{ + 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; + + let extensions: Extensions = Extensions::tls_deserialize_exact(vector.extensions).unwrap(); + + 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_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 + ); + + let token_response = issuer_server + .issue_token_response(&issuer_key_store, token_request) + .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_with_extensions(&origin_key_store, &nonce_store, token, &extensions) + .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" + } +] diff --git a/tests/public_tokens.rs b/tests/public_tokens.rs index 3aa5a6d..d0be832 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::common::extensions::{Extension, ExtensionType, Extensions}; use privacypass::{ TokenType, auth::authenticate::TokenChallenge, @@ -170,3 +173,104 @@ 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 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_extensions(rng, public_key, &token_challenge, extensions.clone()) + .unwrap(); + + // Issuer server: Issue a TokenResponse + let token_response = issuer_server + .issue_token_response(&issuer_key_store, token_request) + .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_with_extensions(&origin_key_store, &nonce_store, token.clone(), &extensions) + .await + .unwrap(); + + // Origin server: Test double spend protection + assert_eq!( + origin_server + .redeem_token_with_extensions(&origin_key_store, &nonce_store, token, &extensions) + .await, + Err(RedeemTokenError::DoubleSpending) + ); +}