use base64::Engine;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum JadesError {
EmptyCertificateChain,
EmptyThumbprint,
}
impl std::fmt::Display for JadesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyCertificateChain => write!(
f,
"x5c was supplied with no certificates, leaving the signature without the certificate reference TS 119 182-1 clause 5.1.7 requires"
),
Self::EmptyThumbprint => write!(
f,
"x5t#S256 was supplied as an empty string, so the signature carries \
the parameter without the signing-certificate reference TS 119 182-1 \
Table 1 gives cardinality 1"
),
}
}
}
impl std::error::Error for JadesError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CertificateRef {
Thumbprint(String),
Chain(Vec<String>),
ChainWithThumbprint {
chain: Vec<String>,
thumbprint: String,
},
}
impl CertificateRef {
#[must_use]
pub fn is_eu_recognised_profile(&self) -> bool {
match self {
Self::ChainWithThumbprint { chain, thumbprint } => {
!chain.is_empty() && !thumbprint.is_empty()
}
_ => false,
}
}
#[must_use]
pub fn thumbprint_of_der(der: &[u8]) -> Self {
let digest = Sha256::digest(der);
Self::Thumbprint(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest))
}
pub fn chain_of_der(chain: &[Vec<u8>]) -> Result<Self, JadesError> {
let signing = chain.first().ok_or(JadesError::EmptyCertificateChain)?;
let Self::Thumbprint(thumbprint) = Self::thumbprint_of_der(signing) else {
unreachable!("thumbprint_of_der always yields a Thumbprint");
};
Ok(Self::ChainWithThumbprint {
chain: chain
.iter()
.map(|der| base64::engine::general_purpose::STANDARD.encode(der))
.collect(),
thumbprint,
})
}
fn insert_into(&self, map: &mut Map<String, Value>) -> Result<(), JadesError> {
let chain_value =
|chain: &Vec<String>| Value::Array(chain.iter().cloned().map(Value::String).collect());
match self {
Self::Thumbprint(t) => {
map.insert("x5t#S256".to_owned(), Value::String(t.clone()));
Ok(())
}
Self::Chain(chain) | Self::ChainWithThumbprint { chain, .. } if chain.is_empty() => {
Err(JadesError::EmptyCertificateChain)
}
Self::ChainWithThumbprint { thumbprint, .. } if thumbprint.is_empty() => {
Err(JadesError::EmptyThumbprint)
}
Self::Chain(chain) => {
map.insert("x5c".to_owned(), chain_value(chain));
Ok(())
}
Self::ChainWithThumbprint { chain, thumbprint } => {
map.insert("x5c".to_owned(), chain_value(chain));
map.insert("x5t#S256".to_owned(), Value::String(thumbprint.clone()));
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JadesHeader {
pub alg: String,
pub iat: i64,
pub certificate: CertificateRef,
pub content_type: Option<String>,
}
impl JadesHeader {
#[must_use]
pub fn now(alg: impl Into<String>, certificate: CertificateRef) -> Self {
Self {
alg: alg.into(),
iat: chrono::Utc::now().timestamp(),
certificate,
content_type: None,
}
}
#[must_use]
pub fn with_content_type(mut self, cty: impl Into<String>) -> Self {
self.content_type = Some(cty.into());
self
}
pub fn to_json_bytes(&self) -> Result<Vec<u8>, JadesError> {
let mut map = Map::new();
map.insert("alg".to_owned(), Value::String(self.alg.clone()));
if let Some(cty) = &self.content_type {
map.insert("cty".to_owned(), Value::String(cty.clone()));
}
self.certificate.insert_into(&mut map)?;
map.insert("iat".to_owned(), Value::Number(self.iat.into()));
Ok(serde_json::to_vec(&Value::Object(map)).expect("header is plain JSON"))
}
}