use core::fmt;
use indexmap::IndexMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{Jwk, JwsAlgorithm, JwsError, JwsVerifier, SignError, VerificationError, base64url};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct JwsHeader {
pub alg: JwsAlgorithm,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub typ: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cty: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jwk: Option<Jwk>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crit: Option<Vec<String>>,
#[serde(flatten)]
pub additional: IndexMap<String, Value>,
}
impl JwsHeader {
pub fn new(alg: JwsAlgorithm) -> Self {
Self {
alg,
typ: None,
cty: None,
kid: None,
jwk: None,
crit: None,
additional: IndexMap::new(),
}
}
#[must_use]
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
#[must_use]
pub fn with_typ(mut self, typ: impl Into<String>) -> Self {
self.typ = Some(typ.into());
self
}
}
#[derive(Debug, Clone)]
pub struct CompactJws {
token: String,
signing_input_end: usize,
header: JwsHeader,
payload: Vec<u8>,
signature: Vec<u8>,
}
impl CompactJws {
pub fn decode(token: &str) -> Result<Self, JwsError> {
let mut parts = token.split('.');
let (Some(header_b64), Some(payload_b64), Some(signature_b64), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(JwsError::InvalidFormat {
parts: token.split('.').count(),
});
};
let header_json = base64url::decode(header_b64)
.map_err(|_| JwsError::InvalidBase64Url { part: "header" })?;
let header = parse_header(&header_json)?;
let payload = base64url::decode(payload_b64)
.map_err(|_| JwsError::InvalidBase64Url { part: "payload" })?;
let signature = base64url::decode(signature_b64)
.map_err(|_| JwsError::InvalidBase64Url { part: "signature" })?;
Ok(Self {
token: token.to_owned(),
signing_input_end: header_b64.len() + 1 + payload_b64.len(),
header,
payload,
signature,
})
}
pub fn header(&self) -> &JwsHeader {
&self.header
}
pub fn alg(&self) -> JwsAlgorithm {
self.header.alg
}
pub fn kid(&self) -> Option<&str> {
self.header.kid.as_deref()
}
pub fn signing_input(&self) -> &[u8] {
&self.token.as_bytes()[..self.signing_input_end]
}
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn signature(&self) -> &[u8] {
&self.signature
}
pub fn claims<T: DeserializeOwned>(&self) -> Result<T, JwsError> {
serde_json::from_slice(&self.payload).map_err(|e| JwsError::InvalidPayload(e.to_string()))
}
pub fn as_str(&self) -> &str {
&self.token
}
pub fn verify<V: JwsVerifier + ?Sized>(
&self,
verifier: &V,
key: &Jwk,
) -> Result<(), VerificationError> {
verifier.verify(self.alg(), self.signing_input(), &self.signature, key)
}
}
impl fmt::Display for CompactJws {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.token)
}
}
impl AsRef<str> for CompactJws {
fn as_ref(&self) -> &str {
&self.token
}
}
fn parse_header(json: &[u8]) -> Result<JwsHeader, JwsError> {
let value: Value =
serde_json::from_slice(json).map_err(|e| JwsError::InvalidHeader(e.to_string()))?;
let Some(object) = value.as_object() else {
return Err(JwsError::InvalidHeader("not a JSON object".to_owned()));
};
match object.get("alg") {
Some(Value::String(alg)) if alg.parse::<JwsAlgorithm>().is_ok() => {}
Some(Value::String(alg)) => return Err(JwsError::UnsupportedAlgorithm(alg.clone())),
Some(other) => return Err(JwsError::UnsupportedAlgorithm(other.to_string())),
None => return Err(JwsError::InvalidHeader("missing `alg`".to_owned())),
}
if object
.get("b64")
.is_some_and(|b64| b64 != &Value::Bool(true))
{
return Err(JwsError::UnencodedPayload);
}
let header: JwsHeader =
serde_json::from_value(value).map_err(|e| JwsError::InvalidHeader(e.to_string()))?;
if let Some(crit) = &header.crit {
return Err(JwsError::UnsupportedCritical(crit.clone()));
}
Ok(header)
}
#[async_trait::async_trait]
pub trait Signer: Send + Sync {
fn alg(&self) -> JwsAlgorithm;
fn kid(&self) -> Option<&str>;
async fn sign(&self, signing_input: &[u8]) -> Result<Vec<u8>, SignError>;
}
pub async fn encode_compact(
header: &JwsHeader,
payload: &[u8],
signer: &dyn Signer,
) -> Result<String, JwsError> {
if header.alg != signer.alg() {
return Err(JwsError::AlgorithmMismatch {
header: header.alg,
signer: signer.alg(),
});
}
let header_json =
serde_json::to_vec(header).map_err(|e| JwsError::InvalidHeader(e.to_string()))?;
let mut token = base64url::encode(&header_json);
token.push('.');
token.push_str(&base64url::encode(payload));
let signature = signer
.sign(token.as_bytes())
.await
.map_err(JwsError::Signer)?;
token.push('.');
token.push_str(&base64url::encode(&signature));
Ok(token)
}