use crate::jwt::Jwt;
use crate::Error;
use crate::Hasher;
use crate::JsonObject;
use crate::JwsSigner;
use crate::SdJwt;
use crate::SHA_ALG_NAME;
use anyhow::Context as _;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::fmt::Display;
use std::ops::Deref;
use std::str::FromStr;
pub const KB_JWT_HEADER_TYP: &str = "kb+jwt";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyBindingJwt(Jwt<KeyBindingJwtClaims>);
impl Display for KeyBindingJwt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl FromStr for KeyBindingJwt {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let jwt = Jwt::<KeyBindingJwtClaims>::from_str(s)?;
let valid_jwt_type = jwt.header.get("typ").is_some_and(|typ| typ == KB_JWT_HEADER_TYP);
if !valid_jwt_type {
return Err(Error::DeserializationError(format!(
"invalid KB-JWT: typ must be \"{KB_JWT_HEADER_TYP}\""
)));
}
let valid_alg = jwt.header.get("alg").is_some_and(|alg| alg != "none");
if !valid_alg {
return Err(Error::DeserializationError(
"invalid KB-JWT: alg must be set and cannot be \"none\"".to_string(),
));
}
Ok(Self(jwt))
}
}
impl KeyBindingJwt {
pub fn builder() -> KeyBindingJwtBuilder {
KeyBindingJwtBuilder::default()
}
pub fn claims(&self) -> &KeyBindingJwtClaims {
&self.0.claims
}
}
#[derive(Debug, Default, Clone)]
pub struct KeyBindingJwtBuilder {
header: JsonObject,
payload: JsonObject,
}
impl KeyBindingJwtBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn from_object(object: JsonObject) -> Self {
Self {
header: JsonObject::default(),
payload: object,
}
}
pub fn header(mut self, header: JsonObject) -> Self {
self.header = header;
self
}
pub fn iat(mut self, iat: i64) -> Self {
self.payload.insert("iat".to_string(), iat.into());
self
}
pub fn aud<'a, S>(mut self, aud: S) -> Self
where
S: Into<Cow<'a, str>>,
{
self.payload.insert("aud".to_string(), aud.into().into_owned().into());
self
}
pub fn nonce<'a, S>(mut self, nonce: S) -> Self
where
S: Into<Cow<'a, str>>,
{
self
.payload
.insert("nonce".to_string(), nonce.into().into_owned().into());
self
}
pub fn insert_property(mut self, name: &str, value: Value) -> Self {
self.payload.insert(name.to_string(), value);
self
}
pub async fn finish<S>(
self,
sd_jwt: &SdJwt,
hasher: &dyn Hasher,
alg: &str,
signer: &S,
) -> Result<KeyBindingJwt, Error>
where
S: JwsSigner,
{
let mut claims = self.payload;
if alg == "none" {
return Err(Error::DataTypeMismatch(
"A KeyBindingJwt cannot use algorithm \"none\"".to_string(),
));
}
if sd_jwt.key_binding_jwt().is_some() {
return Err(Error::DataTypeMismatch(
"the provided SD-JWT already has a KB-JWT attached".to_string(),
));
}
if sd_jwt.claims()._sd_alg.as_deref().unwrap_or(SHA_ALG_NAME) != hasher.alg_name() {
return Err(Error::InvalidHasher(format!(
"invalid hashing algorithm \"{}\"",
hasher.alg_name()
)));
}
let sd_hash = hasher.encoded_digest(&sd_jwt.to_string());
claims.insert("sd_hash".to_string(), sd_hash.into());
let mut header = self.header;
header.insert("alg".to_string(), alg.to_owned().into());
header
.entry("typ")
.or_insert_with(|| KB_JWT_HEADER_TYP.to_owned().into());
let parsed_claims = serde_json::from_value::<KeyBindingJwtClaims>(claims.clone().into())
.map_err(|e| Error::DeserializationError(format!("invalid KB-JWT claims: {e}")))?;
let jws = signer
.sign(&header, &claims)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
.and_then(|jws_bytes| String::from_utf8(jws_bytes).context("invalid JWS"))
.map_err(|e| Error::JwsSignerFailure(e.to_string()))?;
Ok(KeyBindingJwt(Jwt {
header,
claims: parsed_claims,
jws,
}))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct KeyBindingJwtClaims {
pub iat: i64,
pub aud: String,
pub nonce: String,
pub sd_hash: String,
#[serde(flatten)]
properties: JsonObject,
}
impl Deref for KeyBindingJwtClaims {
type Target = JsonObject;
fn deref(&self) -> &Self::Target {
&self.properties
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum RequiredKeyBinding {
Jwk(JsonObject),
Jwe(String),
Kid(String),
Jwu {
jwu: String,
kid: String,
},
#[serde(untagged)]
Custom(Value),
}