use alloc::vec::Vec;
use super::{Certificate, Error, oid};
use crate::der::Reader;
use crate::hash::{Digest, Sha256};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SctVersion {
V1,
}
#[derive(Clone, Debug)]
pub struct Sct {
pub version: SctVersion,
pub log_id: [u8; 32],
pub timestamp: u64,
pub extensions: Vec<u8>,
pub sig_hash: u8,
pub sig_alg: u8,
pub signature: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct CtLog {
pub log_id: [u8; 32],
pub spki_der: Vec<u8>,
}
impl CtLog {
pub fn from_spki_der(spki_der: &[u8]) -> CtLog {
let log_id = Sha256::digest(spki_der);
CtLog {
log_id,
spki_der: spki_der.to_vec(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SctVerification {
Valid,
UnknownLog,
BadSignature,
FutureTimestamp,
}
struct TlsReader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> TlsReader<'a> {
fn new(buf: &'a [u8]) -> Self {
TlsReader { buf, pos: 0 }
}
fn remaining(&self) -> usize {
self.buf.len() - self.pos
}
fn is_empty(&self) -> bool {
self.pos >= self.buf.len()
}
fn take(&mut self, n: usize) -> Result<&'a [u8], Error> {
if self.remaining() < n {
return Err(Error::Malformed);
}
let s = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, Error> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, Error> {
let b = self.take(2)?;
Ok(((b[0] as u16) << 8) | b[1] as u16)
}
fn u64(&mut self) -> Result<u64, Error> {
let b = self.take(8)?;
let mut v = 0u64;
for &x in b {
v = (v << 8) | x as u64;
}
Ok(v)
}
fn opaque_u16(&mut self) -> Result<&'a [u8], Error> {
let n = self.u16()? as usize;
self.take(n)
}
}
pub fn parse_sct_list(body: &[u8]) -> Result<Vec<Sct>, Error> {
let mut r = TlsReader::new(body);
let total = r.u16()? as usize;
if r.remaining() != total {
return Err(Error::Malformed);
}
let mut out = Vec::new();
while !r.is_empty() {
let one = r.opaque_u16()?;
out.push(parse_one_sct(one)?);
}
Ok(out)
}
pub fn sct_list_from_extension(ext_value: &[u8]) -> Result<Vec<Sct>, Error> {
let mut r = Reader::new(ext_value);
let inner = r.read_octet_string()?;
r.finish()?;
parse_sct_list(inner)
}
fn parse_one_sct(bytes: &[u8]) -> Result<Sct, Error> {
let mut r = TlsReader::new(bytes);
let version = match r.u8()? {
0 => SctVersion::V1,
_ => return Err(Error::UnsupportedAlgorithm),
};
let log_id: [u8; 32] = r.take(32)?.try_into().map_err(|_| Error::Malformed)?;
let timestamp = r.u64()?;
let extensions = r.opaque_u16()?.to_vec();
let sig_hash = r.u8()?;
let sig_alg = r.u8()?;
let signature = r.opaque_u16()?.to_vec();
if !r.is_empty() {
return Err(Error::Malformed);
}
Ok(Sct {
version,
log_id,
timestamp,
extensions,
sig_hash,
sig_alg,
signature,
})
}
impl Sct {
pub fn signed_data_for_precert(&self, issuer_spki_der: &[u8], tbs_der: &[u8]) -> Vec<u8> {
let issuer_key_hash = Sha256::digest(issuer_spki_der);
let mut out = Vec::with_capacity(64 + tbs_der.len());
out.push(0); out.push(0); out.extend_from_slice(&self.timestamp.to_be_bytes());
out.extend_from_slice(&[0x00, 0x01]); out.extend_from_slice(&issuer_key_hash);
push_u24_vec(&mut out, tbs_der);
push_u16_vec(&mut out, &self.extensions);
out
}
pub fn signed_data_for_x509(&self, cert_der: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(16 + cert_der.len());
out.push(0);
out.push(0);
out.extend_from_slice(&self.timestamp.to_be_bytes());
out.extend_from_slice(&[0x00, 0x00]); push_u24_vec(&mut out, cert_der);
push_u16_vec(&mut out, &self.extensions);
out
}
fn verify_signature(&self, log: &CtLog, signed_data: &[u8]) -> bool {
if self.sig_hash != 4 {
return false;
}
let sig_alg_oid: &[u64] = match self.sig_alg {
1 => oid::SHA256_WITH_RSA,
3 => oid::ECDSA_WITH_SHA256,
_ => return false,
};
let Ok(key) = super::AnyPublicKey::from_spki_der(&log.spki_der) else {
return false;
};
key.verify(sig_alg_oid, signed_data, &self.signature)
.is_ok()
}
}
impl Sct {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(match self.version {
SctVersion::V1 => 0,
});
out.extend_from_slice(&self.log_id);
out.extend_from_slice(&self.timestamp.to_be_bytes());
push_u16_vec(&mut out, &self.extensions);
out.push(self.sig_hash);
out.push(self.sig_alg);
push_u16_vec(&mut out, &self.signature);
out
}
}
pub fn serialize_sct_list(scts: &[Sct]) -> Vec<u8> {
let mut inner = Vec::new();
for s in scts {
push_u16_vec(&mut inner, &s.to_bytes());
}
let mut out = Vec::new();
push_u16_vec(&mut out, &inner);
out
}
fn push_u24_vec(out: &mut Vec<u8>, data: &[u8]) {
let n = data.len();
out.push((n >> 16) as u8);
out.push((n >> 8) as u8);
out.push(n as u8);
out.extend_from_slice(data);
}
fn push_u16_vec(out: &mut Vec<u8>, data: &[u8]) {
out.push((data.len() >> 8) as u8);
out.push(data.len() as u8);
out.extend_from_slice(data);
}
pub fn reconstruct_precert_tbs(cert: &Certificate) -> Result<Vec<u8>, Error> {
let tbs = cert.tbs_der()?;
rebuild_tbs_without(tbs, &[oid::CT_POISON, oid::SCT_LIST])
}
fn rebuild_tbs_without(tbs_der: &[u8], remove_oids: &[&[u64]]) -> Result<Vec<u8>, Error> {
use crate::der::{encode_context, encode_sequence, parse_oid, tag};
let mut outer = Reader::new(tbs_der);
let mut seq = outer.read_sequence()?;
outer.finish()?;
let mut prefix: Vec<u8> = Vec::new();
if seq.peek_tag() == Some(tag::context(0)) {
prefix.extend_from_slice(seq.read_element()?); }
prefix.extend_from_slice(seq.read_element()?); prefix.extend_from_slice(seq.read_element()?); prefix.extend_from_slice(seq.read_element()?); prefix.extend_from_slice(seq.read_element()?); prefix.extend_from_slice(seq.read_element()?); prefix.extend_from_slice(seq.read_element()?); while matches!(seq.peek_tag(), Some(0x81) | Some(0x82)) {
prefix.extend_from_slice(seq.read_element()?); }
let exts_wrapper = match seq.peek_tag() {
Some(t) if t == tag::context(3) => seq.read_tlv(tag::context(3))?,
_ => {
seq.finish()?;
return Ok(tbs_der.to_vec());
}
};
seq.finish()?;
let mut ew = Reader::new(exts_wrapper);
let mut exts = ew.read_sequence()?;
ew.finish()?;
let mut kept: Vec<u8> = Vec::new();
while !exts.is_empty() {
let ext_tlv = exts.read_element()?;
let mut er = Reader::new(ext_tlv);
let mut es = er.read_sequence()?;
let id = parse_oid(es.read_oid()?)?;
let remove = remove_oids.contains(&id.as_slice());
if !remove {
kept.extend_from_slice(ext_tlv);
}
}
let mut rebuilt = prefix;
if !kept.is_empty() {
rebuilt.extend_from_slice(&encode_context(3, &encode_sequence(&kept)));
}
Ok(encode_sequence(&rebuilt))
}
pub fn verify_embedded_scts(
cert: &Certificate,
issuer: &Certificate,
logs: &[CtLog],
now_ms: u64,
) -> Result<(Vec<SctVerification>, usize), Error> {
let Some(ext_value) = cert.sct_list_extension()? else {
return Ok((Vec::new(), 0));
};
let scts = sct_list_from_extension(&ext_value)?;
let issuer_spki = issuer.spki_der()?;
let tbs = reconstruct_precert_tbs(cert)?;
let mut results = Vec::with_capacity(scts.len());
let mut valid = 0usize;
for sct in &scts {
let res = verify_one(sct, issuer_spki, &tbs, logs, now_ms);
if res == SctVerification::Valid {
valid += 1;
}
results.push(res);
}
Ok((results, valid))
}
fn verify_one(
sct: &Sct,
issuer_spki: &[u8],
tbs: &[u8],
logs: &[CtLog],
now_ms: u64,
) -> SctVerification {
if sct.timestamp > now_ms {
return SctVerification::FutureTimestamp;
}
let Some(log) = logs.iter().find(|l| l.log_id == sct.log_id) else {
return SctVerification::UnknownLog;
};
let signed = sct.signed_data_for_precert(issuer_spki, tbs);
if sct.verify_signature(log, &signed) {
SctVerification::Valid
} else {
SctVerification::BadSignature
}
}
pub fn verify_standalone_sct(
sct: &Sct,
leaf_cert_der: &[u8],
logs: &[CtLog],
now_ms: u64,
) -> SctVerification {
if sct.timestamp > now_ms {
return SctVerification::FutureTimestamp;
}
let Some(log) = logs.iter().find(|l| l.log_id == sct.log_id) else {
return SctVerification::UnknownLog;
};
let signed = sct.signed_data_for_x509(leaf_cert_der);
if sct.verify_signature(log, &signed) {
SctVerification::Valid
} else {
SctVerification::BadSignature
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ec::{BoxedEcdsaPrivateKey, CurveId};
use crate::rng::HmacDrbg;
use crate::test_util::rsa_test_key_a;
use crate::x509::extension;
use crate::x509::{
AnyPublicKey, CertSigner, Certificate, DistinguishedName, GeneralName, Time, Validity,
};
use alloc::vec;
fn validity() -> Validity {
Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
)
}
struct TestLog {
sk: BoxedEcdsaPrivateKey,
log: CtLog,
}
fn make_test_log(seed: &[u8]) -> TestLog {
let mut rng = HmacDrbg::<Sha256>::new(b"ct-log", seed, &[]);
let sk = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let spki = AnyPublicKey::Ecdsa(sk.public_key()).to_spki_der();
let log = CtLog::from_spki_der(&spki);
TestLog { sk, log }
}
fn sign_sct(log: &TestLog, timestamp: u64, signed_data: &[u8]) -> Sct {
let sig = log.sk.sign::<Sha256>(signed_data).unwrap();
Sct {
version: SctVersion::V1,
log_id: log.log.log_id,
timestamp,
extensions: Vec::new(),
sig_hash: 4, sig_alg: 3, signature: sig.to_der(CurveId::P256),
}
}
fn issue_issuer_and_leaf() -> (Certificate, Certificate) {
let ca_key = rsa_test_key_a();
let ca_b = BoxedRsaPrivateKey::from_pkcs1_der(&ca_key.to_pkcs1_der()).unwrap();
let ca_name = DistinguishedName::common_name("CT Issuer");
let leaf_name = DistinguishedName::common_name("ct.example");
let issuer = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf = Certificate::issue_with_extensions(
&CertSigner::Rsa(&ca_b),
&ca_name,
&leaf_name,
&AnyPublicKey::Rsa(ca_b.public_key()),
&validity(),
2,
&[
extension::basic_constraints(false, None),
extension::subject_alt_name(&[GeneralName::Dns("ct.example".into())]),
],
)
.unwrap();
(issuer, leaf)
}
use crate::rsa::BoxedRsaPrivateKey;
#[test]
fn sct_list_round_trip() {
let log = make_test_log(b"rt");
let sct = sign_sct(&log, 1_700_000_000_000, b"data");
let list = serialize_sct_list(core::slice::from_ref(&sct));
let parsed = parse_sct_list(&list).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].log_id, sct.log_id);
assert_eq!(parsed[0].timestamp, sct.timestamp);
assert_eq!(parsed[0].signature, sct.signature);
let ext = extension::sct_list(&list);
let via_ext = sct_list_from_extension(&ext.value).unwrap();
assert_eq!(via_ext.len(), 1);
assert_eq!(via_ext[0].timestamp, sct.timestamp);
}
#[test]
fn standalone_x509_sct_accepts_and_rejects() {
let (_issuer, leaf) = issue_issuer_and_leaf();
let leaf_der = leaf.to_der().to_vec();
let log = make_test_log(b"x509");
let ts = 1_700_000_000_000u64;
let now = ts + 1000;
let proto = Sct {
version: SctVersion::V1,
log_id: log.log.log_id,
timestamp: ts,
extensions: Vec::new(),
sig_hash: 4,
sig_alg: 3,
signature: Vec::new(),
};
let signed = proto.signed_data_for_x509(&leaf_der);
let sct = sign_sct(&log, ts, &signed);
assert_eq!(
verify_standalone_sct(&sct, &leaf_der, core::slice::from_ref(&log.log), now),
SctVerification::Valid
);
let other = make_test_log(b"other");
assert_eq!(
verify_standalone_sct(&sct, &leaf_der, core::slice::from_ref(&other.log), now),
SctVerification::UnknownLog
);
let mut tampered = sct.clone();
tampered.timestamp = ts + 5;
assert_eq!(
verify_standalone_sct(&tampered, &leaf_der, core::slice::from_ref(&log.log), now),
SctVerification::BadSignature
);
let mut badsig = sct.clone();
let n = badsig.signature.len();
badsig.signature[n - 1] ^= 0x01;
assert_eq!(
verify_standalone_sct(&badsig, &leaf_der, core::slice::from_ref(&log.log), now),
SctVerification::BadSignature
);
assert_eq!(
verify_standalone_sct(&sct, &leaf_der, core::slice::from_ref(&log.log), ts - 1),
SctVerification::FutureTimestamp
);
}
#[test]
fn embedded_precert_sct_accepts_and_rejects() {
let (issuer, leaf) = issue_issuer_and_leaf();
let issuer_spki = issuer.spki_der().unwrap().to_vec();
let tbs = reconstruct_precert_tbs(&leaf).unwrap();
let log = make_test_log(b"embed");
let ts = 1_700_000_000_000u64;
let now = ts + 1000;
let proto = Sct {
version: SctVersion::V1,
log_id: log.log.log_id,
timestamp: ts,
extensions: Vec::new(),
sig_hash: 4,
sig_alg: 3,
signature: Vec::new(),
};
let signed = proto.signed_data_for_precert(&issuer_spki, &tbs);
let sct = sign_sct(&log, ts, &signed);
let list = serialize_sct_list(core::slice::from_ref(&sct));
let ca_key = rsa_test_key_a();
let ca_b = BoxedRsaPrivateKey::from_pkcs1_der(&ca_key.to_pkcs1_der()).unwrap();
let leaf_with_sct = Certificate::issue_with_extensions(
&CertSigner::Rsa(&ca_b),
&DistinguishedName::common_name("CT Issuer"),
&DistinguishedName::common_name("ct.example"),
&AnyPublicKey::Rsa(ca_b.public_key()),
&validity(),
2,
&[
extension::basic_constraints(false, None),
extension::subject_alt_name(&[GeneralName::Dns("ct.example".into())]),
extension::sct_list(&list),
],
)
.unwrap();
assert_eq!(reconstruct_precert_tbs(&leaf_with_sct).unwrap(), tbs);
let (results, valid) = verify_embedded_scts(
&leaf_with_sct,
&issuer,
core::slice::from_ref(&log.log),
now,
)
.unwrap();
assert_eq!(results, vec![SctVerification::Valid]);
assert_eq!(valid, 1);
let wrong_issuer = {
let k = crate::test_util::rsa_test_key_b();
Certificate::self_signed(
&k,
&DistinguishedName::common_name("Wrong"),
&validity(),
9,
true,
)
.unwrap()
};
let (r2, v2) = verify_embedded_scts(
&leaf_with_sct,
&wrong_issuer,
core::slice::from_ref(&log.log),
now,
)
.unwrap();
assert_eq!(r2, vec![SctVerification::BadSignature]);
assert_eq!(v2, 0);
let other = make_test_log(b"nope");
let (r3, v3) = verify_embedded_scts(
&leaf_with_sct,
&issuer,
core::slice::from_ref(&other.log),
now,
)
.unwrap();
assert_eq!(r3, vec![SctVerification::UnknownLog]);
assert_eq!(v3, 0);
let (r4, _) = verify_embedded_scts(
&leaf_with_sct,
&issuer,
core::slice::from_ref(&log.log),
ts - 1,
)
.unwrap();
assert_eq!(r4, vec![SctVerification::FutureTimestamp]);
}
#[test]
fn precert_reconstruction_strips_poison_and_sct() {
let ca_key = rsa_test_key_a();
let ca_b = BoxedRsaPrivateKey::from_pkcs1_der(&ca_key.to_pkcs1_der()).unwrap();
let with_both = Certificate::issue_with_extensions(
&CertSigner::Rsa(&ca_b),
&DistinguishedName::common_name("CA"),
&DistinguishedName::common_name("leaf"),
&AnyPublicKey::Rsa(ca_b.public_key()),
&validity(),
2,
&[
extension::basic_constraints(false, None),
extension::ct_poison(),
extension::subject_alt_name(&[GeneralName::Dns("leaf".into())]),
extension::sct_list(&[0x00, 0x00]),
],
)
.unwrap();
let without = Certificate::issue_with_extensions(
&CertSigner::Rsa(&ca_b),
&DistinguishedName::common_name("CA"),
&DistinguishedName::common_name("leaf"),
&AnyPublicKey::Rsa(ca_b.public_key()),
&validity(),
2,
&[
extension::basic_constraints(false, None),
extension::subject_alt_name(&[GeneralName::Dns("leaf".into())]),
],
)
.unwrap();
let rebuilt = reconstruct_precert_tbs(&with_both).unwrap();
assert_eq!(rebuilt, without.tbs_der().unwrap());
}
}