Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- **The pure-Aether TLS client no longer accepts a certificate signed by a
non-CA.** `std.cryptography.tls13_cert`'s `chain_verify_path` checked only
names and signatures at each hop, so any leaf certificate a trusted CA had
issued could sign a certificate for any host and the client accepted it. An
issuing certificate must now carry basicConstraints `cA=TRUE` and, when it has
a keyUsage extension, `keyCertSign`. `leaf_is_ca` and `leaf_key_cert_sign`
report both. The rest of RFC 5280 path validation listed in #3303
(pathLenConstraint, extendedKeyUsage, critical extensions, name constraints,
intermediate validity, backtracking) is still open. The OpenSSL backend was
not affected. Contributed by Adil Alperen Çiftci (#3340).
121 changes: 97 additions & 24 deletions std/cryptography/tls13_cert/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports(
leaf_not_before, leaf_not_before_len, leaf_not_after, leaf_not_after_len,
leaf_dns_count, leaf_dns_name,
leaf_tbs_len, leaf_subject_len, leaf_issuer_len,
leaf_is_ca, leaf_key_cert_sign,
SCHEME_ECDSA_P256_SHA256, SCHEME_ECDSA_P384_SHA384, SCHEME_ED25519, SCHEME_RSA_PSS_RSAE_SHA256,
check_validity, time_to_key, check_hostname,
chain_verify, chain_verify_path, verify_cert_signature, find_issuer_cert,
Expand Down Expand Up @@ -442,6 +443,9 @@ struct LeafCert {
not_after_len: int
dns_names: ptr // std.list of dNSName BYTES buffers (SAN), or null
ip_addrs: ptr // std.list of iPAddress SAN BYTES buffers (4 or 16 bytes), or null
is_ca: int
has_key_usage: int
key_cert_sign: int
}

// Parse a DER Certificate into `out`. Returns "" on success or an error.
Expand Down Expand Up @@ -470,6 +474,9 @@ parse_certificate(der: byte[], out: *LeafCert) -> string {
out.not_after_len = 0
out.dns_names = null
out.ip_addrs = null
out.is_ca = 0
out.has_key_usage = 0
out.key_cert_sign = 0

view = bytes.new(len)
if len > 0 { bytes.set(view, len - 1, 0) }
Expand Down Expand Up @@ -655,8 +662,11 @@ struct CertReaderView {
}

// Parse the [3] extensions content: SEQUENCE OF Extension, each
// { extnID OID, critical BOOLEAN OPTIONAL, extnValue OCTET STRING }. When the
// OID is subjectAltName (2.5.29.17), decode its dNSNames into out.dns_names.
// { extnID OID, critical BOOLEAN OPTIONAL, extnValue OCTET STRING }. Decodes
// subjectAltName (2.5.29.17) into out.dns_names / out.ip_addrs, and
// basicConstraints (2.5.29.19) and keyUsage (2.5.29.15) into out.is_ca,
// out.has_key_usage and out.key_cert_sign, which chain_verify_path checks on
// every issuing certificate.
fn parse_extensions_for_san(ext_wrapper: @keep ptr, out: *LeafCert) {
wr = asn1.reader_new(ext_wrapper)
// [3] wraps a single SEQUENCE OF Extension.
Expand All @@ -670,25 +680,12 @@ fn parse_extensions_for_san(ext_wrapper: @keep ptr, out: *LeafCert) {
xr = asn1.reader_new(one)
oid, oiderr = asn1.read_oid(xr)
if string.equals(oiderr, "") == 1 {
if string.equals(oid, "2.5.29.17") == 1 {
// skip optional critical BOOLEAN, then extnValue OCTET STRING
nx, nxerr = asn1.read_tlv(xr)
ntag = asn1.last_tag(xr)
if string.equals(nxerr, "") == 1 {
if ntag == 0x01 {
// was critical BOOLEAN; read the OCTET STRING next
bytes.free(nx)
os2, os2e = asn1.read_tlv(xr)
if string.equals(os2e, "") == 1 {
decode_san_dnsnames(os2, out)
bytes.free(os2)
}
} else {
// nx WAS the OCTET STRING content
decode_san_dnsnames(nx, out)
bytes.free(nx)
}
}
val = read_extn_value(xr)
if val != null {
if string.equals(oid, "2.5.29.17") == 1 { decode_san_dnsnames(val, out) }
if string.equals(oid, "2.5.29.19") == 1 { decode_basic_constraints(val, out) }
if string.equals(oid, "2.5.29.15") == 1 { decode_key_usage(val, out) }
bytes.free(val)
}
string.release(oid)
}
Expand All @@ -699,6 +696,70 @@ fn parse_extensions_for_san(ext_wrapper: @keep ptr, out: *LeafCert) {
bytes.free(seq_content)
}

// Read an Extension's extnValue OCTET STRING content, skipping the optional
// critical BOOLEAN before it. Returns fresh bytes (caller frees), or null.
fn read_extn_value(xr: ptr) -> ptr {
nx, nxerr = asn1.read_tlv(xr)
if string.equals(nxerr, "") != 1 { return null }
if asn1.last_tag(xr) != 0x01 { return nx }
bytes.free(nx)
os2, os2e = asn1.read_tlv(xr)
if string.equals(os2e, "") != 1 { return null }
return os2
}

// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE,
// pathLenConstraint INTEGER OPTIONAL }. An absent or malformed cA leaves
// out.is_ca at 0, so the certificate cannot issue.
fn decode_basic_constraints(octet_content: @keep ptr, out: *LeafCert) {
bcr = asn1.reader_new(octet_content)
seq_content, serr = asn1.read_tlv(bcr)
stag = asn1.last_tag(bcr)
asn1.reader_free(bcr)
if string.equals(serr, "") != 1 { return }
if stag != 0x30 {
bytes.free(seq_content)
return
}
sr = asn1.reader_new(seq_content)
if asn1.reader_eof(sr) != 1 {
val, verr = asn1.read_tlv(sr)
tag = asn1.last_tag(sr)
if string.equals(verr, "") == 1 {
if tag == 0x01 {
if bytes.length(val) > 0 {
if bytes.get(val, 0) != 0 {
out.is_ca = 1
}
}
}
bytes.free(val)
}
}
asn1.reader_free(sr)
bytes.free(seq_content)
}

// keyUsage ::= BIT STRING. The first content byte counts the unused bits;
// keyCertSign is bit 5, which is 0x04 of the next byte.
fn decode_key_usage(octet_content: @keep ptr, out: *LeafCert) {
kur = asn1.reader_new(octet_content)
ku_content, kerr = asn1.read_tlv(kur)
tag = asn1.last_tag(kur)
asn1.reader_free(kur)
if string.equals(kerr, "") != 1 { return }
if tag == 0x03 {
out.has_key_usage = 1
if bytes.length(ku_content) >= 2 {
b = bytes.get(ku_content, 1)
if (b & 0x04) != 0 {
out.key_cert_sign = 1
}
}
}
bytes.free(ku_content)
}

// The SAN extnValue OCTET STRING wraps GeneralNames ::= SEQUENCE OF GeneralName.
// dNSName is [2] IMPLICIT IA5String (context tag 0x82). Add each to dns_names,
// rejecting any with an embedded NUL.
Expand Down Expand Up @@ -768,6 +829,8 @@ leaf_not_after_len(c: *LeafCert) -> int { return c.not_after_len }
leaf_tbs_len(c: *LeafCert) -> int { return c.tbs_len }
leaf_subject_len(c: *LeafCert) -> int { return c.subject_len }
leaf_issuer_len(c: *LeafCert) -> int { return c.issuer_len }
leaf_is_ca(c: *LeafCert) -> int { return c.is_ca }
leaf_key_cert_sign(c: *LeafCert) -> int { return c.key_cert_sign }

leaf_dns_count(c: *LeafCert) -> int {
if c.dns_names == null { return 0 }
Expand Down Expand Up @@ -1844,16 +1907,23 @@ const MAX_CHAIN_DEPTH = 8

// Like find_issuer but skips any intermediate already in `used` (pointer
// identity), preventing a signed-intermediate cycle from looping forever.
// Only a CA can issue (RFC 5280 §4.2.1.9): basicConstraints cA must be TRUE,
// and keyUsage, when present, must assert keyCertSign (§4.2.1.3). Without
// this a leaf a trusted CA issued could sign a certificate for any host.
fn find_issuer_unused(cert: *LeafCert, intermediates: ptr, used: ptr) -> int {
n = list_size(intermediates)
i = 0
while i < n {
m = list.at(intermediates, i) as *LeafCert
if m != null {
if is_in_list(intermediates, i, used) == 0 {
if dn_equal(bytes.view(cert.issuer)[0..cert.issuer_len], bytes.view(m.subject)[0..m.subject_len]) == 1 {
if verify_cert_signature(cert, m) == 1 {
return i
if m.is_ca == 1 {
if m.has_key_usage == 0 || m.key_cert_sign == 1 {
if dn_equal(bytes.view(cert.issuer)[0..cert.issuer_len], bytes.view(m.subject)[0..m.subject_len]) == 1 {
if verify_cert_signature(cert, m) == 1 {
return i
}
}
}
}
}
Expand Down Expand Up @@ -2049,6 +2119,9 @@ init_leaf(l: *LeafCert) {
l.not_after = null; l.not_after_len = 0
l.dns_names = null
l.ip_addrs = null
l.is_ca = 0
l.has_key_usage = 0
l.key_cert_sign = 0
}

// Free a trust-anchor list built by trust_store_load: each *LeafCert's buffers,
Expand Down
Loading
Loading