Skip to content

Commit

Permalink
Implement the public metadata issuance protocol
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cbranch committed Dec 4, 2023
1 parent dc41732 commit 52b4970
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 27 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 45 additions & 14 deletions src/public_tokens/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ 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)]
pub struct TokenState {
blinding_result: BlindingResult,
token_input: TokenInput,
challenge_digest: ChallengeDigest,
/// A prepared version of token_input
prepared_input: Vec<u8>,
/// An augmented public key if used by the issuance protocol.
/// If none, the client's public key should be used.
public_key: Option<PublicKey>,
}

/// Errors that can occur when issuing token requests.
Expand Down Expand Up @@ -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<R: RngCore + CryptoRng>(
pub fn issue_token_request_protocol<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
challenge: TokenChallenge,
protocol: TokenProtocol,
) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> {
let mut nonce: Nonce = [0u8; 32];
rng.fill_bytes(&mut nonce);
Expand All @@ -76,20 +85,24 @@ 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);
let mut blinded_msg = [0u8; NK];
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,
};
Expand All @@ -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<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
challenge: TokenChallenge,
) -> Result<(TokenRequest, TokenState), IssueTokenRequestError> {
self.issue_token_request_protocol(rng, challenge, TokenProtocol::Basic)
}

/// Issue a token.
///
/// # Errors
Expand All @@ -112,23 +139,27 @@ impl Client {
token_state: &TokenState,
) -> Result<Token<U256>, 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<u8, U256> =
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,
Expand Down
128 changes: 128 additions & 0 deletions src/public_tokens/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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::<sha2::Sha384>::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() % &phi;
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<u8> {
[
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<PublicKey> {
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<u8>) -> Vec<u8> {
match self {
TokenProtocol::Basic => input_message,
TokenProtocol::PublicMetadata { metadata } => {
encode_message_metadata(&input_message, metadata)
}
}
}
}
Loading

0 comments on commit 52b4970

Please # to comment.