use bh_jws_utils::{
jwt, JwkPublic, JwtSigner, JwtVerifier as _, SignatureVerifier, SigningAlgorithm,
};
use bherror::{
traits::{ForeignBoxed, ForeignError, PropagateError},
Error,
};
use jwt::claims::SecondsSinceEpoch;
use serde::{Deserialize, Serialize};
use crate::{
holder::{HolderError, Result as HolderResult},
sd_jwt::{SdJwt, SdJwtKB, SD_JWT_DELIMITER},
utils,
verifier::{Result as VerifierResult, VerifierError},
Hasher, Result,
};
#[derive(strum_macros::Display, PartialEq, Debug, Clone)]
pub enum KBError {
#[strum(to_string = "Missing key binding")]
MissingKeyBinding,
#[strum(to_string = "Invalid KBJwt syntax: {0}")]
InvalidKBJwtSyntax(String),
#[strum(to_string = "Invalid KBJwt signature")]
InvalidKBJwtSignature,
#[strum(to_string = "Invalid KBJwt type {0}")]
InvalidKBJwtType(String),
#[strum(to_string = "KBJwt expired: iat is {0}, expiration offset {1} and current time {2}")]
KBJwtExpired(u64, u64, u64),
#[strum(to_string = "Invalid KBJwt nonce. Provided nonce was {0}")]
InvalidKBJwtNonce(String),
#[strum(to_string = "Invalid KBJwt aud. Provided aud was `{0}`; expected `{1}`")]
InvalidKBJwtAud(String, String),
#[strum(to_string = "Invalid KBJwt hash. Claims hash was {0}, provided was {1}")]
InvalidKBJwtSdHash(String, String),
#[strum(to_string = "Missing signature verifier: {0}")]
MissingSignatureVerifier(SigningAlgorithm),
}
impl bherror::BhError for KBError {}
pub(crate) const KB_JWT_HEADER_TYP: &str = "kb+jwt";
pub(crate) const KB_JWT_EXPIRATION_OFFSET: SecondsSinceEpoch = 5 * 60;
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub(crate) struct KBJwtHeader {
pub(crate) typ: String,
pub(crate) alg: SigningAlgorithm,
}
impl KBJwtHeader {
pub(crate) fn new(alg: SigningAlgorithm) -> Self {
Self {
typ: KB_JWT_HEADER_TYP.to_owned(),
alg,
}
}
}
impl jwt::JoseHeader for KBJwtHeader {
fn algorithm_type(&self) -> jwt::AlgorithmType {
self.alg.into()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub(crate) struct KBJwtClaims {
pub(crate) iat: SecondsSinceEpoch,
pub(crate) aud: String,
pub(crate) nonce: String,
pub(crate) sd_hash: String,
}
impl KBJwtClaims {
pub(crate) fn new(
challenge: KeyBindingChallenge,
current_time: SecondsSinceEpoch,
sd_hash: String,
) -> Self {
Self {
iat: current_time,
aud: challenge.aud,
nonce: challenge.nonce,
sd_hash,
}
}
}
#[derive(Debug, Clone)]
pub struct KeyBindingChallenge {
pub aud: String,
pub nonce: String,
}
pub(crate) struct KBJwt<Status>(jwt::Token<KBJwtHeader, KBJwtClaims, Status>);
type KBJwtUnverified<'a> = jwt::Token<KBJwtHeader, KBJwtClaims, jwt::Unverified<'a>>;
impl<Status> KBJwt<Status> {
pub(crate) fn header(&self) -> &KBJwtHeader {
self.0.header()
}
pub(crate) fn claims(&self) -> &KBJwtClaims {
self.0.claims()
}
}
impl KBJwt<jwt::token::Signed> {
pub(crate) fn new(claims: KBJwtClaims, signer: &impl JwtSigner) -> HolderResult<Self> {
let header = KBJwtHeader::new(signer.algorithm());
let token_unsigned = jwt::Token::new(header, claims);
let token_signed = signer
.sign_jwt(token_unsigned)
.foreign_boxed_err(|| HolderError::KBJwtSigningFailed)?;
Ok(KBJwt(token_signed))
}
pub(crate) fn into_string(self) -> String {
self.0.into()
}
}
impl KBJwt<jwt::token::Verified> {
pub(crate) fn validate<'a>(
kb_jwt: &str,
holder_public_key: &JwkPublic,
get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
challenge: &KeyBindingChallenge,
current_time: SecondsSinceEpoch,
sd_hash: &str,
) -> Result<Self, KBError> {
let (token_unverified, verifier) =
Self::get_signature_verifier(kb_jwt, get_signature_verifier)?;
let token_verified = verifier
.verify_jwt_signature(token_unverified, holder_public_key)
.foreign_boxed_err(|| KBError::InvalidKBJwtSignature)?;
let jwt = Self(token_verified);
jwt.validate_header()?;
jwt.validate_claims(challenge, current_time, sd_hash)?;
Ok(jwt)
}
fn get_signature_verifier<'a>(
kb_jwt: &str,
get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
) -> Result<(KBJwtUnverified<'_>, &'a dyn SignatureVerifier), KBError> {
let token_unverified: KBJwtUnverified = jwt::Token::parse_unverified(kb_jwt)
.foreign_err(|| KBError::InvalidKBJwtSyntax(kb_jwt.to_string()))?;
let signing_algorithm = token_unverified.header().alg;
let verifier = get_signature_verifier(signing_algorithm)
.ok_or_else(|| Error::root(KBError::MissingSignatureVerifier(signing_algorithm)))?;
Ok((token_unverified, verifier))
}
fn validate_header(&self) -> Result<(), KBError> {
let header = self.header();
if header.typ != KB_JWT_HEADER_TYP {
return Err(Error::root(KBError::InvalidKBJwtType(header.typ.clone())));
}
Ok(())
}
fn validate_claims(
&self,
challenge: &KeyBindingChallenge,
current_time: SecondsSinceEpoch,
sd_hash: &str,
) -> Result<(), KBError> {
let claims = self.claims();
if claims.iat + KB_JWT_EXPIRATION_OFFSET < current_time {
return Err(Error::root(KBError::KBJwtExpired(
claims.iat,
KB_JWT_EXPIRATION_OFFSET,
current_time,
)));
}
if claims.nonce != challenge.nonce {
return Err(Error::root(KBError::InvalidKBJwtNonce(
claims.nonce.clone(),
)));
}
if claims.aud != challenge.aud {
return Err(Error::root(KBError::InvalidKBJwtAud(
claims.aud.clone(),
challenge.aud.clone(),
)));
}
if claims.sd_hash != sd_hash {
return Err(Error::root(KBError::InvalidKBJwtSdHash(
claims.sd_hash.clone(),
sd_hash.to_string(),
)));
}
Ok(())
}
}
impl SdJwt {
pub(crate) fn calc_key_binding_jwt(
&self,
hasher: impl Hasher,
challenge: KeyBindingChallenge,
current_time: SecondsSinceEpoch,
signer: &impl JwtSigner,
) -> HolderResult<String> {
let sd_hash = sd_hash(self, hasher);
let claims = KBJwtClaims::new(challenge, current_time, sd_hash);
let signed_kb_jwt = KBJwt::<jwt::token::Signed>::new(claims, signer)?;
Ok(signed_kb_jwt.into_string())
}
pub(crate) fn add_key_binding_jwt(
self,
hasher: impl Hasher,
challenge: KeyBindingChallenge,
current_time: SecondsSinceEpoch,
signer: &impl JwtSigner,
) -> HolderResult<SdJwtKB> {
let key_binding_jwt = self.calc_key_binding_jwt(hasher, challenge, current_time, signer)?;
Ok(SdJwtKB {
sd_jwt: self,
key_binding_jwt,
})
}
}
impl SdJwtKB {
pub(crate) fn verify_key_binding_jwt<'a>(
&self,
hasher: impl Hasher,
holder_public_key: &JwkPublic,
challenge: &KeyBindingChallenge,
current_time: SecondsSinceEpoch,
get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
) -> VerifierResult<KBJwt<jwt::token::Verified>> {
let kb_jwt = &self.key_binding_jwt;
let sd_hash = sd_hash(&self.sd_jwt, hasher);
let verified = KBJwt::<jwt::token::Verified>::validate(
kb_jwt,
holder_public_key,
get_signature_verifier,
challenge,
current_time,
&sd_hash,
)
.match_err(|kb_error| VerifierError::KeyBinding(kb_error.clone()))?;
Ok(verified)
}
}
fn sd_hash_payload(sd_jwt: &SdJwt) -> String {
let mut payload = String::new();
payload += &sd_jwt.jwt;
payload += SD_JWT_DELIMITER;
for disclosure in &sd_jwt.disclosures {
payload += disclosure;
payload += SD_JWT_DELIMITER;
}
payload
}
fn sd_hash(sd_jwt: &SdJwt, hasher: impl Hasher) -> String {
utils::base64_url_digest(sd_hash_payload(sd_jwt).as_bytes(), hasher)
}