// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// A certificate the peer chose, read into something the TLS side can act on. RFC 5280 §4 layers
// rules on top of X.690 that a generator never has to think about — a DEFAULT that must be left
// out, an extension that must not repeat, an inner signature algorithm that must equal the outer
// one, a critical extension that must not be skipped — and each of them exists because a
// certificate with two readings has two identities. They are enforced here, so a caller holding
// a `Cert` is holding one certificate rather than a choice of them.
///|
/// A validity window, in seconds since the Unix epoch.
pub(all) struct Validity {
not_before : Int64
not_after : Int64
} derive(Eq, Debug)
///|
/// A parsed certificate. `tbs` is the exact encoding the signature covers and `raw` the whole
/// certificate, both as views into the input: a signature is checked over the bytes that arrived,
/// never over a re-encoding of what they were understood to mean. `params` is the encoding of
/// the signature algorithm's parameters, which RSASSA-PSS needs.
pub(all) struct Cert {
raw : BytesView
tbs : BytesView
version : Int32
serial : BigInt
alg : String
params : Bytes?
issuer : Name
validity : Validity
subject : Name
spki : Spki
exts : Exts
sig : @asn1.Bits
}
///|
/// A Validity (RFC 5280 §4.1.2.5).
fn read_window(tlv : @asn1.Tlv) -> Validity raise @asn1.Refused {
if tlv.tag != 0x30 {
raise @asn1.Malformed("Validity is not a SEQUENCE")
}
let items = tlv.items()
if items.length() != 2 {
raise @asn1.Malformed("Validity is not a notBefore and a notAfter")
}
{ not_before: items[0].time(), not_after: items[1].time(), }
}
///|
/// Parse a DER certificate (RFC 5280 §4.1). Raises on anything that is not exactly one
/// well-formed certificate: a truncated or over-long field, a payload glued to the end, a version
/// that does not admit the fields present, or an outer signature algorithm that disagrees with
/// the one the signature actually covers.
pub fn parse(der : BytesView) -> Cert raise @asn1.Refused {
let cert = @asn1.only(der)
if cert.tag != 0x30 {
raise @asn1.Malformed("Certificate is not a SEQUENCE")
}
let top = cert.items()
if top.length() != 3 {
raise @asn1.Malformed(
"Certificate is not a TBSCertificate, an algorithm and a signature",
)
}
let tbs = top[0]
if tbs.tag != 0x30 {
raise @asn1.Malformed("TBSCertificate is not a SEQUENCE")
}
let f = tbs.items()
let mut version = 1
let mut i = 0
if f.length() > 0 && f[0].tag == 0xa0 {
let v = @asn1.only(f[0].value).int()
if v < (0 : BigInt) || v > (2 : BigInt) {
raise @asn1.Malformed("unknown certificate version")
}
version = v.to_int() + 1
i = 1
}
if f.length() < i + 6 {
raise @asn1.Malformed("TBSCertificate is missing fields")
}
let serial = f[i].int()
let (alg, params) = read_alg(f[i + 1])
// RFC 5280 §4.1.1.2: the two algorithm identifiers are the same field written twice, and only
// the inner one is signed. A peer that can make them differ chooses the algorithm a verifier
// reads without touching what the issuer signed.
if top[1].raw != f[i + 1].raw {
raise @asn1.Malformed(
"the signature algorithm differs from the one the TBSCertificate names",
)
}
let issuer = read_name(f[i + 2])
let validity = read_window(f[i + 3])
let subject = read_name(f[i + 4])
let spki = read_spki(f[i + 5])
let mut exts = no_exts
let mut prev = 0
for j = i + 6; j < f.length(); j = j + 1 {
let t = f[j]
if (t.tag & 0xc0) != 0x80 {
raise @asn1.Malformed("unexpected field in TBSCertificate")
}
let n = t.tag & 0x1f
if n < 1 || n > 3 || n <= prev {
raise @asn1.Malformed("unexpected field in TBSCertificate")
}
prev = n
if n == 3 {
if t.tag != 0xa3 {
raise @asn1.Malformed("extensions are not EXPLICIT-tagged")
}
if version != 3 {
raise @asn1.Malformed("extensions in a certificate below v3")
}
exts = read_exts(t)
} else if version == 1 {
raise @asn1.Malformed("unique identifier in a v1 certificate")
}
}
{
raw: cert.raw,
tbs: tbs.raw,
version,
serial,
alg,
params: params.map(p => p.to_owned()),
issuer,
validity,
subject,
spki,
exts,
sig: top[2].bits(),
}
}
///|
/// Whether `now`, in seconds since the Unix epoch, falls inside the certificate's validity
/// window. Both ends are inclusive: RFC 5280 §4.1.2.5 gives them as the first and last instant
/// the certificate is valid, not as an open interval.
pub fn Cert::valid_at(self : Cert, now : Int64) -> Bool {
now >= self.validity.not_before && now <= self.validity.not_after
}
///|
/// Whether the dNSName `pattern` names `host` (RFC 6125 §6.4.3): label by label, ASCII case
/// insensitive. A first label of exactly `*` stands for one whole label, so `*.a.example` covers
/// `b.a.example` and neither `a.example` nor `c.b.a.example`. A `*` anywhere else, or fused to
/// the rest of its label as in `b*.a.example`, matches nothing — a partial wildcard is a claim on
/// a name the issuer never checked.
pub fn dns_match(pattern : String, host : String) -> Bool {
let p = pattern.split(".").to_array()
let h = host.split(".").to_array()
if p.length() != h.length() || p.length() == 0 {
return false
}
// A lone `*` would otherwise name every single-label host. How much of the tail a wildcard may
// cover past that is a question about public suffixes, which needs a list this package has no
// business carrying; that rule belongs to the caller.
if p[0] == "*" && p.length() < 2 {
return false
}
for i in 0.. Bool {
match ip_literal(host) {
Some(addr) => self.exts.san.iter().any(n => n is Ip(a) && a == addr)
None => {
let h = match host.strip_suffix(".") {
Some(v) => v.to_owned()
None => host
}
self.exts.san.iter().any(n => n is Dns(p) && dns_match(p, h))
}
}
}
///|
pub extend Validity with Debug::{to_repr}
///|
pub extend Validity with Eq::{not_equal, equal}