use serde::{Deserialize, Serialize};
use crate::keys::{KeriDecodeError, KeriPublicKey};
use crate::state::KeyState;
pub const AUTHS_KERI_BINDING_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 59999, 1, 1];
pub const DID_KERI_SCHEME: &str = "did:keri:";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsCertError {
#[error("decode AID key-state key: {0}")]
Key(#[from] KeriDecodeError),
#[error("generate certificate: {0}")]
Generate(String),
#[error("load TLS keypair: {0}")]
KeyPair(String),
#[error("parse certificate: {0}")]
ParseCert(String),
#[error("certificate carries no auths KEL binding extension")]
MissingBinding,
#[error("malformed auths KEL binding extension: {0}")]
MalformedBinding(String),
#[error("certificate binding does not match the replayed KEL: {0}")]
BindingMismatch(String),
#[error("certificate did:keri SAN mismatch: {0}")]
SanMismatch(String),
#[error("certificate carries no did:keri subjectAltName")]
NoSanIdentity,
#[error("did:keri SAN carries an invalid AID: {0}")]
InvalidSanAid(#[from] crate::types::KeriTypeError),
#[error("certificate carries no AID authorization over its TLS key")]
MissingAuthorization,
#[error("authorization key index {index} is out of range (key-state has {len} current keys)")]
AuthorizationIndexOutOfRange {
index: usize,
len: usize,
},
#[error("decode authorizing key: {0}")]
AuthorizationKey(String),
#[error("AID did not authorize this TLS key: {0}")]
Unauthorized(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TlsKeyAuthorization {
pub key_index: usize,
#[serde(with = "hex::serde")]
pub signature: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthsKeriBinding {
pub aid: String,
pub current_keys: Vec<String>,
pub kel_tip: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls_key_authorization: Option<TlsKeyAuthorization>,
}
impl AuthsKeriBinding {
pub fn from_key_state(state: &KeyState) -> Result<Self, TlsCertError> {
let mut current_keys = Vec::with_capacity(state.current_keys.len());
for key in &state.current_keys {
KeriPublicKey::parse(key.as_str())?;
current_keys.push(key.as_str().to_string());
}
Ok(Self {
aid: state.prefix.as_str().to_string(),
current_keys,
kel_tip: state.last_event_said.as_str().to_string(),
tls_key_authorization: None,
})
}
pub fn with_authorization(mut self, authorization: TlsKeyAuthorization) -> Self {
self.tls_key_authorization = Some(authorization);
self
}
pub fn did_keri(&self) -> String {
format!("{DID_KERI_SCHEME}{}", self.aid)
}
pub fn to_canonical_json(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap_or_default()
}
pub fn from_canonical_json(bytes: &[u8]) -> Result<Self, TlsCertError> {
serde_json::from_slice(bytes).map_err(|e| TlsCertError::MalformedBinding(e.to_string()))
}
}
pub trait TlsKeyAuthorizer {
fn current_key_index(&self) -> usize;
fn sign_tls_key(&self, spki_der: &[u8]) -> Result<Vec<u8>, TlsCertError>;
}
#[cfg(feature = "tls-cert")]
fn check_tls_key_authorization(
authorization: &TlsKeyAuthorization,
current_keys: &[String],
spki_der: &[u8],
) -> Result<(), TlsCertError> {
let key_str = current_keys.get(authorization.key_index).ok_or(
TlsCertError::AuthorizationIndexOutOfRange {
index: authorization.key_index,
len: current_keys.len(),
},
)?;
let key =
KeriPublicKey::parse(key_str).map_err(|e| TlsCertError::AuthorizationKey(e.to_string()))?;
key.verify_signature(spki_der, &authorization.signature)
.map_err(TlsCertError::Unauthorized)
}
#[cfg(feature = "tls-cert")]
mod backend {
use super::*;
use rcgen::string::Ia5String;
use rcgen::{
CertificateParams, CustomExtension, DnType, ExtendedKeyUsagePurpose, KeyPair,
KeyUsagePurpose, PublicKeyData, SanType,
};
pub struct IssuedCert {
pub cert_pem: String,
pub key_pem: zeroize::Zeroizing<String>,
pub binding: AuthsKeriBinding,
}
pub fn issue_kel_rooted_cert(
state: &KeyState,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let binding = AuthsKeriBinding::from_key_state(state)?;
let key_pair =
KeyPair::generate().map_err(|e| TlsCertError::KeyPair(format!("generate: {e}")))?;
issue_with_keypair(&binding, &key_pair, extra_sans)
}
pub fn issue_kel_rooted_cert_with_key(
state: &KeyState,
tls_key_pkcs8_pem: &str,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let binding = AuthsKeriBinding::from_key_state(state)?;
let key_pair = KeyPair::from_pem(tls_key_pkcs8_pem)
.map_err(|e| TlsCertError::KeyPair(format!("from pem: {e}")))?;
issue_with_keypair(&binding, &key_pair, extra_sans)
}
pub fn issue_authorized_kel_rooted_cert(
state: &KeyState,
authorizer: &dyn TlsKeyAuthorizer,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let key_pair =
KeyPair::generate().map_err(|e| TlsCertError::KeyPair(format!("generate: {e}")))?;
issue_authorized_with_keypair(state, authorizer, &key_pair, extra_sans)
}
pub fn issue_authorized_kel_rooted_cert_with_key(
state: &KeyState,
authorizer: &dyn TlsKeyAuthorizer,
tls_key_pkcs8_pem: &str,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let key_pair = KeyPair::from_pem(tls_key_pkcs8_pem)
.map_err(|e| TlsCertError::KeyPair(format!("from pem: {e}")))?;
issue_authorized_with_keypair(state, authorizer, &key_pair, extra_sans)
}
fn issue_authorized_with_keypair(
state: &KeyState,
authorizer: &dyn TlsKeyAuthorizer,
key_pair: &KeyPair,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let base = AuthsKeriBinding::from_key_state(state)?;
let spki_der = key_pair.subject_public_key_info();
let key_index = authorizer.current_key_index();
if base.current_keys.get(key_index).is_none() {
return Err(TlsCertError::AuthorizationIndexOutOfRange {
index: key_index,
len: base.current_keys.len(),
});
}
let signature = authorizer.sign_tls_key(&spki_der)?;
let authorization = TlsKeyAuthorization {
key_index,
signature,
};
check_tls_key_authorization(&authorization, &base.current_keys, &spki_der)?;
let binding = base.with_authorization(authorization);
issue_with_keypair(&binding, key_pair, extra_sans)
}
pub(crate) fn issue_with_keypair(
binding: &AuthsKeriBinding,
key_pair: &KeyPair,
extra_sans: &[String],
) -> Result<IssuedCert, TlsCertError> {
let mut params = CertificateParams::new(Vec::new())
.map_err(|e| TlsCertError::Generate(format!("params: {e}")))?;
params
.distinguished_name
.push(DnType::CommonName, binding.did_keri());
let did_uri = Ia5String::try_from(binding.did_keri())
.map_err(|e| TlsCertError::Generate(format!("did:keri SAN: {e}")))?;
params.subject_alt_names.push(SanType::URI(did_uri));
for san in extra_sans {
params.subject_alt_names.push(san_for(san)?);
}
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];
let content = yasna_octet_string(&binding.to_canonical_json());
let mut ext = CustomExtension::from_oid_content(AUTHS_KERI_BINDING_OID, content);
ext.set_criticality(false);
params.custom_extensions.push(ext);
let cert = params
.self_signed(key_pair)
.map_err(|e| TlsCertError::Generate(format!("self-sign: {e}")))?;
Ok(IssuedCert {
cert_pem: cert.pem(),
key_pem: zeroize::Zeroizing::new(key_pair.serialize_pem()),
binding: binding.clone(),
})
}
fn san_for(host: &str) -> Result<SanType, TlsCertError> {
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
Ok(SanType::IpAddress(ip))
} else {
let dns = Ia5String::try_from(host.to_string())
.map_err(|e| TlsCertError::Generate(format!("DNS SAN {host:?}: {e}")))?;
Ok(SanType::DnsName(dns))
}
}
fn yasna_octet_string(bytes: &[u8]) -> Vec<u8> {
yasna::construct_der(|w| w.write_bytes(bytes))
}
pub fn extract_binding(cert_pem: &str) -> Result<AuthsKeriBinding, TlsCertError> {
use x509_parser::prelude::*;
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
.map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
let (_, cert) = X509Certificate::from_der(&pem.contents)
.map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
let oid_str = oid_string(AUTHS_KERI_BINDING_OID);
for ext in cert.extensions() {
if ext.oid.to_id_string() == oid_str {
let inner = unwrap_octet_string(ext.value)?;
return AuthsKeriBinding::from_canonical_json(&inner);
}
}
Err(TlsCertError::MissingBinding)
}
pub fn extract_did_keri_san(cert_pem: &str) -> Result<Option<String>, TlsCertError> {
use x509_parser::prelude::*;
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
.map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
let (_, cert) = X509Certificate::from_der(&pem.contents)
.map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
if let Ok(Some(san)) = cert.subject_alternative_name() {
for name in &san.value.general_names {
if let GeneralName::URI(uri) = name
&& uri.starts_with(DID_KERI_SCHEME)
{
return Ok(Some((*uri).to_string()));
}
}
}
Ok(None)
}
pub fn extract_aid_from_san(cert_pem: &str) -> Result<crate::types::Prefix, TlsCertError> {
match extract_did_keri_san(cert_pem)? {
Some(uri) => {
let aid = uri.strip_prefix(DID_KERI_SCHEME).ok_or_else(|| {
TlsCertError::SanMismatch(format!("SAN {uri} is not a {DID_KERI_SCHEME} URI"))
})?;
Ok(crate::types::Prefix::new(aid.to_string())?)
}
None => Err(TlsCertError::NoSanIdentity),
}
}
pub fn extract_spki_der(cert_pem: &str) -> Result<Vec<u8>, TlsCertError> {
use x509_parser::prelude::*;
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
.map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
let (_, cert) = X509Certificate::from_der(&pem.contents)
.map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
Ok(cert.public_key().raw.to_vec())
}
fn check_binds_to_key_state(
cert_pem: &str,
embedded: &AuthsKeriBinding,
expected: &AuthsKeriBinding,
) -> Result<(), TlsCertError> {
if embedded.aid != expected.aid {
return Err(TlsCertError::BindingMismatch(format!(
"AID {} in cert != {} from KEL replay",
embedded.aid, expected.aid
)));
}
if embedded.current_keys != expected.current_keys {
return Err(TlsCertError::BindingMismatch(
"current signing keys in cert do not match the KEL replay".to_string(),
));
}
if embedded.kel_tip != expected.kel_tip {
return Err(TlsCertError::BindingMismatch(format!(
"KEL tip {} in cert != {} from replay",
embedded.kel_tip, expected.kel_tip
)));
}
match extract_did_keri_san(cert_pem)? {
Some(san) if san == expected.did_keri() => Ok(()),
Some(san) => Err(TlsCertError::SanMismatch(format!(
"SAN {san} != {}",
expected.did_keri()
))),
None => Err(TlsCertError::SanMismatch(
"certificate carries no did:keri SAN".to_string(),
)),
}
}
pub fn verify_binds_to_key_state(
cert_pem: &str,
state: &KeyState,
) -> Result<AuthsKeriBinding, TlsCertError> {
let expected = AuthsKeriBinding::from_key_state(state)?;
let embedded = extract_binding(cert_pem)?;
check_binds_to_key_state(cert_pem, &embedded, &expected)?;
Ok(embedded)
}
pub fn verify_authorized_against_key_state(
cert_pem: &str,
state: &KeyState,
) -> Result<AuthsKeriBinding, TlsCertError> {
let expected = AuthsKeriBinding::from_key_state(state)?;
let embedded = extract_binding(cert_pem)?;
check_binds_to_key_state(cert_pem, &embedded, &expected)?;
let authorization = embedded
.tls_key_authorization
.as_ref()
.ok_or(TlsCertError::MissingAuthorization)?;
let spki_der = extract_spki_der(cert_pem)?;
check_tls_key_authorization(authorization, &expected.current_keys, &spki_der)?;
Ok(embedded)
}
fn oid_string(arc: &[u64]) -> String {
arc.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(".")
}
fn unwrap_octet_string(der: &[u8]) -> Result<Vec<u8>, TlsCertError> {
yasna::parse_der(der, |r| r.read_bytes())
.map_err(|e| TlsCertError::MalformedBinding(format!("OCTET STRING: {e}")))
}
}
#[cfg(feature = "tls-cert")]
pub use backend::{
IssuedCert, extract_aid_from_san, extract_binding, extract_did_keri_san, extract_spki_der,
issue_authorized_kel_rooted_cert, issue_authorized_kel_rooted_cert_with_key,
issue_kel_rooted_cert, issue_kel_rooted_cert_with_key, verify_authorized_against_key_state,
verify_binds_to_key_state,
};
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::types::{CesrKey, Prefix, Said, Threshold};
fn state(aid: &str, keys: &[&str], tip: &str) -> KeyState {
KeyState::from_inception(
Prefix::new_unchecked(aid.to_string()),
keys.iter()
.map(|k| CesrKey::new_unchecked(k.to_string()))
.collect(),
vec![Said::new_unchecked("ENext0".to_string())],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked(tip.to_string()),
vec![],
Threshold::Simple(0),
vec![],
)
}
fn ed25519_key(raw: &[u8; 32]) -> String {
KeriPublicKey::ed25519(raw).unwrap().to_qb64().unwrap()
}
#[test]
fn binding_projects_key_state() {
let k = ed25519_key(&[7u8; 32]);
let st = state("EAidAAA", &[&k], "ETip000");
let b = AuthsKeriBinding::from_key_state(&st).unwrap();
assert_eq!(b.aid, "EAidAAA");
assert_eq!(b.current_keys, vec![k]);
assert_eq!(b.kel_tip, "ETip000");
assert_eq!(b.did_keri(), "did:keri:EAidAAA");
}
#[test]
fn binding_rejects_undecodable_key_at_boundary() {
let st = state("EAidAAA", &["Xnot-a-verkey"], "ETip000");
assert!(matches!(
AuthsKeriBinding::from_key_state(&st),
Err(TlsCertError::Key(_))
));
}
#[test]
fn binding_canonical_json_round_trips() {
let k = ed25519_key(&[3u8; 32]);
let st = state("EAidAAA", &[&k], "ETip000");
let b = AuthsKeriBinding::from_key_state(&st).unwrap();
let json = b.to_canonical_json();
let back = AuthsKeriBinding::from_canonical_json(&json).unwrap();
assert_eq!(back, b);
}
#[test]
fn binding_json_field_order_is_canonical() {
let k = ed25519_key(&[1u8; 32]);
let st = state("EAidAAA", &[&k], "ETip000");
let b = AuthsKeriBinding::from_key_state(&st).unwrap();
let s = String::from_utf8(b.to_canonical_json()).unwrap();
let i_aid = s.find("\"aid\"").unwrap();
let i_keys = s.find("\"current_keys\"").unwrap();
let i_tip = s.find("\"kel_tip\"").unwrap();
assert!(i_aid < i_keys && i_keys < i_tip, "field order: {s}");
}
#[test]
fn malformed_binding_json_is_an_error_not_a_panic() {
assert!(matches!(
AuthsKeriBinding::from_canonical_json(b"not json"),
Err(TlsCertError::MalformedBinding(_))
));
}
#[cfg(feature = "tls-cert")]
mod backend_tests {
use super::*;
use crate::tls_cert::backend::issue_with_keypair;
use rcgen::PublicKeyData;
fn multi_state() -> (KeyState, Vec<String>) {
let k1 = ed25519_key(&[1u8; 32]);
let k2 = ed25519_key(&[2u8; 32]);
let st = state(
"EAidMultiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
&[&k1, &k2],
"ETipMultiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
);
(st, vec![k1, k2])
}
struct Ed25519Authorizer {
keypair: ring::signature::Ed25519KeyPair,
key_index: usize,
}
impl Ed25519Authorizer {
fn from_seed(seed: &[u8; 32], key_index: usize) -> Self {
let keypair = ring::signature::Ed25519KeyPair::from_seed_unchecked(seed).unwrap();
Self { keypair, key_index }
}
fn cesr_key(&self) -> String {
use ring::signature::KeyPair;
let raw: [u8; 32] = self.keypair.public_key().as_ref().try_into().unwrap();
ed25519_key(&raw)
}
}
impl TlsKeyAuthorizer for Ed25519Authorizer {
fn current_key_index(&self) -> usize {
self.key_index
}
fn sign_tls_key(&self, spki_der: &[u8]) -> Result<Vec<u8>, TlsCertError> {
Ok(self.keypair.sign(spki_der).as_ref().to_vec())
}
}
fn authorized_state(seed: &[u8; 32]) -> (KeyState, Ed25519Authorizer) {
let auth = Ed25519Authorizer::from_seed(seed, 0);
let key = auth.cesr_key();
let st = state(
"EAidAuthAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
&[&key],
"ETipAuthAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
);
(st, auth)
}
#[test]
fn issued_cert_carries_binding_and_san() {
let (st, keys) = multi_state();
let issued =
issue_kel_rooted_cert(&st, &["localhost".to_string(), "127.0.0.1".to_string()])
.unwrap();
assert!(issued.cert_pem.contains("BEGIN CERTIFICATE"));
assert!(!issued.key_pem.is_empty());
let binding = extract_binding(&issued.cert_pem).unwrap();
assert_eq!(binding.aid, st.prefix.as_str());
assert_eq!(binding.current_keys, keys);
let san = extract_did_keri_san(&issued.cert_pem).unwrap();
assert_eq!(san, Some(format!("did:keri:{}", st.prefix.as_str())));
}
#[test]
fn aid_reads_out_of_the_san_without_the_kel() {
let (st, _) = multi_state();
let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
let aid = extract_aid_from_san(&issued.cert_pem).unwrap();
assert_eq!(aid.as_str(), st.prefix.as_str());
}
#[test]
fn plain_cert_has_no_san_identity() {
let kp = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let cert = params.self_signed(&kp).unwrap();
assert!(matches!(
extract_aid_from_san(&cert.pem()),
Err(TlsCertError::NoSanIdentity)
));
}
#[test]
fn malformed_san_aid_is_rejected_at_the_boundary() {
let kp = rcgen::KeyPair::generate().unwrap();
let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
let bad = rcgen::string::Ia5String::try_from("did:keri:".to_string()).unwrap();
params.subject_alt_names.push(rcgen::SanType::URI(bad));
let cert = params.self_signed(&kp).unwrap();
assert!(matches!(
extract_aid_from_san(&cert.pem()),
Err(TlsCertError::InvalidSanAid(_))
));
}
#[test]
fn issued_cert_verifies_against_the_same_key_state() {
let (st, _) = multi_state();
let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
let binding = verify_binds_to_key_state(&issued.cert_pem, &st).unwrap();
assert_eq!(binding.aid, st.prefix.as_str());
}
#[test]
fn cert_is_rejected_against_a_different_key_state() {
let (st, _) = multi_state();
let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
let other_key = ed25519_key(&[9u8; 32]);
let other = state(
st.prefix.as_str(),
&[&other_key],
st.last_event_said.as_str(),
);
assert!(matches!(
verify_binds_to_key_state(&issued.cert_pem, &other),
Err(TlsCertError::BindingMismatch(_))
));
}
#[test]
fn cert_is_rejected_against_a_different_aid() {
let (st, _) = multi_state();
let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
let k = ed25519_key(&[1u8; 32]);
let other = state(
"EAidOTHERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
&[&k],
"ETip",
);
assert!(matches!(
verify_binds_to_key_state(&issued.cert_pem, &other),
Err(TlsCertError::BindingMismatch(_))
));
}
#[test]
fn plain_cert_has_no_binding() {
let kp = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let cert = params.self_signed(&kp).unwrap();
assert!(matches!(
extract_binding(&cert.pem()),
Err(TlsCertError::MissingBinding)
));
}
#[test]
fn issue_with_supplied_key_is_deterministic_in_binding() {
let (st, _) = multi_state();
let kp = rcgen::KeyPair::generate().unwrap();
let pem = kp.serialize_pem();
let a = issue_kel_rooted_cert_with_key(&st, &pem, &["localhost".to_string()]).unwrap();
let b = issue_kel_rooted_cert_with_key(&st, &pem, &["localhost".to_string()]).unwrap();
assert_eq!(
extract_binding(&a.cert_pem).unwrap(),
extract_binding(&b.cert_pem).unwrap()
);
}
#[test]
fn authorized_cert_carries_authorization_and_verifies() {
let (st, auth) = authorized_state(&[7u8; 32]);
let issued =
issue_authorized_kel_rooted_cert(&st, &auth, &["localhost".to_string()]).unwrap();
let embedded = extract_binding(&issued.cert_pem).unwrap();
assert!(
embedded.tls_key_authorization.is_some(),
"an authorized leaf must embed the authorization"
);
let binding = verify_authorized_against_key_state(&issued.cert_pem, &st).unwrap();
assert_eq!(binding.aid, st.prefix.as_str());
}
#[test]
fn forged_binding_over_attackers_tls_key_is_rejected() {
let (st, _auth) = authorized_state(&[7u8; 32]);
let attacker = Ed25519Authorizer::from_seed(&[99u8; 32], 0);
let forged_kp = rcgen::KeyPair::generate().unwrap();
let spki = forged_kp.subject_public_key_info();
let forged_sig = attacker.sign_tls_key(&spki).unwrap();
let binding = AuthsKeriBinding::from_key_state(&st)
.unwrap()
.with_authorization(TlsKeyAuthorization {
key_index: 0,
signature: forged_sig,
});
let issued =
issue_with_keypair(&binding, &forged_kp, &["localhost".to_string()]).unwrap();
assert!(verify_binds_to_key_state(&issued.cert_pem, &st).is_ok());
assert!(matches!(
verify_authorized_against_key_state(&issued.cert_pem, &st),
Err(TlsCertError::Unauthorized(_))
));
}
#[test]
fn stripped_authorization_is_rejected() {
let (st, _auth) = authorized_state(&[7u8; 32]);
let unauthorized = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
assert!(matches!(
verify_authorized_against_key_state(&unauthorized.cert_pem, &st),
Err(TlsCertError::MissingAuthorization)
));
}
#[test]
fn stripped_binding_plain_cert_is_rejected() {
let (st, _auth) = authorized_state(&[7u8; 32]);
let kp = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let plain = params.self_signed(&kp).unwrap();
assert!(matches!(
verify_authorized_against_key_state(&plain.pem(), &st),
Err(TlsCertError::MissingBinding)
));
}
#[test]
fn revoked_or_rotated_aid_is_rejected() {
let (old_state, auth) = authorized_state(&[7u8; 32]);
let issued =
issue_authorized_kel_rooted_cert(&old_state, &auth, &["localhost".to_string()])
.unwrap();
let rotated_key = ed25519_key(&[8u8; 32]);
let current_state = state(
old_state.prefix.as_str(),
&[&rotated_key],
"ETipRotatedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
);
assert!(matches!(
verify_authorized_against_key_state(&issued.cert_pem, ¤t_state),
Err(TlsCertError::BindingMismatch(_))
));
}
#[test]
fn authorization_with_out_of_range_index_is_rejected() {
let (st, auth) = authorized_state(&[7u8; 32]);
let kp = rcgen::KeyPair::generate().unwrap();
let spki = kp.subject_public_key_info();
let sig = auth.sign_tls_key(&spki).unwrap();
let binding = AuthsKeriBinding::from_key_state(&st)
.unwrap()
.with_authorization(TlsKeyAuthorization {
key_index: 5, signature: sig,
});
let issued = issue_with_keypair(&binding, &kp, &["localhost".to_string()]).unwrap();
assert!(matches!(
verify_authorized_against_key_state(&issued.cert_pem, &st),
Err(TlsCertError::AuthorizationIndexOutOfRange { index: 5, len: 1 })
));
}
#[test]
fn issuer_rejects_authorizer_signing_with_wrong_key() {
let (st, _auth) = authorized_state(&[7u8; 32]);
let wrong = Ed25519Authorizer::from_seed(&[42u8; 32], 0);
assert!(matches!(
issue_authorized_kel_rooted_cert(&st, &wrong, &["localhost".to_string()]),
Err(TlsCertError::Unauthorized(_))
));
}
#[test]
fn issuer_rejects_out_of_range_authorizer_index() {
let (st, _auth) = authorized_state(&[7u8; 32]);
let bad_index = Ed25519Authorizer::from_seed(&[7u8; 32], 9);
assert!(matches!(
issue_authorized_kel_rooted_cert(&st, &bad_index, &["localhost".to_string()]),
Err(TlsCertError::AuthorizationIndexOutOfRange { index: 9, len: 1 })
));
}
#[test]
fn authorization_round_trips_through_binding_json() {
let (st, auth) = authorized_state(&[7u8; 32]);
let issued =
issue_authorized_kel_rooted_cert(&st, &auth, &["localhost".to_string()]).unwrap();
let embedded = extract_binding(&issued.cert_pem).unwrap();
let json = embedded.to_canonical_json();
let back = AuthsKeriBinding::from_canonical_json(&json).unwrap();
assert_eq!(back, embedded);
assert!(back.tls_key_authorization.is_some());
}
}
}