use crate::{
config, constraint_violation_error,
data::{DataError, Statement, ValidationError},
fingerprint_it, MyError,
};
use base64::{
prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD},
Engine,
};
use chrono::Utc;
use josekit::jws::{RS256, RS384, RS512};
use openssl::{asn1::Asn1Time, pkey::PKey, x509::X509};
use serde_json::{Map, Value};
use std::{cmp::Ordering, str};
use tracing::{debug, error, info, warn};
const JWS_ALGOS: [&str; 3] = ["RS256", "RS384", "RS512"];
pub struct Signature(u64);
impl Signature {
pub(crate) fn from(buffer: Vec<u8>) -> Result<Self, MyError> {
let v: Vec<_> = buffer
.iter()
.enumerate()
.filter(|&(_, c)| *c == b'.')
.map(|(n, _)| n)
.collect();
if v.len() != 2 {
constraint_violation_error!("JWS compact signature must contain two dots")
}
let n1 = v[0];
let n2 = v[1];
let header_bytes = BASE64_URL_SAFE_NO_PAD.decode(&buffer[..n1])?;
let header_str = str::from_utf8(&header_bytes)?;
let header: Map<String, Value> = serde_json::from_str(header_str).map_err(|x| {
error!("Failed deserializing header: {}", x);
MyError::Data(DataError::JSON(x))
})?;
let Some(Value::String(alg)) = header.get("alg") else {
constraint_violation_error!("Missing 'alg' in JWS Header")
};
debug!("alg = {}", alg);
if !JWS_ALGOS.contains(&alg.as_str()) {
constraint_violation_error!("Unknown/unsupported ({}) JWS algorithm", alg)
}
let mut jws_signer_public_key_pem: Vec<u8> = vec![];
match header.get("x5c") {
Some(Value::Array(x5c)) => {
if x5c.is_empty() {
constraint_violation_error!("Empty certificate chain")
}
let mut cert_chain: Vec<X509> = vec![];
for (i, cert) in x5c.iter().enumerate() {
let Value::String(b64_der_cert) = cert else {
constraint_violation_error!("Item #{} of 'x5c' is not a JSON string", i)
};
let der = BASE64_STANDARD.decode(b64_der_cert.as_bytes())?;
let x509 = X509::from_der(&der)?;
cert_chain.push(x509);
}
let limit = x5c.len();
if config().jws_strict {
info!("Will validate X.509 certificate chain...");
for (i, cert) in cert_chain.iter().enumerate() {
let now = Asn1Time::from_unix(Utc::now().timestamp())?;
if now < cert.not_before() {
constraint_violation_error!("Certificate #{} is not yet valid", i)
}
if now > cert.not_after() {
constraint_violation_error!("Certificate #{} is no more valid", i)
}
if i + 1 < limit {
let issuer_cert = &cert_chain[i + 1];
let issuer_dn = cert.issuer_name();
let subject_dn = issuer_cert.subject_name();
match issuer_dn.try_cmp(subject_dn) {
Ok(Ordering::Equal) => (),
_ => {
constraint_violation_error!(
"Certificate #{} was not issued by next one in the chain",
i
)
}
}
let issuer_public_key: PKey<_> = issuer_cert.public_key()?;
let verified = cert.verify(&issuer_public_key)?;
if !verified {
constraint_violation_error!(
"Certificate #{} was not signed by next one in the chain",
i
)
}
}
}
} else {
warn!("Skip JWS certificate-chain validation...");
}
jws_signer_public_key_pem = cert_chain[0]
.public_key()?
.rsa()?
.public_key_to_pem_pkcs1()?;
}
_ => warn!("No 'x5c' in JWS Header. Unable to verify JWS Signature"),
}
let payload_bytes = BASE64_URL_SAFE_NO_PAD.decode(&buffer[n1 + 1..n2])?;
let payload_str = str::from_utf8(&payload_bytes)?;
let payload: Statement = serde_json::from_str(payload_str).map_err(|x| {
error!("Failed deserializing payload: {}", x);
MyError::Data(DataError::JSON(x))
})?;
let fingerprint = fingerprint_it(&payload);
if config().jws_strict && !jws_signer_public_key_pem.is_empty() {
info!("Will verify JWS signature with issuer X.509 certificate...");
let rest = &buffer[n2 + 1..];
let mut len = rest.len();
if rest[len - 1] == 0x0A {
len -= 1;
if rest[len - 1] == 0x0D {
len -= 1;
}
}
let signature = BASE64_URL_SAFE_NO_PAD.decode(&rest[..len])?;
let verifier = match alg.as_str() {
"RS256" => RS256.verifier_from_pem(jws_signer_public_key_pem)?,
"RS384" => RS384.verifier_from_pem(jws_signer_public_key_pem)?,
_ => RS512.verifier_from_pem(jws_signer_public_key_pem)?,
};
verifier.verify(&buffer[..n2], &signature)?;
} else {
warn!("Skip JWS signature verification...");
}
Ok(Signature(fingerprint))
}
pub(crate) fn verify(&self, that: &Statement) -> bool {
self.0 == fingerprint_it(that)
}
}
#[cfg(test)]
mod tests {
use super::*;
use josekit::jws::{self, JwsHeader};
use openssl::asn1::Asn1Time;
use std::{borrow::Cow, fs, str};
const C1: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/samples/C1.pem");
const C2: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/samples/C2.pem");
const P2_PRIVATE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/samples/P2_private.pem");
const P2_PUBLIC: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/samples/P2_public.pem");
#[test]
fn test_x509_verification() -> Result<(), MyError> {
let c1_bytes = fs::read(C1).expect("Failed reading C1");
let c1 = X509::from_pem(&c1_bytes).expect("Failed parsing C1");
let c2_bytes = fs::read(C2).expect("Failed reading C2");
let c2 = X509::from_pem(&c2_bytes).expect("Failed parsing C2");
let now = Asn1Time::from_unix(Utc::now().timestamp()).expect("Failed instantiating now");
if now < c1.not_before() {
return Err(MyError::Runtime(Cow::Borrowed("Now is too early")));
}
if now > c1.not_after() {
return Err(MyError::Runtime(Cow::Borrowed("Now is too late")));
}
if now < c2.not_before() {
return Err(MyError::Runtime(Cow::Borrowed("Now is too early")));
}
if now > c2.not_after() {
return Err(MyError::Runtime(Cow::Borrowed("Now is too late")));
}
let c2_issuer = c2.issuer_name();
let c1_subject = c1.subject_name();
match c2_issuer.try_cmp(c1_subject) {
Ok(Ordering::Equal) => (),
Ok(_) => return Err(MyError::Runtime(format!("C2 issuer != C1 subject").into())),
Err(x) => {
return Err(MyError::Runtime(
format!("Failed comparing C2 issuer w/ C1 subject: {}", x).into(),
))
}
}
let c1_public_key: PKey<_> = c1.public_key().expect("Failed extracting C1 public key");
let verified = c2.verify(&c1_public_key).expect("Failed RSA verification");
assert_eq!(verified, true);
Ok(())
}
#[test]
fn test_jws() {
let c1_bytes = fs::read(C1).expect("Failed reading C1");
let c1 = X509::from_pem(&c1_bytes).expect("Failed parsing C1");
let c2_bytes = fs::read(C2).expect("Failed reading C2");
let c2 = X509::from_pem(&c2_bytes).expect("Failed parsing C2");
let mut x5c = vec![];
x5c.push(c2.to_der().expect("Failed converting C2 to DER"));
x5c.push(c1.to_der().expect("Failed converting C1 to DER"));
let mut header = JwsHeader::new();
header.set_algorithm("RS256");
header.set_x509_certificate_chain(&x5c);
let payload = "one if by land, two if by sea.";
let private_key = fs::read_to_string(P2_PRIVATE).expect("Failed reading C2 private key");
let signer = RS256
.signer_from_pem(&private_key)
.expect("Failed making JWS Signer");
let jws_sig = jws::serialize_compact(payload.as_bytes(), &header, &signer)
.expect("Failed generating JWS signature");
let parts = jws_sig.split('.').collect::<Vec<&str>>();
let z_b64 = BASE64_URL_SAFE_NO_PAD
.decode(parts[0])
.expect("Failed decoding header");
let z_utf8 = str::from_utf8(&z_b64).expect("Failed converting header to UTF8");
let z_header: Map<String, Value> =
serde_json::from_str(z_utf8).expect("Failed deserializing header");
assert_eq!(z_header.keys().len(), 2);
assert!(z_header.contains_key("alg"));
let Some(Value::String(alg)) = z_header.get("alg") else {
panic!("Missing 'alg' claim")
};
assert_eq!(alg, "RS256");
assert!(z_header.contains_key("x5c"));
let Some(Value::Array(x5c)) = z_header.get("x5c") else {
panic!("Missing 'x5c' claim")
};
assert_eq!(x5c.len(), 2);
let Value::String(z_b64_der_cert) = &x5c[0] else {
panic!("Missing C2 certificate")
};
let z_cert = X509::from_der(
&BASE64_STANDARD
.decode(z_b64_der_cert.as_bytes())
.expect("Failed base-64 decoding C2 from JWS Header"),
)
.expect("Failed DER decoding C2");
assert_eq!(z_cert, c2);
let msg = BASE64_URL_SAFE_NO_PAD
.decode(parts[1])
.expect("Failed decoding payload");
let msg = str::from_utf8(&msg).expect("Failed converting payload to UTF8");
assert_eq!(msg, payload);
let mut to_sign = String::from("");
to_sign.push_str(parts[0]);
to_sign.push('.');
to_sign.push_str(parts[1]);
let sig_bytes = BASE64_URL_SAFE_NO_PAD
.decode(parts[2])
.expect("Failed decoding last part");
let public_key = fs::read_to_string(P2_PUBLIC).expect("Failed reading C2 public key");
let verifier = RS256
.verifier_from_pem(public_key)
.expect("Failed making JWS verifier (#1)");
verifier
.verify(to_sign.as_bytes(), &sig_bytes)
.expect("Failed verification (#1)");
debug!("Ok (alternative #1)");
let public_key = c2
.public_key()
.expect("Failed extracting public key from C2");
let public_key_pem = public_key
.rsa()
.expect("Failed coercing to RSA")
.public_key_to_pem_pkcs1()
.expect("Failed encoding public key as PEM PKCS1");
let verifier = RS256
.verifier_from_pem(public_key_pem)
.expect("Failed making JWS verifier (#2)");
verifier
.verify(to_sign.as_bytes(), &sig_bytes)
.expect("Failed verification (#2)");
debug!("Ok (alternative #2)");
}
fn build_compact_signature(rsa: &str) -> Result<String, MyError> {
const S: &str = r#"{
"actor":{"mbox":"mailto:example@example.com","objectType":"Agent"},
"verb":{"id":"http://adlnet.gov/expapi/verbs/experienced"},
"object":{"id":"https://www.theirtube.net/watch?v=whatever","objectType":"Activity"}}"#;
let c1_bytes = fs::read(C1)?;
let c1 = X509::from_pem(&c1_bytes)?;
let c2_bytes = fs::read(C2)?;
let c2 = X509::from_pem(&c2_bytes)?;
let mut x5c = vec![];
x5c.push(c2.to_der()?);
x5c.push(c1.to_der()?);
let mut header = JwsHeader::new();
header.set_x509_certificate_chain(&x5c);
let payload = S;
let private_key = fs::read_to_string(P2_PRIVATE)?;
let signer = match rsa {
"RS256" => RS256.signer_from_pem(&private_key)?,
"RS384" => RS384.signer_from_pem(&private_key)?,
"RS512" => RS512.signer_from_pem(&private_key)?,
x => panic!("Unknown/unsupported ({}) JWS signing algorithm", x),
};
Ok(jws::serialize_compact(
payload.as_bytes(),
&header,
&signer,
)?)
}
#[test]
#[should_panic]
fn test_bad_jws_algorithm() {
let compact_ser1 = build_compact_signature("RS256").unwrap();
let Some((jws_sig, rest)) = compact_ser1.split_once('.') else {
panic!("Missing dot")
};
let mut header: Map<String, Value> = serde_json::from_str(
str::from_utf8(&BASE64_URL_SAFE_NO_PAD.decode(jws_sig).unwrap()).unwrap(),
)
.unwrap();
let old_alg = header.insert("alg".to_owned(), Value::String("HS256".to_owned()));
assert_eq!(old_alg, Some(Value::String("RS256".to_owned())));
let mut compact_ser2 =
String::from(BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()));
compact_ser2.push('.');
compact_ser2.push_str(rest);
assert_eq!(compact_ser1, compact_ser2);
let _ = Signature::from(compact_ser2.as_bytes().to_vec()).unwrap();
}
#[test]
fn test_good_jws_algorithms() -> Result<(), MyError> {
for algo in JWS_ALGOS {
let jws_sig = build_compact_signature(algo)?;
let _ = Signature::from(jws_sig.as_bytes().to_vec())?;
}
Ok(())
}
}