diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ce36826..261a3e00e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,8 @@ jobs: run: ./odin test tests/core/crypto/wycheproof -vet -vet-tabs -strict-style -vet-style -vet-cast -warnings-as-errors -disallow-do -o:speed - name: Noise Protocol Framework tests run: ./odin test tests/core/crypto/noise -vet -vet-tabs -strict-style -vet-style -vet-cast -warnings-as-errors -disallow-do -o:speed + - name: X.509 limbo tests + run: ./odin test tests/core/crypto/x509_limbo -vet -vet-tabs -strict-style -vet-style -vet-cast -warnings-as-errors -disallow-do -o:speed - name: Vendor library tests run: ./odin test tests/vendor -all-packages -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -define:ODIN_TEST_FANCY=false -define:ODIN_TEST_FAIL_ON_BAD_MEMORY=true -sanitize:address if: matrix.os != 'macos-15-intel' && matrix.os != 'macos-latest' @@ -250,6 +252,11 @@ jobs: run: | call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" odin test tests/core/crypto/noise -vet -vet-tabs -strict-style -vet-style -vet-cast -warnings-as-errors -disallow-do -o:speed -microarch:native + - name: X.509 limbo tests + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + odin test tests/core/crypto/x509_limbo -vet -vet-tabs -strict-style -vet-style -vet-cast -warnings-as-errors -disallow-do -o:speed -microarch:native - name: Vendor library tests shell: cmd run: | diff --git a/core/crypto/ecdsa/ecdsa_asn1.odin b/core/crypto/ecdsa/ecdsa_asn1.odin index 74c9d65e6..0bad6a263 100644 --- a/core/crypto/ecdsa/ecdsa_asn1.odin +++ b/core/crypto/ecdsa/ecdsa_asn1.odin @@ -1,17 +1,13 @@ package ecdsa +import "core:encoding/asn1" import secec "core:crypto/_weierstrass" -// ASN.1 format ECDSA signatures are`SEQUENCE { r INTEGER, s INTEGER }` -// this implements enough to generate/parse signatures. Eventually when -// we have a full ASN.1 DER library, these routines will be removed. +// ASN.1 ECDSA signatures are `SEQUENCE { r INTEGER, s INTEGER }`. These thin +// wrappers over core:encoding/asn1 generate/parse that structure the DER +// minimal-encoding rules. -@(private="file") -TAG_SEQUENCE :: 0x30 -@(private="file") -TAG_INTEGER :: 0x02 - -@(private,require_results) +@(private, require_results) generate_asn1_sig :: proc(r, s: ^$T, allocator := context.allocator) -> []byte { when T == secec.Scalar_p256r1 { SC_SZ :: secec.SC_SIZE_P256R1 @@ -21,163 +17,35 @@ generate_asn1_sig :: proc(r, s: ^$T, allocator := context.allocator) -> []byte { #panic("crypto/ecdsa: invalid curve") } - INT_TLP :: 3 // tag, tength, (optional) leading zero-byte - encode_uint :: proc(b: []byte) -> []byte { - b := b + r_buf, s_buf: [SC_SZ]byte = ---, --- + secec.sc_bytes(r_buf[:], r) + secec.sc_bytes(s_buf[:], s) - // DER requires minimal encoding. - off := INT_TLP - for v in b[off:] { - if v != 0 { - break - } - off += 1 - } - // If the sign big is set, add a leading zero. - if b[off] & 0x80 == 0x80 { - off -= 1 - b[off] = 0 - } - - // Encode the length (up to 127 octets, adequate for ECDSA). - l := len(b[off:]) - off -= 1 - b[off] = byte(l) - - // Encode the tag - off -= 1 - b[off] = TAG_INTEGER - - return b[off:] + sig, err := asn1.marshal( + asn1.sequence({asn1.integer_unsigned(r_buf[:]), asn1.integer_unsigned(s_buf[:])}), + allocator, + ) + if err != .None { + return nil } - - r_buf, s_buf: [INT_TLP+SC_SZ]byte = ---, --- - secec.sc_bytes(r_buf[INT_TLP:], r) - secec.sc_bytes(s_buf[INT_TLP:], s) - - r_bytes, s_bytes := encode_uint(r_buf[:]), encode_uint(s_buf[:]) - seq_len := len(r_bytes) + len(s_bytes) - - // WARNING: If secp521r1 support is added, this needs to support - // long-form length encoding. - ensure(seq_len <= 127, "BUG: crypto/ecdsa: signature length too large") - b := make([]byte, seq_len + 2, allocator) - b[0] = TAG_SEQUENCE - b[1] = byte(seq_len) - copy(b[2:], r_bytes) - copy(b[2+len(r_bytes):], s_bytes) - - return b + return sig } -@(private,require_results) +@(private, require_results) parse_asn1_sig :: proc(sig: []byte) -> (r, s: []byte, ok: bool) { - read_seq :: proc(b: []byte) -> (v: []byte, rest: []byte, ok: bool) { - b_len := len(b) - if b_len < 3 { - return nil, nil, false - } - if b[0] != TAG_SEQUENCE { - return nil, nil, false - } - seq_len, off: int - if b[1] & 0x80 == 0x80 { - if b[1] != 0x81 || b_len < 4 { // 2-length octets is sufficient for ecdsa. - return nil, nil, false - } - if b[2] & 0x80 == 0x80 || b[3] & 0x80 == 80 { - return nil, nil, false - } - seq_len = int(b[2]) * 127 + int(b[3]) - off = 4 - } else { - seq_len = int(b[1]) - off = 2 - } - if b_len - off < seq_len { - return nil, nil, false - } - return b[off:off+seq_len], b[off+seq_len:], true - } - - read_int :: proc(b: []byte) -> (v: []byte, rest: []byte, ok: bool) { - b_len := len(b) - if b_len < 3 { - return nil, nil, false - } - if b[0] != TAG_INTEGER { - return nil, nil, false - } - v_len := int(b[1]) - if v_len > 0x80 || b_len - 2 < v_len { // 127-bytes max. - return nil, nil, false - } - - return b[2:2+v_len], b[2+v_len:], true - } - - // SEQUENCE - seq_bytes, rest: []byte - seq_bytes, rest, ok = read_seq(sig) - if !ok { - return nil, nil, false - } - if len(rest) != 0 { + cur: asn1.Cursor + asn1.cursor_init(&cur, sig) + seq, e0 := asn1.read_sequence(&cur) + if e0 != .None || asn1.done(&cur) != .None { return nil, nil, false } - // INTEGER (r) - r, rest, ok = read_int(seq_bytes) - if !ok { + // r and s are unsigned; read_unsigned_integer_bytes validates the INTEGER + // and strips the DER sign octet, returning the magnitude as a view of sig. + rb, e1 := asn1.read_unsigned_integer_bytes(&seq) + sb, e2 := asn1.read_unsigned_integer_bytes(&seq) + if e1 != .None || e2 != .None || asn1.done(&seq) != .None { return nil, nil, false } - - // INTEGER (s) - s, rest, ok = read_int(rest) - if !ok { - return nil, nil, false - } - if len(rest) != 0 { - return nil, nil, false - } - - // DER requires a leading 0 if and only if (⟺) the sign bit of the leading byte - // is set to distinguish between positive and negative integers, - // and the minimal length representation. `r` and `s` are always - // going to be unsigned, so we validate malformed DER and strip - // the leading 0 as needed. - fixup_der_uint :: proc(b: []byte) -> ([]byte, bool) { - switch len(b) { - case 0: - // 0 length is invalid - return nil, false - case 1: - // Missing leading zero - if b[0] & 0x80 == 0x80 { - return nil, false - } - case: - if b[0] == 0 { - // Sign bit not set - if b[1] & 0x80 != 0x80 { - return nil, false - } - return b[1:], true - } else if b[0] & 0x80 == 0x80 { - // Missing leading zero - return nil, false - } - } - - return b, true - } - - if r, ok = fixup_der_uint(r); !ok { - return nil, nil, false - } - if s, ok = fixup_der_uint(s); !ok { - return nil, nil, false - } - - return r, s, true + return rb, sb, true } diff --git a/core/crypto/x509/doc.odin b/core/crypto/x509/doc.odin new file mode 100644 index 000000000..3c0f9c95c --- /dev/null +++ b/core/crypto/x509/doc.odin @@ -0,0 +1,99 @@ +/* +X.509 v3 certificate parsing, signature verification, and chain (path) +validation. + +The parser is built on the strict DER reader in core:encoding/asn1 and +is zero-copy where possible: the returned Certificate's byte-slice +fields are views into the input DER, which must outlive it. The few +allocated fields (the extension/SAN tables) are released with +`destroy`. + +Input is DER. To parse a PEM certificate, decode it first with +core:encoding/pem (label "CERTIFICATE") and pass the resulting bytes. + +A successful `parse` means the bytes were well-formed, NOT that the +certificate is valid or trusted. The Certificate carries everything +the verifier needs: `raw_tbs` (the exact byte range a signature covers), +`raw_spki` (the range hashed for tls-server-end-point channel binding, +RFC 5929, and SPKI pinning), and `raw_issuer`/`raw_subject` (for the RFC +5280 binary-comparison rule). + +Hostname verification (`verify_hostname`) implements the RFC 6125 +subset modern clients use: subject alternative names only (no +CommonName fallback), with at most one wildcard as the entire +left-most label. + +Trust is established by `verify_chain`, which builds a path from a leaf +to a supplied trust anchor through supplied intermediates and checks, +for each certificate, validity, signature, name chaining, and the CA / +keyCertSign / pathLenConstraint rules; `verify_signature` exposes the +single-edge signature check on its own. + +LIMITATIONS: + + - Signature verification covers RSA PKCS#1 v1.5 and RSA-PSS + (SHA-256/384/512), ECDSA P-256/P-384, and Ed25519. These paths return + .Unsupported_Algorithm: SHA-1 (deprecated and rejected, RFC 9155), ECDSA + P-521 (effectively dead in web PKI), and RSA-PSS naming a digest or MGF + this package does not recognize. + - Name constraints (RFC 5280 4.2.1.10) are enforced for the dNSName and + iPAddress forms: a CA's permitted/excluded subtrees are checked against + every subordinate certificate's SANs, regardless of the extension's + criticality. A NameConstraints that uses any other base form + (directoryName, rfc822Name, URI, otherName), a minimum/maximum, or that + is malformed cannot be fully evaluated, so the whole chain is rejected + (fail closed) rather than accepted unchecked. NOT enforced: the RFC 5280 + rule that the extension be critical, and dNSName syntax validation (a + leading-period constraint is accepted, as OpenSSL does). + - REVOCATION IS NOT CHECKED. verify_chain performs NO CRL or OCSP + revocation checking. Callers that need revocation (e.g. TLS clients) + MUST supply it separately (OCSP stapling, CRLite, …). + - Certificate policies / policy constraints are not evaluated, and + there is no Public Suffix List: a (CABF-forbidden) wildcard such as + "*.com" would match "host.com". As a backstop, verify_chain still + fails closed on any uninterpreted CRITICAL extension + (.Unhandled_Critical_Extension). + - EKU is checked only when opts.required_eku is set, and then by RFC + 5280 semantics: a certificate with no EKU extension is unrestricted + (it is not required to assert the purpose); a certificate that DOES + assert EKU must include the purpose, enforced across the leaf and + every intermediate (EKU nesting). Leaf KeyUsage is not checked + against the intended protocol use. + +Parsing is deliberately lenient wherever strictness is a validation +concern rather than a structural one. Exception: Parser rejects +duplicate extension OIDs (Duplicate_Extension, RFC 5280 section 4.2). + + - Only dNSName and iPAddress subject alternative names are decoded + (into `dns_names` / `ip_addresses`). Other GeneralName forms (URI, + rfc822Name, directoryName, otherName) are skipped; the raw SAN + extension is still available via `extensions`. + - Only the extensions path validation needs are decoded + (BasicConstraints, KeyUsage, ExtKeyUsage, SubjectAltName, + Subject/Authority Key Identifier; NameConstraints is decoded at + verification time). All others (AIA, CRL distribution points, + certificate policies, …) are left raw in `extensions`. + - Subject and issuer are kept as raw DER (`raw_subject` / `raw_issuer`), + which is what name chaining compares (the RFC 5280 binary rule). The + attributes (CN, O, …) are decoded on demand by `parse_dn`, not at parse + time; `dn_get` / `dn_string` read them out and `serial_string` formats + the serial. + - Unsupported public-key curves yield Public_Key_Algorithm.Unknown. + - Non-conformant-but-extractable values are preserved: negative or + over-long serials, and validity dates far in the future. Validity + is stored as core:time.Time, which tops out near year 2262; dates + beyond that (the RFC 5280 "99991231235959Z" no-expiration sentinel) + saturate to that bound at parse time rather than failing, so they + read as "effectively never expires". + - Per-extension criticality rules (e.g. that subjectKeyIdentifier be + non-critical) are left to the caller via `Extension.critical`, and + a critical extension this package does not understand sets + `unhandled_critical` rather than failing the parse. + + +See: +- [[ https://www.rfc-editor.org/rfc/rfc5280 ]] +- [[ https://www.rfc-editor.org/rfc/rfc6125 ]] +- [[ https://www.rfc-editor.org/rfc/rfc5929 ]] +*/ +package x509 diff --git a/core/crypto/x509/ext.odin b/core/crypto/x509/ext.odin new file mode 100644 index 000000000..fbff6d88b --- /dev/null +++ b/core/crypto/x509/ext.odin @@ -0,0 +1,160 @@ +package x509 + +import "core:encoding/asn1" + +// Extension encoders. Each returns a complete DER Extension +// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING } +// with extnValue carrying the encoded value (the inverse of the matching +// _parse_known_extension branch). The returned slice is allocated; +// a certificate builder splices these into the extensions [3] list via +// asn1.raw. critical=false is omitted per DEFAULT FALSE. + +// marshal_ext_basic_constraints encodes id-ce-basicConstraints: +// BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER OPTIONAL } +// cA is emitted only when `is_ca`; pathLenConstraint only when is_ca and +// `max_path_len` >= 0 (a negative value means "absent"). +@(require_results) +marshal_ext_basic_constraints :: proc(is_ca: bool, max_path_len: int, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) { + vals: [2]asn1.Value + path_buf: [8]byte + n := 0 + if is_ca { + vals[n] = asn1.boolean(true) + n += 1 + if max_path_len >= 0 { + vals[n] = asn1.integer_unsigned(_uint_be(max_path_len, path_buf[:])) + n += 1 + } + } + return _marshal_extension(_OID_EXT_BASIC_CONSTRAINTS, critical, asn1.sequence(vals[:n]), allocator) +} + +// marshal_ext_key_usage encodes id-ce-keyUsage as a KeyUsage BIT STRING, +// minimally (trailing zero bits dropped, per DER named bit strings). +@(require_results) +marshal_ext_key_usage :: proc(usage: Key_Usage, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) { + hi := -1 + for bit in Key_Usage_Bit { + if bit in usage { + hi = int(bit) // bits enumerate ascending, so the last present is the highest + } + } + buf: [3]byte // unused-bits octet + up to 2 payload octets (9 named bits) + content: []byte + if hi < 0 { + content = buf[:1] // no bits set: empty bit string, 0 unused + } else { + nbytes := hi / 8 + 1 + buf[0] = byte(nbytes * 8 - (hi + 1)) // unused-bits count + for bit in Key_Usage_Bit { + i := int(bit) + if i <= hi && bit in usage { + buf[1 + i / 8] |= 0x80 >> uint(i % 8) + } + } + content = buf[:1 + nbytes] + } + return _marshal_extension(_OID_EXT_KEY_USAGE, critical, asn1.primitive(asn1.universal(.Bit_String), content), allocator) +} + +// marshal_ext_ext_key_usage encodes id-ce-extKeyUsage: +// ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId +// in EKU_Bit order. +@(require_results) +marshal_ext_ext_key_usage :: proc(eku: Ext_Key_Usage, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) { + purposes: [7]asn1.Value + n := 0 + for bit in EKU_Bit { + if bit in eku { + purposes[n] = asn1.object_identifier(_eku_oid(bit)) + n += 1 + } + } + return _marshal_extension(_OID_EXT_EXT_KEY_USAGE, critical, asn1.sequence(purposes[:n]), allocator) +} + +// marshal_ext_san encodes id-ce-subjectAltName: +// GeneralNames ::= SEQUENCE OF GeneralName +// emitting dNSName [2] IA5String entries (in order) followed by iPAddress [7] +// OCTET STRING entries. IP values are the raw 4- or 16-octet address. +@(require_results) +marshal_ext_san :: proc(dns_names: []string, ip_addresses: [][]byte, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) { + n := len(dns_names) + len(ip_addresses) + names, merr := make([]asn1.Value, n, allocator) // dynamic count: scaffolding, freed below + if merr != nil { + return nil, .Allocation_Failed + } + defer delete(names, allocator) + + i := 0 + for d in dns_names { + names[i] = asn1.context_primitive(2, transmute([]byte)d) // [2] IMPLICIT IA5String + i += 1 + } + for ip in ip_addresses { + names[i] = asn1.context_primitive(7, ip) // [7] IMPLICIT OCTET STRING + i += 1 + } + return _marshal_extension(_OID_EXT_SAN, critical, asn1.sequence(names[:]), allocator) +} + +// _marshal_extension wraps an extension value tree as a complete Extension. +// `value`'s borrowed backing lives in the caller's frame, which is active for +// the duration of this synchronous call, so the encode below sees it intact. +@(private, require_results) +_marshal_extension :: proc(oid: []byte, critical: bool, value: asn1.Value, allocator := context.allocator) -> (der: []byte, err: Error) { + out: []byte + merr: asn1.Error + if critical { + out, merr = asn1.marshal( + asn1.sequence({asn1.object_identifier(oid), asn1.boolean(true), asn1.octet_string_wrap({value})}), + allocator, + ) + } else { + out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid), asn1.octet_string_wrap({value})}), allocator) + } + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// _uint_be writes the minimal big-endian magnitude of a non-negative int into +// buf and returns the slice (empty for zero, which integer_unsigned encodes as 0). +@(private) +_uint_be :: proc(v: int, buf: []byte) -> []byte { + if v <= 0 { + return buf[:0] + } + n := 0 + x := v + for x > 0 { + n += 1 + x >>= 8 + } + for i in 0 ..< n { + buf[n - 1 - i] = byte(v >> uint(8 * i)) + } + return buf[:n] +} + +@(private) +_eku_oid :: proc(bit: EKU_Bit) -> []byte { + switch bit { + case .Server_Auth: + return _OID_EKU_SERVER_AUTH + case .Client_Auth: + return _OID_EKU_CLIENT_AUTH + case .Code_Signing: + return _OID_EKU_CODE_SIGNING + case .Email_Protection: + return _OID_EKU_EMAIL_PROTECTION + case .Time_Stamping: + return _OID_EKU_TIME_STAMPING + case .OCSP_Signing: + return _OID_EKU_OCSP_SIGNING + case .Any: + return _OID_EKU_ANY + } + return nil +} diff --git a/core/crypto/x509/marshal.odin b/core/crypto/x509/marshal.odin new file mode 100644 index 000000000..dd24da01d --- /dev/null +++ b/core/crypto/x509/marshal.odin @@ -0,0 +1,438 @@ +package x509 + +import "core:encoding/asn1" +import "core:time" + +// Distinguished-name attribute types the DN builder knows by name. `Other` +// carries any attribute outside this set via DN_Attribute.oid (used by +// parse_dn for attributes it does not recognize by name). +DN_Attribute_Type :: enum { + Common_Name, // CN, 2.5.4.3 + Country, // C, 2.5.4.6 + Locality, // L, 2.5.4.7 + State_Or_Province, // ST, 2.5.4.8 + Organization, // O, 2.5.4.10 + Organizational_Unit, // OU, 2.5.4.11 + Serial_Number, // 2.5.4.5 + Other, // any other attribute; its type OID is in `oid` +} + +// DN_Attribute is one relative distinguished name: a type and its value. When +// `type` is `Other`, `oid` holds the raw attribute-type OID content octets. +// Values are emitted as UTF8String, except Country and Serial_Number which +// are PrintableString (X.520), the policy RFC 5280 section 4.1.2.4 advises. +DN_Attribute :: struct { + type: DN_Attribute_Type, + value: string, + oid: []byte, // meaningful only when type == Other +} + +@(rodata, private) +_OID_AT_CN := []byte{0x55, 0x04, 0x03} +@(rodata, private) +_OID_AT_C := []byte{0x55, 0x04, 0x06} +@(rodata, private) +_OID_AT_L := []byte{0x55, 0x04, 0x07} +@(rodata, private) +_OID_AT_ST := []byte{0x55, 0x04, 0x08} +@(rodata, private) +_OID_AT_O := []byte{0x55, 0x04, 0x0A} +@(rodata, private) +_OID_AT_OU := []byte{0x55, 0x04, 0x0B} +@(rodata, private) +_OID_AT_SERIAL := []byte{0x55, 0x04, 0x05} + +// marshal_dn encodes `attrs` as a DER Name (RDNSequence): one single-valued +// RelativeDistinguishedName per attribute, in the given order. The returned +// slice is the caller's to free; the attribute value bytes are copied into +// it, so `attrs` need not outlive the call. +@(require_results) +marshal_dn :: proc(attrs: []DN_Attribute, allocator := context.allocator) -> (der: []byte, err: Error) { + n := len(attrs) + // Value-tree scaffolding, freed once the bytes are produced. Pre-sized so + // the sub-slices handed to set()/sequence() never move before the encode. + rdns, e1 := make([]asn1.Value, n, allocator) // one SET per attribute + if e1 != nil { + return nil, .Allocation_Failed + } + defer delete(rdns, allocator) + atvs, e2 := make([]asn1.Value, n, allocator) // the AttributeTypeAndValue SEQUENCE + if e2 != nil { + return nil, .Allocation_Failed + } + defer delete(atvs, allocator) + pairs, e3 := make([]asn1.Value, 2 * n, allocator) // {type OID, value} per attribute + if e3 != nil { + return nil, .Allocation_Failed + } + defer delete(pairs, allocator) + + for a, i in attrs { + pairs[2 * i] = asn1.object_identifier(_dn_oid(a)) + pairs[2 * i + 1] = asn1.primitive(_dn_string_tag(a.type), transmute([]byte)a.value) + atvs[i] = asn1.sequence(pairs[2 * i:2 * i + 2]) + rdns[i] = asn1.set(atvs[i:i + 1]) + } + + out, merr := asn1.marshal(asn1.sequence(rdns), allocator) + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +@(private) +_dn_oid :: proc(a: DN_Attribute) -> []byte { + switch a.type { + case .Common_Name: + return _OID_AT_CN + case .Country: + return _OID_AT_C + case .Locality: + return _OID_AT_L + case .State_Or_Province: + return _OID_AT_ST + case .Organization: + return _OID_AT_O + case .Organizational_Unit: + return _OID_AT_OU + case .Serial_Number: + return _OID_AT_SERIAL + case .Other: + return a.oid + } + return nil +} + +@(private) +_dn_string_tag :: proc(type: DN_Attribute_Type) -> asn1.Tag { + #partial switch type { + case .Country, .Serial_Number: + return asn1.universal(.Printable_String) + } + return asn1.universal(.UTF8_String) +} + +@(rodata, private) +_DER_INT_ZERO := []byte{0x00} + +// marshal_csr_info encodes the CertificationRequestInfo, the to-be-signed +// portion of a PKCS#10 CSR (RFC 2986): version v1, the subject DN, the subject +// public key, and the attributes set. Sign the returned bytes and pass them +// with the signature to marshal_csr. The slice is the caller's to free; +// `subject`, `key`, and `extensions` need not outlive the call. +// +// `extensions` is a list of pre-encoded Extension DER (from the marshal_ext_* +// helpers); when non-empty it is requested via a single PKCS#9 +// extensionRequest attribute (the standard way a CSR asks the CA to place +// extensions, SANs, key usage in the issued certificate). Pass nil for the +// empty attributes set. +@(require_results) +marshal_csr_info :: proc(subject: []DN_Attribute, key: Public_Key, extensions: [][]byte = nil, allocator := context.allocator) -> (cri_der: []byte, err: Error) { + dn := marshal_dn(subject, allocator) or_return + defer delete(dn, allocator) + spki := marshal_spki(key, allocator) or_return + defer delete(spki, allocator) + attrs := _marshal_csr_attributes(extensions, allocator) or_return + defer delete(attrs, allocator) + + // CertificationRequestInfo ::= SEQUENCE { version, subject, subjectPKInfo, [0] attributes } + out, merr := asn1.marshal( + asn1.sequence( + { + asn1.integer_raw(_DER_INT_ZERO), // version v1 (0) + asn1.raw(dn), // subject Name + asn1.raw(spki), // subjectPublicKeyInfo + asn1.raw(attrs), // attributes [0] + }, + ), + allocator, + ) + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// _marshal_csr_attributes encodes the CertificationRequestInfo attributes +// field, [0] IMPLICIT SET OF Attribute. With no extensions it is the empty +// set (A0 00); otherwise it carries a single PKCS#9 extensionRequest attribute +// Attribute ::= SEQUENCE { type extensionRequest, values SET { Extensions } } +// whose value is the Extensions SEQUENCE OF Extension built from `extensions`. +@(private, require_results) +_marshal_csr_attributes :: proc(extensions: [][]byte, allocator := context.allocator) -> (der: []byte, err: Error) { + out: []byte + merr: asn1.Error + if len(extensions) == 0 { + out, merr = asn1.marshal(asn1.context_explicit(0, {}), allocator) + } else { + // Pre-sized scaffolding for the Extensions SEQUENCE OF, kept alive + // (never resized) through the marshal below. + ext_raws, e := make([]asn1.Value, len(extensions), allocator) + if e != nil { + return nil, .Allocation_Failed + } + defer delete(ext_raws, allocator) + for ext, i in extensions { + ext_raws[i] = asn1.raw(ext) + } + out, merr = asn1.marshal( + asn1.context_explicit( + 0, + {asn1.sequence({asn1.object_identifier(_OID_EXT_REQUEST), asn1.set({asn1.sequence(ext_raws[:])})})}, + ), + allocator, + ) + } + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// marshal_csr wraps a (separately signed) CertificationRequestInfo into a +// complete PKCS#10 CertificationRequest. `signature` is the raw signature +// value over `cri_der`, a DER ECDSA-Sig-Value for ECDSA, the 64-byte value +// for Ed25519, and `signature_algorithm` selects the matching +// AlgorithmIdentifier. The slice is the caller's to free. +@(require_results) +marshal_csr :: proc(cri_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (csr_der: []byte, err: Error) { + // CertificationRequest ::= SEQUENCE { CRI, signatureAlgorithm, signature BIT STRING } + return _marshal_signed(cri_der, signature_algorithm, signature, allocator) +} + +// _marshal_signed wraps an already-encoded body (a CertificationRequestInfo +// or a TBSCertificate) with its signature algorithm and signature BIT STRING: +// SEQUENCE { body, AlgorithmIdentifier, BIT STRING signature } +// the common shape of PKCS#10 CSRs and X.509 certificates. +@(private, require_results) +_marshal_signed :: proc(body_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (der: []byte, err: Error) { + oid, null_params, ok := _sig_alg_identifier(signature_algorithm) + if !ok { + return nil, .Unsupported_Algorithm + } + out: []byte + merr: asn1.Error + if null_params { + out, merr = asn1.marshal( + asn1.sequence( + { + asn1.raw(body_der), + asn1.sequence({asn1.object_identifier(oid), asn1.null()}), + asn1.bit_string_octets(signature), + }, + ), + allocator, + ) + } else { + out, merr = asn1.marshal( + asn1.sequence( + {asn1.raw(body_der), asn1.sequence({asn1.object_identifier(oid)}), asn1.bit_string_octets(signature)}, + ), + allocator, + ) + } + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// _marshal_alg_id encodes a standalone AlgorithmIdentifier (NULL params for +// RSA PKCS#1, absent for ECDSA/EdDSA) for embedding in a TBSCertificate. +@(private, require_results) +_marshal_alg_id :: proc(signature_algorithm: Signature_Algorithm, allocator := context.allocator) -> (der: []byte, err: Error) { + oid, null_params, ok := _sig_alg_identifier(signature_algorithm) + if !ok { + return nil, .Unsupported_Algorithm + } + out: []byte + merr: asn1.Error + if null_params { + out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid), asn1.null()}), allocator) + } else { + out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid)}), allocator) + } + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// Maps a signature algorithm to its AlgorithmIdentifier OID and whether the +// parameters field is an explicit NULL (RSA PKCS#1) or absent (ECDSA, EdDSA). +@(private) +_sig_alg_identifier :: proc(alg: Signature_Algorithm) -> (oid: []byte, null_params: bool, ok: bool) { + #partial switch alg { + case .RSA_SHA256: + return _OID_SIG_RSA_SHA256, true, true + case .RSA_SHA384: + return _OID_SIG_RSA_SHA384, true, true + case .RSA_SHA512: + return _OID_SIG_RSA_SHA512, true, true + case .ECDSA_SHA256: + return _OID_SIG_ECDSA_SHA256, false, true + case .ECDSA_SHA384: + return _OID_SIG_ECDSA_SHA384, false, true + case .ECDSA_SHA512: + return _OID_SIG_ECDSA_SHA512, false, true + case .Ed25519: + return _OID_ED25519, false, true + } + return nil, false, false +} + +@(rodata, private) +_DER_INT_V3 := []byte{0x02} // Version v3 (value 2) + +// _marshal_extensions_field encodes the TBSCertificate extensions field: +// [3] EXPLICIT Extensions, Extensions ::= SEQUENCE OF Extension +// from a list of pre-encoded Extension DER, or nil when there are none (the +// field is OPTIONAL, so an empty list omits it entirely). +@(private, require_results) +_marshal_extensions_field :: proc(extensions: [][]byte, allocator := context.allocator) -> (der: []byte, err: Error) { + if len(extensions) == 0 { + return nil, .None + } + raws, merr := make([]asn1.Value, len(extensions), allocator) + if merr != nil { + return nil, .Allocation_Failed + } + defer delete(raws, allocator) + for e, i in extensions { + raws[i] = asn1.raw(e) + } + out, ferr := asn1.marshal(asn1.context_explicit(3, {asn1.sequence(raws[:])}), allocator) + if ferr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// TBS_Certificate gathers the fields of a TBSCertificate to encode. `issuer` +// and `subject` are RDNSequences; `serial` is the serialNumber's unsigned +// magnitude; `extensions` is a list of pre-encoded Extension DER (from the +// marshal_ext_* helpers), embedded in order. +TBS_Certificate :: struct { + serial: []byte, + signature_algorithm: Signature_Algorithm, + issuer: []DN_Attribute, + not_before: time.Time, + not_after: time.Time, + subject: []DN_Attribute, + public_key: Public_Key, + extensions: [][]byte, +} + +// marshal_tbs_certificate encodes a v3 TBSCertificate (RFC 5280 section 4.1), +// the to-be-signed portion of a certificate. Sign the returned bytes and pass +// them with the signature to marshal_certificate. The slice is the caller's to +// free; the inputs need not outlive the call. +@(require_results) +marshal_tbs_certificate :: proc(tbs: TBS_Certificate, allocator := context.allocator) -> (der: []byte, err: Error) { + issuer_dn := marshal_dn(tbs.issuer, allocator) or_return + defer delete(issuer_dn, allocator) + subject_dn := marshal_dn(tbs.subject, allocator) or_return + defer delete(subject_dn, allocator) + spki := marshal_spki(tbs.public_key, allocator) or_return + defer delete(spki, allocator) + sig_alg := _marshal_alg_id(tbs.signature_algorithm, allocator) or_return + defer delete(sig_alg, allocator) + ext_field := _marshal_extensions_field(tbs.extensions, allocator) or_return + defer delete(ext_field, allocator) + + // raw() splices the independently-marshalled pieces in place; raw(nil) for + // an absent extensions field contributes nothing. + out, merr := asn1.marshal( + asn1.sequence( + { + asn1.context_explicit(0, {asn1.integer_raw(_DER_INT_V3)}), // version [0] EXPLICIT v3 + asn1.integer_unsigned(tbs.serial), // serialNumber + asn1.raw(sig_alg), // signature AlgorithmIdentifier + asn1.raw(issuer_dn), // issuer + asn1.sequence({asn1.time(tbs.not_before), asn1.time(tbs.not_after)}), // validity + asn1.raw(subject_dn), // subject + asn1.raw(spki), // subjectPublicKeyInfo + asn1.raw(ext_field), // extensions [3] (absent when empty) + }, + ), + allocator, + ) + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} + +// marshal_certificate wraps a (separately signed) TBSCertificate into a +// complete X.509 Certificate. `signature_algorithm` must match the one inside +// the TBS (RFC 5280 section 4.1.1.2). The slice is the caller's to free. +@(require_results) +marshal_certificate :: proc(tbs_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (der: []byte, err: Error) { + return _marshal_signed(tbs_der, signature_algorithm, signature, allocator) +} + +// Public_Key holds the subject public-key material to encode into a +// SubjectPublicKeyInfo, mirroring the fields parse() extracts onto a +// Certificate: rsa_n/rsa_e (unsigned magnitudes) for RSA; ec_point for +// ECDSA (the uncompressed point 0x04||X||Y) and Ed25519 (the 32-byte key). +Public_Key :: struct { + algorithm: Public_Key_Algorithm, + rsa_n: []byte, + rsa_e: []byte, + ec_point: []byte, +} + +// marshal_spki encodes `key` as a DER SubjectPublicKeyInfo, the inverse of +// the SPKI decoding in parse(); the returned slice is the caller's to free. +// Unknown / unsupported key algorithms yield .Unsupported_Algorithm. +@(require_results) +marshal_spki :: proc(key: Public_Key, allocator := context.allocator) -> (der: []byte, err: Error) { + out: []byte + merr: asn1.Error + switch key.algorithm { + case .RSA: + // SEQUENCE { SEQUENCE { OID rsaEncryption, NULL }, BIT STRING { RSAPublicKey } } + out, merr = asn1.marshal( + asn1.sequence( + { + asn1.sequence({asn1.object_identifier(_OID_KEY_RSA), asn1.null()}), + asn1.bit_string_wrap({asn1.sequence({asn1.integer_unsigned(key.rsa_n), asn1.integer_unsigned(key.rsa_e)})}), + }, + ), + allocator, + ) + case .ECDSA_P256, .ECDSA_P384, .ECDSA_P521: + curve_oid: []byte + #partial switch key.algorithm { + case .ECDSA_P256: + curve_oid = _OID_CURVE_P256 + case .ECDSA_P384: + curve_oid = _OID_CURVE_P384 + case .ECDSA_P521: + curve_oid = _OID_CURVE_P521 + } + // SEQUENCE { SEQUENCE { OID ecPublicKey, OID namedCurve }, BIT STRING point } + out, merr = asn1.marshal( + asn1.sequence( + { + asn1.sequence({asn1.object_identifier(_OID_KEY_EC), asn1.object_identifier(curve_oid)}), + asn1.bit_string_octets(key.ec_point), + }, + ), + allocator, + ) + case .Ed25519: + // SEQUENCE { SEQUENCE { OID Ed25519 }, BIT STRING key } (no params, RFC 8410) + out, merr = asn1.marshal( + asn1.sequence({asn1.sequence({asn1.object_identifier(_OID_ED25519)}), asn1.bit_string_octets(key.ec_point)}), + allocator, + ) + case .Unknown: + return nil, .Unsupported_Algorithm + } + if merr != .None { + return nil, .Allocation_Failed + } + return out, .None +} diff --git a/core/crypto/x509/name.odin b/core/crypto/x509/name.odin new file mode 100644 index 000000000..a8238e2ac --- /dev/null +++ b/core/crypto/x509/name.odin @@ -0,0 +1,198 @@ +package x509 + +import "core:bytes" +import "core:encoding/asn1" +import "core:strings" + +// Consumer-facing helpers for reading the primitives `parse` leaves as raw DER: +// distinguished-name (Name) decoding, plus a serial-number display formatter. +// These are conveniences over the raw fields, path validation does not need +// them, and `raw_subject` / `raw_issuer` remain available for the RFC 5280 +// binary-comparison rule (which is what issuer/subject matching uses). + +// parse_dn decodes a DER Name (an RDNSequence, e.g. cert.raw_subject or +// cert.raw_issuer) into its attributes, the inverse of marshal_dn. Attributes +// outside the recognized set (see DN_Attribute_Type) come back as `Other` with +// their type OID in `oid`. The returned slice is the caller's to free; every +// `value` (and `oid`) is a VIEW into `der`, which must outlive the result. +// +// Values are taken as the raw content octets of the attribute value: correct +// for the UTF8String / PrintableString / IA5String forms certificates use in +// practice, but Teletex/BMP/Universal strings are NOT transcoded (returned as +// their raw bytes). +@(require_results) +parse_dn :: proc(der: []byte, allocator := context.allocator) -> (attrs: []DN_Attribute, err: Error) { + cur: asn1.Cursor + asn1.cursor_init(&cur, der) + seq, e := asn1.read_sequence(&cur) + if e != .None || asn1.done(&cur) != .None { + return nil, .Malformed + } + + // Count AttributeTypeAndValue entries first (an RDN is a SET OF, usually one + // element), so the result is exact-sized. + count := 0 + { + tmp := seq + for !asn1.is_empty(&tmp) { + rdn, re := asn1.read_set(&tmp) + if re != .None { + return nil, .Malformed + } + for !asn1.is_empty(&rdn) { + if _, ae := asn1.read_sequence(&rdn); ae != .None { + return nil, .Malformed + } + count += 1 + } + } + } + if count == 0 { + return nil, .None // an empty Name is valid + } + + out, merr := make([]DN_Attribute, count, allocator) + if merr != nil { + return nil, .Allocation_Failed + } + i := 0 + for !asn1.is_empty(&seq) { + rdn, re := asn1.read_set(&seq) + if re != .None { + delete(out, allocator) + return nil, .Malformed + } + for !asn1.is_empty(&rdn) { + atv, ae := asn1.read_sequence(&rdn) + oid, oe := asn1.read_oid(&atv) + _, val, ve := asn1.read_any(&atv) + if ae != .None || oe != .None || ve != .None || asn1.done(&atv) != .None { + delete(out, allocator) + return nil, .Malformed + } + t, known := _dn_type_from_oid(oid) + out[i] = DN_Attribute { + type = t, + value = string(val), + } + if !known { + out[i].oid = oid + } + i += 1 + } + } + return out[:i], .None +} + +// dn_get returns the value of the first attribute of `type` (e.g. .Common_Name), +// and whether one was present. +dn_get :: proc(attrs: []DN_Attribute, type: DN_Attribute_Type) -> (value: string, ok: bool) { + for a in attrs { + if a.type == type { + return a.value, true + } + } + return "", false +} + +// dn_string renders `attrs` as an RFC 4514 string ("CN=leaf,O=Acme,C=US"): +// RDNs in reverse order, short names for the recognized attributes and the +// dotted OID for `Other`, with RFC 4514 section 2.4 special characters escaped. +// The returned string is the caller's to free. +@(require_results) +dn_string :: proc(attrs: []DN_Attribute, allocator := context.allocator) -> string { + b := strings.builder_make(allocator) + for i := len(attrs) - 1; i >= 0; i -= 1 { + a := attrs[i] + if i != len(attrs) - 1 { + strings.write_byte(&b, ',') + } + switch a.type { + case .Common_Name: + strings.write_string(&b, "CN") + case .Country: + strings.write_string(&b, "C") + case .Locality: + strings.write_string(&b, "L") + case .State_Or_Province: + strings.write_string(&b, "ST") + case .Organization: + strings.write_string(&b, "O") + case .Organizational_Unit: + strings.write_string(&b, "OU") + case .Serial_Number: + strings.write_string(&b, "serialNumber") + case .Other: + if s, e := asn1.oid_to_string(a.oid, context.temp_allocator); e == .None { + strings.write_string(&b, s) + } else { + strings.write_byte(&b, '?') + } + } + strings.write_byte(&b, '=') + _dn_escape(&b, a.value) + } + return strings.to_string(b) +} + +// serial_string formats the certificate serial as upper-case colon-separated +// hex ("07:44:76:…"). The serial is an opaque identifier (up to 20 octets) +// Allocated String +@(require_results) +serial_string :: proc(cert: ^Certificate, allocator := context.allocator) -> string { + HEX := "0123456789ABCDEF" + b := strings.builder_make(allocator) + for octet, i in cert.serial { + if i != 0 { + strings.write_byte(&b, ':') + } + strings.write_byte(&b, HEX[octet >> 4]) + strings.write_byte(&b, HEX[octet & 0x0F]) + } + return strings.to_string(b) +} + +// _dn_type_from_oid maps an attribute-type OID to a DN_Attribute_Type, the +// inverse of _dn_oid +@(private) +_dn_type_from_oid :: proc(oid: []byte) -> (type: DN_Attribute_Type, is_known_oid: bool) { + switch { + case bytes.equal(oid, _OID_AT_CN): + return .Common_Name, true + case bytes.equal(oid, _OID_AT_C): + return .Country, true + case bytes.equal(oid, _OID_AT_L): + return .Locality, true + case bytes.equal(oid, _OID_AT_ST): + return .State_Or_Province, true + case bytes.equal(oid, _OID_AT_O): + return .Organization, true + case bytes.equal(oid, _OID_AT_OU): + return .Organizational_Unit, true + case bytes.equal(oid, _OID_AT_SERIAL): + return .Serial_Number, true + } + return .Other, false +} + +// _dn_escape writes `s` into `b` with the RFC 4514 section 2.4 escapes: a +// leading '#' or space and a trailing space are escaped, as are the characters +// " + , ; < > \ and the NUL byte. +@(private) +_dn_escape :: proc(b: ^strings.Builder, s: string) { + for i in 0 ..< len(s) { + c := s[i] + lead := i == 0 && (c == ' ' || c == '#') + trail := i == len(s) - 1 && c == ' ' + switch c { + case '"', '+', ',', ';', '<', '>', '\\', 0x00: + strings.write_byte(b, '\\') + strings.write_byte(b, c) + case: + if lead || trail { + strings.write_byte(b, '\\') + } + strings.write_byte(b, c) + } + } +} diff --git a/core/crypto/x509/name_constraints.odin b/core/crypto/x509/name_constraints.odin new file mode 100644 index 000000000..bb1d2dcfa --- /dev/null +++ b/core/crypto/x509/name_constraints.odin @@ -0,0 +1,363 @@ +package x509 + +import "core:bytes" +import "core:encoding/asn1" +import "core:strings" + +// Name-constraint processing (RFC 5280 section 4.2.1.10 + the section 6.1.4 +// path-validation checks), scoped to the dNSName and iPAddress GeneralName +// forms. A NameConstraints extension that uses any other base form +// (directoryName, rfc822Name, uniformResourceIdentifier, otherName, …), a +// non-zero minimum, a maximum, or that fails to decode causes the whole chain +// to be rejected (fail closed): we never accept a constraint we cannot fully +// evaluate. + +// GeneralName context tags used in NameConstraints (X.509 GeneralName CHOICE). +@(private) +_GN_OTHER_NAME :: 0 // otherName [0] (constructed) +@(private) +_GN_RFC822 :: 1 // rfc822Name [1] +@(private) +_GN_DNS :: 2 // dNSName [2] +@(private) +_GN_DIR :: 4 // directoryName [4] (constructed) +@(private) +_GN_URI :: 6 // uniformResourceIdentifier [6] +@(private) +_GN_IP :: 7 // iPAddress [7] + +// _check_name_constraints enforces every NameConstraints extension in `chain` +// (leaf at index 0, trust anchor last). Each CA's constraints apply to every +// certificate below it; checking each CA independently against each +// subordinate yields the RFC 5280 permitted=intersection / excluded=union +// semantics without accumulator state. Returns true when the chain is +// acceptable, false when a name is forbidden (or a constraint cannot be +// evaluated, in which case we reject rather than guess). +@(private) +_check_name_constraints :: proc(chain: []^Certificate) -> bool { + // RFC 5280 4.2.1.10: NameConstraints MUST appear only in a CA certificate. + // A non-CA end entity that carries it is malformed — reject the chain. + if _, leaf_has := _find_name_constraints(chain[0]); leaf_has && !chain[0].is_ca { + return false + } + // ci walks the CAs (anchor down to the first intermediate above the leaf); + // the leaf (index 0) never constrains. + for ci := len(chain) - 1; ci >= 1; ci -= 1 { + nc, has := _find_name_constraints(chain[ci]) + if !has { + continue + } + if !_nc_decidable(nc) { + return false // a form/feature we cannot evaluate: fail closed + } + for sub := ci - 1; sub >= 0; sub -= 1 { + c := chain[sub] + // RFC 5280 section 6.1.4(b): constraints are not applied to a + // self-issued intermediate, only to the final (leaf) certificate. + if sub != 0 && bytes.equal(c.raw_subject, c.raw_issuer) { + continue + } + if !_names_permitted(nc, c) { + return false + } + } + } + return true +} + +// _find_name_constraints returns the raw extnValue DER of the cert's +// NameConstraints extension, if present. +@(private) +_find_name_constraints :: proc(cert: ^Certificate) -> (der: []byte, ok: bool) { + for ext in cert.extensions { + if bytes.equal(ext.oid, _OID_EXT_NAME_CONSTRAINTS) { + return ext.value, true + } + } + return nil, false +} + +// _names_permitted checks a subordinate certificate's dNSName and iPAddress +// SANs against one CA's NameConstraints: each name form, when the CA lists +// permitted subtrees for it, must match at least one; and no name may match an +// excluded subtree. +@(private) +_names_permitted :: proc(nc: []byte, sub: ^Certificate) -> bool { + for dns in sub.dns_names { + if _nc_section_has(nc, false, _GN_DNS) && !_nc_dns_match(nc, false, dns) { + return false + } + if _nc_dns_match(nc, true, dns) { + return false + } + } + for ip in sub.ip_addresses { + if _nc_section_has(nc, false, _GN_IP) && !_nc_ip_match(nc, false, ip) { + return false + } + if _nc_ip_match(nc, true, ip) { + return false + } + } + return true +} + +// _nc_decidable structurally validates a NameConstraints and reports whether we +// can fully evaluate it. It returns false, so the caller fails closed, when the +// extension is malformed (an element other than permittedSubtrees [0] / +// excludedSubtrees [1], neither section present, or an empty GeneralSubtrees, +// which SIZE (1..MAX) forbids), or when a subtree uses a base form other than +// dNSName / iPAddress or carries a minimum/maximum (barred by the RFC 5280 +// profile). Only a NameConstraints whose every subtree is a bare +// dNSName/iPAddress is decidable. +@(private) +_nc_decidable :: proc(nc: []byte) -> bool { + cur: asn1.Cursor + asn1.cursor_init(&cur, nc) + seq, e := asn1.read_sequence(&cur) + if e != .None || asn1.done(&cur) != .None { + return false + } + sections := 0 + for !asn1.is_empty(&seq) { + tag, content, re := asn1.read_any(&seq) + if re != .None { + return false + } + // The only permitted members are permittedSubtrees [0] / excludedSubtrees + // [1]; a NULL or any other element (CABF 7.1.2.5.2) is malformed. + if tag.class != .Context_Specific || (tag.number != 0 && tag.number != 1) { + return false + } + sections += 1 + if !_nc_section_wellformed(content) { + return false + } + } + return sections > 0 +} + +// _nc_section_wellformed checks one GeneralSubtrees body: at least one subtree +// (SIZE (1..MAX)), every base a bare dNSName/iPAddress, no minimum/maximum. +@(private) +_nc_section_wellformed :: proc(content: []byte) -> bool { + cur: asn1.Cursor + asn1.cursor_init(&cur, content) + count := 0 + for !asn1.is_empty(&cur) { + gs, e := asn1.read_sequence(&cur) + if e != .None { + return false + } + count += 1 + tag, _, be := asn1.read_any(&gs) + if be != .None || tag.class != .Context_Specific { + return false + } + if tag.number != _GN_DNS && tag.number != _GN_IP { + return false // directoryName / rfc822 / URI / otherName / … + } + // minimum [0] / maximum [1] must both be absent (RFC 5280 profile). + if asn1.done(&gs) != .None { + return false + } + } + return count > 0 +} + +// _nc_section returns the raw content octets of the permitted [0] (or excluded +// [1]) GeneralSubtrees, i.e. the concatenation of GeneralSubtree elements. +@(private) +_nc_section :: proc(nc: []byte, excluded: bool) -> (content: []byte, present: bool) { + want := u32(excluded ? 1 : 0) + cur: asn1.Cursor + asn1.cursor_init(&cur, nc) + seq, e := asn1.read_sequence(&cur) + if e != .None { + return nil, false + } + for !asn1.is_empty(&seq) { + tag, c, re := asn1.read_any(&seq) + if re != .None { + return nil, false + } + if tag.class == .Context_Specific && tag.number == want { + return c, true + } + } + return nil, false +} + +// _nc_section_has reports whether the given section contains at least one +// subtree whose base is `form`. +@(private) +_nc_section_has :: proc(nc: []byte, excluded: bool, form: u32) -> bool { + content, present := _nc_section(nc, excluded) + if !present { + return false + } + cur: asn1.Cursor + asn1.cursor_init(&cur, content) + for !asn1.is_empty(&cur) { + gs, e := asn1.read_sequence(&cur) + if e != .None { + return false + } + tag, _, be := asn1.read_any(&gs) + if be != .None { + return false + } + if tag.class == .Context_Specific && tag.number == form { + return true + } + } + return false +} + +// _nc_dns_match reports whether `name` matches any dNSName subtree in the +// given section. +@(private) +_nc_dns_match :: proc(nc: []byte, excluded: bool, name: string) -> bool { + content, present := _nc_section(nc, excluded) + if !present { + return false + } + cur: asn1.Cursor + asn1.cursor_init(&cur, content) + for !asn1.is_empty(&cur) { + gs, e := asn1.read_sequence(&cur) + if e != .None { + return false + } + tag, base, be := asn1.read_any(&gs) + if be != .None { + return false + } + if tag.class == .Context_Specific && tag.number == _GN_DNS { + c := string(base) + // A wildcard SAN "*.B" stands for every "