From dc417321b5bb4b7edaef86d58a4e7e81e29b8f54 Mon Sep 17 00:00:00 2001 From: Chris Branch Date: Thu, 16 Nov 2023 14:30:57 +0000 Subject: [PATCH 1/3] Parse extensions provided in the HTTP Authorization header Extensions may be used in new Privacy Pass issuance protocols to encode additional information alongside a token. --- src/auth/authorize.rs | 108 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 92cdc76..42eeca0 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -148,6 +148,30 @@ pub fn build_authorization_header>( Ok((header_name, header_value)) } +/// Builds a `Authorize` header according to the following scheme: +/// +/// `PrivateToken token=...,extensions=...` +/// +/// # Errors +/// Returns an error if the token is not valid. +pub fn build_authorization_header_ext>( + token: &Token, + extensions: &[u8], +) -> Result<(HeaderName, HeaderValue), BuildError> { + let value = format!( + "PrivateToken token={},extensions={}", + URL_SAFE.encode( + token + .tls_serialize_detached() + .map_err(|_| BuildError::InvalidToken)? + ), + URL_SAFE.encode(extensions), + ); + 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(Error, Debug)] pub enum BuildError { @@ -167,10 +191,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(Error, Debug)] pub enum ParseError { @@ -189,7 +227,7 @@ fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { let (input, _) = tag("=")(input)?; let (input, _) = opt_spaces(input)?; let (input, value) = match key.to_lowercase().as_str() { - "token" => base64_char(input)?, + "token" | "extensions" => base64_char(input)?, _ => { return Err(nom::Err::Failure(nom::error::make_error( input, @@ -200,13 +238,14 @@ 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")(input)?; let (input, _) = many1(space)(input)?; let (input, key_values) = separated_list1(tag(","), parse_key_value)(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,33 +256,45 @@ 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)(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( - &mut URL_SAFE - .decode(token_value) - .map_err(|_| ParseError::InvalidToken)? - .as_slice(), - ) - .map_err(|_| ParseError::InvalidToken) + .map(|(token_value, extensions_value)| { + let ext = extensions_value.and_then(|x| URL_SAFE.decode(x).ok()); + Ok(( + Token::tls_deserialize( + &mut URL_SAFE + .decode(token_value) + .map_err(|_| ParseError::InvalidToken)? + .as_slice(), + ) + .map_err(|_| ParseError::InvalidToken)?, + ext, + )) }) .collect::, _>>()?; Ok(tokens) @@ -275,3 +326,32 @@ fn builder_parser_test() { assert_eq!(token.token_key_id(), &token_key_id); assert_eq!(token.authenticator(), &authenticator); } + +#[test] +fn builder_parser_extensions_test() { + 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::Private, + nonce, + challenge_digest, + token_key_id, + GenericArray::clone_from_slice(&authenticator), + ); + let extensions = b"hello world"; + 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::Private); + 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.to_vec())); +} From 52b497069518299e3b284c61b575d7e04ed4f65c Mon Sep 17 00:00:00 2001 From: Chris Branch Date: Thu, 16 Nov 2023 22:28:59 +0000 Subject: [PATCH 2/3] Implement the public metadata issuance protocol This is a variant of the Privacy Pass issuance protocol that encodes public information that is cryptographically bound to the token but visible to all parties (i.e. the metadata is cleartext). A new API for issuing public tokens has been created which is extensible in terms of the issuance protocol the client wishes to use. The basic Privacy Pass issuance protocol is implemented in terms of this generic implementation. https://datatracker.ietf.org/doc/html/draft-hendrickson-privacypass-public-metadata --- Cargo.toml | 2 + src/lib.rs | 4 ++ src/public_tokens/client.rs | 59 +++++++++++++---- src/public_tokens/mod.rs | 128 ++++++++++++++++++++++++++++++++++++ src/public_tokens/server.rs | 58 ++++++++++++---- tests/public_tokens.rs | 113 ++++++++++++++++++++++++++++++- 6 files changed, 337 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36b63d0..9c09c68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ categories = ["cryptography", "privacy"] async-trait = "0.1.56" base64 = "0.21.0" generic-array = "0.14.5" +hkdf = "0.12.3" rand = "0.8.5" serde = "1" sha2 = "0.10.2" @@ -32,6 +33,7 @@ p384 = { version = "0.13.0", default-features = false, features = [ "voprf", ] } blind-rsa-signatures = "0.15.0" +num-bigint-dig = "0.8" http = "0.2.8" typenum = "1.15.0" nom = "7" diff --git a/src/lib.rs b/src/lib.rs index 97a5615..3933d1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,10 @@ pub enum TokenType { Public = 2, /// Batched token Batched = 0xF91A, + /// 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/client.rs b/src/public_tokens/client.rs index 5c8c45c..aec15b6 100644 --- a/src/public_tokens/client.rs +++ b/src/public_tokens/client.rs @@ -7,10 +7,13 @@ use thiserror::Error; use crate::{ auth::{authenticate::TokenChallenge, authorize::Token}, - ChallengeDigest, KeyId, TokenInput, TokenType, + ChallengeDigest, KeyId, TokenInput, }; -use super::{key_id_to_token_key_id, public_key_to_key_id, Nonce, TokenRequest, TokenResponse, NK}; +use super::{ + key_id_to_token_key_id, public_key_to_key_id, Nonce, TokenProtocol, TokenRequest, + TokenResponse, NK, +}; /// Client-side state that is kept between the token requests and token responses. #[derive(Debug)] @@ -18,6 +21,11 @@ pub struct TokenState { blinding_result: BlindingResult, token_input: TokenInput, challenge_digest: ChallengeDigest, + /// A prepared version of token_input + prepared_input: Vec, + /// An augmented public key if used by the issuance protocol. + /// If none, the client's public key should be used. + public_key: Option, } /// Errors that can occur when issuing token requests. @@ -55,14 +63,15 @@ impl Client { Self { key_id, public_key } } - /// Issue a token request. + /// Issue a token request using the specified Privacy Pass issuance protocol. /// /// # Errors /// Returns an error if the challenge is invalid. - pub fn issue_token_request( + pub fn issue_token_request_protocol( &mut self, rng: &mut R, challenge: TokenChallenge, + protocol: TokenProtocol, ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { let mut nonce: Nonce = [0u8; 32]; rng.fill_bytes(&mut nonce); @@ -76,12 +85,16 @@ impl Client { // 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, self.key_id); + let token_input = + TokenInput::new(protocol.token_type(), nonce, challenge_digest, self.key_id); + let prepared_input = protocol.prepare_message(token_input.serialize()); + let public_key = protocol.augment_public_key(&self.public_key); let options = Options::default(); - let blinding_result = self - .public_key - .blind(rng, token_input.serialize(), false, &options) + let blinding_result = public_key + .as_ref() + .unwrap_or(&self.public_key) + .blind(rng, &prepared_input, false, &options) .map_err(|_| IssueTokenRequestError::BlindingError)?; debug_assert!(blinding_result.blind_msg.len() == NK); @@ -89,7 +102,7 @@ impl Client { blinded_msg.copy_from_slice(blinding_result.blind_msg.as_slice()); let token_request = TokenRequest { - token_type: TokenType::Public, + token_type: protocol.token_type(), token_key_id: key_id_to_token_key_id(&self.key_id), blinded_msg, }; @@ -98,10 +111,24 @@ impl Client { blinding_result, token_input, challenge_digest, + prepared_input, + public_key, }; Ok((token_request, token_state)) } + /// Issue a token request. + /// + /// # Errors + /// Returns an error if the challenge is invalid. + pub fn issue_token_request( + &mut self, + rng: &mut R, + challenge: TokenChallenge, + ) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> { + self.issue_token_request_protocol(rng, challenge, TokenProtocol::Basic) + } + /// Issue a token. /// /// # Errors @@ -112,23 +139,27 @@ impl Client { token_state: &TokenState, ) -> Result, IssueTokenError> { // authenticator = rsabssa_finalize(pkI, nonce, blind_sig, blind_inv) - let token_input = token_state.token_input.serialize(); let options = Options::default(); let blind_sig = BlindSignature(token_response.blind_sig.to_vec()); - let signature = self - .public_key + let public_key = token_state.public_key.as_ref().unwrap_or(&self.public_key); + let signature = public_key .finalize( &blind_sig, &token_state.blinding_result.secret, None, - token_input, + &token_state.prepared_input, &options, ) .map_err(|_| IssueTokenError::InvalidTokenResponse)?; + + debug_assert!(signature + .verify(public_key, None, &token_state.prepared_input, &options,) + .is_ok()); + let authenticator: GenericArray = GenericArray::clone_from_slice(&signature[0..256]); Ok(Token::new( - TokenType::Public, + token_state.token_input.token_type, token_state.token_input.nonce, token_state.challenge_digest, token_state.token_input.key_id, diff --git a/src/public_tokens/mod.rs b/src/public_tokens/mod.rs index efffc69..4fa8b27 100644 --- a/src/public_tokens/mod.rs +++ b/src/public_tokens/mod.rs @@ -1,5 +1,7 @@ //! # Publicly Verifiable Tokens +use blind_rsa_signatures::reexports::rsa::{BigUint, RsaPrivateKey, RsaPublicKey}; +use blind_rsa_signatures::SecretKey; use sha2::{Digest, Sha256}; use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize}; use typenum::U64; @@ -60,3 +62,129 @@ pub struct TokenRequest { pub struct TokenResponse { blind_sig: [u8; NK], } + +fn augment_public_key(public_key: &PublicKey, metadata: &[u8]) -> PublicKey { + use blind_rsa_signatures::reexports::rsa::PublicKeyParts; + + // expandLen = ceil((ceil(log2(\lambda)) + k) / 8), where k is the security parameter of the suite (e.g., k = 128). + // We stretch the input metadata beyond \lambda bits s.t. the output bytes are indifferentiable from truly random bytes + let lambda = public_key.0.n().bits() / 2; + let expand_len = (lambda + 128) / 8; + let hkdf_salt = public_key.0.n().to_bytes_be(); + let mut hkdf_input = b"key".to_vec(); + hkdf_input.extend_from_slice(metadata); + hkdf_input.push(0); + + let hkdf = hkdf::Hkdf::::new(Some(&hkdf_salt), &hkdf_input); + let mut bytes = vec![0u8; expand_len]; + hkdf.expand(b"PBRSA", &mut bytes).unwrap(); + + // H_MD(D) = 1 || G(x), where G(x) is output of length \lambda-2 bits + // We do this by sampling \lambda bits, clearing the top two bits (so the output is \lambda-2 bits) + // and setting the bottom bit (so the result is odd). + bytes[0] &= 0b00111111; + bytes[(lambda / 8) - 1] |= 1; + let hmd = BigUint::from_bytes_be(&bytes[..lambda / 8]); + + // The public exponent will be significantly larger than a typical RSA + // public key as a result of this augmentation. + PublicKey(RsaPublicKey::new_unchecked(public_key.0.n().clone(), hmd)) +} + +// augmentPrivateKey tweaks the private key using the metadata as input. +// +// See the specification for more details: +// https://datatracker.ietf.org/doc/html/draft-amjad-cfrg-partially-blind-rsa-00#name-private-key-augmentation +fn augment_private_key(secret_key: &SecretKey, metadata: &[u8]) -> SecretKey { + use blind_rsa_signatures::reexports::rsa::PublicKeyParts; + use num_bigint_dig::traits::ModInverse; + + let [p, q, ..] = secret_key.primes() else { + panic!("too few primes") + }; + let one = BigUint::from_slice_native(&[1]); + // pih(N) = (p-1)(q-1) + let pm1 = p - &one; + let qm1 = q - one; + let phi = pm1 * qm1; + + // d = e^-1 mod phi(N) + let pk = augment_public_key(&PublicKey(secret_key.to_public_key()), metadata); + let big_e = pk.e() % φ + let d = big_e.mod_inverse(phi).unwrap().to_biguint().unwrap(); + SecretKey( + RsaPrivateKey::from_components( + pk.n().clone(), + pk.e().clone(), + d, + secret_key.primes().to_vec(), + ) + .unwrap(), + ) +} + +fn encode_message_metadata(message: &[u8], metadata: &[u8]) -> Vec { + [ + b"msg", + &(metadata.len() as u32).to_be_bytes()[..], + metadata, + message, + ] + .iter() + .flat_map(|b| b.into_iter().copied()) + .collect() +} + +/// Token issuance protocol to be used by servers and clients. +#[derive(Clone, Debug)] +pub enum TokenProtocol<'a> { + /// Privacy Pass issuance protocol + Basic, + /// Privacy Pass issuance with Public Metadata + PublicMetadata { + /// A reference to the public metadata, cryptographically bound to + /// the generated token. + metadata: &'a [u8], + }, +} + +impl<'a> Default for TokenProtocol<'a> { + fn default() -> Self { + TokenProtocol::Basic + } +} + +impl<'a> TokenProtocol<'a> { + /// Returns the token type assigned to this variant of the protocol. + pub fn token_type(&self) -> TokenType { + match self { + TokenProtocol::Basic => TokenType::Public, + TokenProtocol::PublicMetadata { .. } => TokenType::PublicMetadata, + } + } + + /// Augments the issuer's public key if required for the protocol. + /// Returns None if the issuer's public key should be used verbatim. + fn augment_public_key(&self, pk: &PublicKey) -> Option { + match self { + TokenProtocol::Basic => None, + TokenProtocol::PublicMetadata { metadata } => Some(augment_public_key(pk, metadata)), + } + } + + fn augment_private_key(&self, sk: SecretKey) -> SecretKey { + match self { + TokenProtocol::Basic => sk, + TokenProtocol::PublicMetadata { metadata } => augment_private_key(&sk, metadata), + } + } + + fn prepare_message(&self, input_message: Vec) -> Vec { + match self { + TokenProtocol::Basic => input_message, + TokenProtocol::PublicMetadata { metadata } => { + encode_message_metadata(&input_message, metadata) + } + } + } +} diff --git a/src/public_tokens/server.rs b/src/public_tokens/server.rs index 6cf90d0..9f908f8 100644 --- a/src/public_tokens/server.rs +++ b/src/public_tokens/server.rs @@ -6,9 +6,11 @@ use generic_array::ArrayLength; use rand::{rngs::OsRng, CryptoRng, RngCore}; use thiserror::Error; -use crate::{auth::authorize::Token, NonceStore, TokenInput, TokenKeyId, TokenType}; +use crate::{auth::authorize::Token, NonceStore, TokenInput, TokenKeyId}; -use super::{key_id_to_token_key_id, public_key_to_key_id, TokenRequest, TokenResponse, NK}; +use super::{ + key_id_to_token_key_id, public_key_to_key_id, TokenProtocol, TokenRequest, TokenResponse, NK, +}; /// Errors that can occur when creating a keypair. #[derive(Error, Debug, PartialEq, Eq)] @@ -104,17 +106,18 @@ impl IssuerServer { Ok(key_pair) } - /// Issues a new token response. + /// Issues a new token response using the specified Privacy Pass issuance protocol. /// /// # Errors /// Returns an error if the token request is invalid. - pub async fn issue_token_response( + pub async fn issue_token_response_protocol( &self, key_store: &IKS, token_request: TokenRequest, + protocol: TokenProtocol<'_>, ) -> Result { let rng = &mut OsRng; - if token_request.token_type != TokenType::Public { + if token_request.token_type != protocol.token_type() { return Err(IssueTokenResponseError::InvalidTokenType); } let key_pair = key_store @@ -124,8 +127,8 @@ impl IssuerServer { // blind_sig = rsabssa_blind_sign(skI, TokenRequest.blinded_msg) let options = Options::default(); - let blind_signature = key_pair - .sk + let secret_key = protocol.augment_private_key(key_pair.sk); + let blind_signature = secret_key .blind_sign(rng, token_request.blinded_msg, &options) .map_err(|_| IssueTokenResponseError::InvalidTokenRequest)?; @@ -136,6 +139,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. #[cfg(feature = "kat")] pub async fn set_keypair(&self, key_store: &IKS, key_pair: KeyPair) { @@ -155,17 +171,18 @@ impl OriginServer { Self {} } - /// Redeems a token. + /// Redeems a token using the specified Privacy Pass issuance protocol. /// /// # Errors /// Returns an error if the token is invalid. - pub async fn redeem_token>( + pub async fn redeem_token_protocol>( &self, key_store: &OKS, nonce_store: &NS, token: Token, + protocol: TokenProtocol<'_>, ) -> Result<(), RedeemTokenError> { - if token.token_type() != TokenType::Public { + if token.token_type() != protocol.token_type() { return Err(RedeemTokenError::InvalidToken); } if token.authenticator().len() != KEYSIZE_IN_BYTES { @@ -186,13 +203,32 @@ impl OriginServer { .await .ok_or(RedeemTokenError::KeyIdNotFound)?; + let public_key = protocol + .augment_public_key(&public_key) + .unwrap_or(public_key); + let options = Options::default(); let signature = Signature(token.authenticator().to_vec()); + let input_message = protocol.prepare_message(token_input.serialize()); signature - .verify(&public_key, None, token_input.serialize(), &options) + .verify(&public_key, None, input_message, &options) .map_err(|_| RedeemTokenError::InvalidToken)?; nonce_store.insert(token.nonce()).await; Ok(()) } + + /// 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 5700c2a..47ca6c7 100644 --- a/tests/public_tokens.rs +++ b/tests/public_tokens.rs @@ -2,12 +2,16 @@ mod public_memory_stores; use public_memory_stores::*; +use blind_rsa_signatures::reexports::rsa::RsaPrivateKey; +use blind_rsa_signatures::{KeyPair, PublicKey, SecretKey}; +use num_bigint_dig::BigUint; use privacypass::{ auth::authenticate::TokenChallenge, - public_tokens::{client::*, public_key_to_token_key_id, server::*}, - TokenType, + public_tokens::{client::*, public_key_to_token_key_id, server::*, TokenProtocol}, + KeyId, TokenType, }; use rand::thread_rng; +use sha2::{Digest, Sha256}; #[tokio::test] async fn public_tokens_cycle() { @@ -75,3 +79,108 @@ async fn public_tokens_cycle() { Err(RedeemTokenError::DoubleSpending) ); } + +fn hex_to_biguint(h: &[u8]) -> BigUint { + BigUint::from_bytes_be( + &h.chunks(2) + .map(|x| u8::from_str_radix(std::str::from_utf8(x).unwrap(), 16).unwrap()) + .collect::>(), + ) +} + +#[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( + RsaPrivateKey::from_components( + hex_to_biguint(N_ENC), + hex_to_biguint(E_ENC), + hex_to_biguint(D_ENC), + vec![hex_to_biguint(P_ENC), hex_to_biguint(Q_ENC)], + ) + .unwrap(), + ); + let public_key = PublicKey(sk.0.to_public_key()); + + let rng = &mut thread_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 public_key_enc = serialize_public_key(&key_pair.pk); + let key_id: KeyId = Sha256::digest(public_key_enc).into(); + let token_key_id = *key_id.iter().last().unwrap_or(&0); + issuer_key_store + .insert(token_key_id, key_pair.clone()) + .await; + + origin_key_store + .insert(public_key_to_token_key_id(&public_key), public_key.clone()) + .await; + + // Client: Create client + let mut client = Client::new(public_key); + + // 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) = client + .issue_token_request_protocol(rng, 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 = client.issue_token(token_response, &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 22f2c003b4e0cb37de03704e3e07e18bba6b608a Mon Sep 17 00:00:00 2001 From: Fisher Darling Date: Tue, 19 Dec 2023 18:33:27 +0100 Subject: [PATCH 3/3] Update to the newest revision and allow for some quirks * delimit kvs with a ` ` rather than `,`. * values can be optionally surrounded with quotes * quirk: allow for URL_SAFE and URL_SAFE_NO_PAD encoding for extension values --- src/auth/authorize.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/auth/authorize.rs b/src/auth/authorize.rs index 42eeca0..ad4a489 100644 --- a/src/auth/authorize.rs +++ b/src/auth/authorize.rs @@ -1,9 +1,12 @@ //! This module contains the authorization logic for redemption phase of the //! protocol. +use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use generic_array::{ArrayLength, GenericArray}; use http::{header::HeaderName, HeaderValue}; +use nom::branch::alt; +use nom::sequence::delimited; use nom::{ bytes::complete::{tag, tag_no_case}, multi::{many1, separated_list1}, @@ -150,7 +153,7 @@ pub fn build_authorization_header>( /// Builds a `Authorize` header according to the following scheme: /// -/// `PrivateToken token=...,extensions=...` +/// `PrivateToken token=... extensions=...` /// /// # Errors /// Returns an error if the token is not valid. @@ -159,7 +162,7 @@ pub fn build_authorization_header_ext>( extensions: &[u8], ) -> Result<(HeaderName, HeaderValue), BuildError> { let value = format!( - "PrivateToken token={},extensions={}", + "PrivateToken token={} extensions={}", URL_SAFE.encode( token .tls_serialize_detached() @@ -227,7 +230,10 @@ fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> { let (input, _) = tag("=")(input)?; let (input, _) = opt_spaces(input)?; let (input, value) = match key.to_lowercase().as_str() { - "token" | "extensions" => base64_char(input)?, + "token" | "extensions" => { + // Values may or may not be delimited with quotes. + alt((delimited(tag("\""), base64_char, tag("\"")), base64_char))(input)? + } _ => { return Err(nom::Err::Failure(nom::error::make_error( input, @@ -242,7 +248,7 @@ fn parse_private_token(input: &str) -> IResult<&str, (&str, Option<&str>)> { let (input, _) = opt_spaces(input)?; let (input, _) = tag_no_case("PrivateToken")(input)?; let (input, _) = many1(space)(input)?; - let (input, key_values) = separated_list1(tag(","), parse_key_value)(input)?; + let (input, key_values) = separated_list1(tag(" "), parse_key_value)(input)?; let mut token = None; let mut extensions = None; @@ -284,7 +290,12 @@ fn parse_header_value>( let tokens = tokens .into_iter() .map(|(token_value, extensions_value)| { - let ext = extensions_value.and_then(|x| URL_SAFE.decode(x).ok()); + let ext = extensions_value.and_then(|x| { + URL_SAFE_NO_PAD + .decode(x) + .or_else(|_| URL_SAFE.decode(x)) + .ok() + }); Ok(( Token::tls_deserialize( &mut URL_SAFE