asn1 DER reader/writer and X.509 reader

This commit is contained in:
kalsprite
2026-06-13 23:40:08 -07:00
parent 47cf0d3f42
commit 4876cf03bf
54 changed files with 4957 additions and 7 deletions

111
core/crypto/x509/doc.odin Normal file
View File

@@ -0,0 +1,111 @@
/*
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:
- RSA & ECDSA P-521 signatures are not implemented in core, these paths
return .Unsupported_Algorithm.
- Name constraints are NOT enforced yet; verify_chain fails CLOSED on
them (a chain through a name-constrained CA is automatically rejected.
- 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.
Name constraints (Future PR):
verify_chain does not yet DECODE name constraints, and it fails CLOSED
on them: any CA, intermediate or trust anchor, that asserts a
nameConstraints extension, critical or not, is refused as an issuer,
so a chain through a name-constrained CA is rejected, never accepted
unchecked. RFC 5280 section 6.1.4(g) requires a validator that
processes name constraints to enforce them regardless of criticality;
until we do, refusing is the only safe stand-in.
Planned order:
1. Enforce dNSName and iPAddress constraints, the forms real
name-constrained CAs almost always use, still failing closed
when a constraint uses a form we do not evaluate (directoryName,
rfc822Name, URI, otherName). A name-form constraint restricts
only names of that form (RFC 5280 section 4.2.1.10), so dNSName
constraints can be checked against dNSName SANs with no
distinguished-name decoding; this recovers the large majority of
name-constrained chains.
2. Full section 4.2.1.10 enforcement: the remaining GeneralName
forms plus distinguished-name parsing and comparison, built and
validated test-first against the x509-limbo / BetterTLS
name-constraints corpus.
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). All others (AIA, CRL
distribution points, certificate policies, name constraints, )
are left raw in `extensions`.
- Subject and issuer are exposed only as raw DER (`raw_subject` /
`raw_issuer`); distinguished-name attribute decoding (CN, O, ) is
not performed.
- 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

634
core/crypto/x509/parse.odin Normal file
View File

@@ -0,0 +1,634 @@
package x509
import "core:bytes"
import "core:encoding/asn1"
/*
Ref: RFC 5280, Section 4.1, wire format:
This grammar is decoded via a cursor over the DER. The contents flatten into
the certificate (see x509.odin).
Certificate / TBSCertificate -> Certificate (flattened; raw, raw_tbs)
signatureAlgorithm + value -> signature_algorithm, signature_oid, signature
Version -> version (int: 1 / 2 / 3)
CertificateSerialNumber -> serial ([]byte, raw INTEGER content)
Validity / Time -> not_before, not_after (time.Time)
Name (issuer / subject) -> raw_issuer, raw_subject ([]byte DER; not decoded)
SubjectPublicKeyInfo -> raw_spki, public_key_algorithm, rsa_n/rsa_e/ec_point
Extension -> Extension struct, in `extensions`
issuer/subjectUniqueID -> skipped (obsolete)
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
extensions [3] EXPLICIT Extensions OPTIONAL
}
Version ::= INTEGER { v1(0), v2(1), v3(2) }
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time }
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime }
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}
Name ::= CHOICE { -- only one possibility for now --
rdnSequence RDNSequence
}
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
AttributeTypeAndValue ::= SEQUENCE {
type AttributeType,
value AttributeValue
}
AttributeType ::= OBJECT IDENTIFIER
AttributeValue ::= ANY -- DEFINED BY AttributeType
DirectoryString ::= CHOICE {
teletexString TeletexString (SIZE (1..MAX)),
printableString PrintableString (SIZE (1..MAX)),
universalString UniversalString (SIZE (1..MAX)),
utf8String UTF8String (SIZE (1..MAX)),
bmpString BMPString (SIZE (1..MAX))
}
*/
// parse decodes one DER certificate. The returned Certificate holds views into `der`, which must outlive it;
// the allocated tables are released with destroy(). Trailing bytes after the certificate are an error.
@(require_results)
parse :: proc(der: []byte, allocator := context.allocator) -> (cert: Certificate, err: Error) {
r: asn1.Cursor
asn1.cursor_init(&r, der)
outer, oerr := asn1.read_sequence(&r)
if oerr != .None || asn1.done(&r) != .None {
return {}, .Malformed
}
cert.raw = der[:r.pos]
// tbsCertificate: capture the full element (header included);
tbs_start := outer.pos
tbs, terr := asn1.read_sequence(&outer)
if terr != .None {
return {}, .Malformed
}
cert.raw_tbs = outer.data[tbs_start:outer.pos]
// signatureAlgorithm + signatureValue.
sig_oid, _, serr := _read_algorithm_identifier(&outer)
if serr != .None {
return {}, .Malformed
}
cert.signature_oid = sig_oid
cert.signature_algorithm = _signature_algorithm(sig_oid)
sig_bits, sberr := asn1.read_bit_string_octets(&outer)
if sberr != .None || asn1.done(&outer) != .None {
return {}, .Malformed
}
cert.signature = sig_bits
// ---- TBSCertificate ----
// [0] EXPLICIT version (DEFAULT v1).
cert.version = 1
vr, has_version, verr := asn1.read_explicit(&tbs, 0)
if verr != .None {
return {}, .Malformed
}
if has_version {
v, vierr := asn1.read_i64(&vr)
if vierr != .None || asn1.done(&vr) != .None {
return {}, .Malformed
}
if v < 0 || v > 2 {
return {}, .Unsupported_Version
}
cert.version = int(v) + 1
}
// The serial is read as a raw INTEGER (not unsigned): RFC 5280 requires it to be positive, but non-conformant CAs issue negative
// serials and rejecting them is a validation policy, not a parsing one. The two's-complement content is preserved verbatim.
serial, snerr := asn1.read_integer_bytes(&tbs)
if snerr != .None {
return {}, .Malformed
}
cert.serial = serial
// signature must match the outer signatureAlgorithm per RFC 5280 section 4.1.1.2.
tbs_sig_oid, _, tserr := _read_algorithm_identifier(&tbs)
if tserr != .None {
return {}, .Malformed
}
if !bytes.equal(tbs_sig_oid, sig_oid) {
return {}, .Malformed
}
// issuer
issuer_start := tbs.pos
if _, ierr := asn1.read_sequence(&tbs); ierr != .None {
return {}, .Malformed
}
cert.raw_issuer = tbs.data[issuer_start:tbs.pos]
// validity
validity, vderr := asn1.read_sequence(&tbs)
if vderr != .None {
return {}, .Invalid_Validity
}
nb, nberr := asn1.read_time(&validity)
na, naerr := asn1.read_time(&validity)
if nberr != .None || naerr != .None || asn1.done(&validity) != .None {
return {}, .Invalid_Validity
}
cert.not_before = nb
cert.not_after = na
// subject
subject_start := tbs.pos
if _, suberr := asn1.read_sequence(&tbs); suberr != .None {
return {}, .Malformed
}
cert.raw_subject = tbs.data[subject_start:tbs.pos]
// subjectPublicKeyInfo, full element preserved for hashing
spki_start := tbs.pos
spki, sperr := asn1.read_sequence(&tbs)
if sperr != .None {
return {}, .Malformed
}
cert.raw_spki = tbs.data[spki_start:tbs.pos]
if kerr := _parse_spki(&cert, &spki); kerr != .None {
return {}, kerr
}
// issuerUniqueID / subjectUniqueID - obsolete; skip if present
for number in u32(1) ..= u32(2) {
if asn1.is_empty(&tbs) {
break
}
tag, perr := asn1.peek_tag(&tbs)
if perr != .None {
return {}, .Malformed
}
if tag.class == .Context_Specific && tag.number == number {
if asn1.skip(&tbs) != .None {
return {}, .Malformed
}
}
}
// [3] EXPLICIT extensions.
cert.max_path_len = -1
er, has_exts, eerr := asn1.read_explicit(&tbs, 3)
if eerr != .None || asn1.done(&tbs) != .None {
return {}, .Malformed
}
if has_exts {
if cert.version != 3 {
return {}, .Malformed
}
if xerr := _parse_extensions(&cert, &er, allocator); xerr != .None {
destroy(&cert, allocator)
return {}, xerr
}
}
return cert, .None
}
// Internals.
// _read_algorithm_identifier reads SEQUENCE { OID, params ANY OPTIONAL }, returning the OID content and the raw parameter element
// (nil when absent).
@(private)
_read_algorithm_identifier :: proc(
r: ^asn1.Cursor,
) -> (
oid: []byte,
params: []byte,
err: asn1.Error,
) {
alg, aerr := asn1.read_sequence(r)
if aerr != .None {
return nil, nil, aerr
}
oid, err = asn1.read_oid(&alg)
if err != .None {
return nil, nil, err
}
if !asn1.is_empty(&alg) {
params_start := alg.pos
if serr := asn1.skip(&alg); serr != .None {
return nil, nil, serr
}
params = alg.data[params_start:alg.pos]
}
if derr := asn1.done(&alg); derr != .None {
return nil, nil, derr
}
return oid, params, .None
}
@(private)
_signature_algorithm :: proc(oid: []byte) -> Signature_Algorithm {
switch {
case bytes.equal(oid, _OID_SIG_RSA_SHA256):
return .RSA_SHA256
case bytes.equal(oid, _OID_SIG_ECDSA_SHA256):
return .ECDSA_SHA256
case bytes.equal(oid, _OID_SIG_RSA_SHA384):
return .RSA_SHA384
case bytes.equal(oid, _OID_SIG_RSA_SHA512):
return .RSA_SHA512
case bytes.equal(oid, _OID_SIG_ECDSA_SHA384):
return .ECDSA_SHA384
case bytes.equal(oid, _OID_SIG_ECDSA_SHA512):
return .ECDSA_SHA512
case bytes.equal(oid, _OID_ED25519):
return .Ed25519
case bytes.equal(oid, _OID_SIG_RSA_PSS):
return .RSA_PSS
case bytes.equal(oid, _OID_SIG_RSA_SHA1):
return .RSA_SHA1
}
return .Unknown
}
@(private)
_parse_spki :: proc(cert: ^Certificate, spki: ^asn1.Cursor) -> Error {
key_oid, key_params, aerr := _read_algorithm_identifier(spki)
if aerr != .None {
return .Malformed
}
key_bits, kberr := asn1.read_bit_string_octets(spki)
if kberr != .None || asn1.done(spki) != .None {
return .Malformed
}
switch {
case bytes.equal(key_oid, _OID_KEY_RSA):
cert.public_key_algorithm = .RSA
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
kr: asn1.Cursor
asn1.cursor_init(&kr, key_bits)
rsa, rerr := asn1.read_sequence(&kr)
if rerr != .None || asn1.done(&kr) != .None {
return .Malformed
}
n, nerr := asn1.read_unsigned_integer_bytes(&rsa)
e, eerr := asn1.read_unsigned_integer_bytes(&rsa)
if nerr != .None || eerr != .None || asn1.done(&rsa) != .None {
return .Malformed
}
cert.rsa_n = n
cert.rsa_e = e
case bytes.equal(key_oid, _OID_KEY_EC):
// Parameters carry the named curve: OID wrapped in the params element we captured raw
pr: asn1.Cursor
asn1.cursor_init(&pr, key_params)
curve_oid, cerr := asn1.read_oid(&pr)
if cerr != .None || asn1.done(&pr) != .None {
return .Malformed
}
switch {
case bytes.equal(curve_oid, _OID_CURVE_P256):
cert.public_key_algorithm = .ECDSA_P256
case bytes.equal(curve_oid, _OID_CURVE_P384):
cert.public_key_algorithm = .ECDSA_P384
case bytes.equal(curve_oid, _OID_CURVE_P521):
cert.public_key_algorithm = .ECDSA_P521
case:
cert.public_key_algorithm = .Unknown
}
cert.ec_point = key_bits
case bytes.equal(key_oid, _OID_ED25519):
cert.public_key_algorithm = .Ed25519
if len(key_bits) != 32 {
return .Malformed
}
cert.ec_point = key_bits
case:
cert.public_key_algorithm = .Unknown
}
return .None
}
@(private)
_parse_extensions :: proc(
cert: ^Certificate,
er: ^asn1.Cursor,
allocator := context.allocator,
) -> Error {
exts, xerr := asn1.read_sequence(er)
if xerr != .None || asn1.done(er) != .None {
return .Malformed
}
// Find allocation size
count := 0
{
tmp := exts
for !asn1.is_empty(&tmp) {
if asn1.skip(&tmp) != .None {
return .Malformed
}
count += 1
}
}
exts_table, merr := make([]Extension, count, allocator)
if merr != nil {
return .Allocation_Failed
}
cert.extensions = exts_table
for i in 0 ..< count {
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING }
ext, eerr := asn1.read_sequence(&exts)
if eerr != .None {
return .Malformed
}
oid, oerr := asn1.read_oid(&ext)
if oerr != .None {
return .Malformed
}
critical := false
tag, perr := asn1.peek_tag(&ext)
if perr != .None {
return .Malformed
}
if tag == asn1.universal(.Boolean) {
c, berr := asn1.read_boolean(&ext)
if berr != .None {
return .Malformed
}
critical = c
}
value, verr := asn1.read_octet_string(&ext)
if verr != .None || asn1.done(&ext) != .None {
return .Malformed
}
// RFC 5280 section 4.2: "A certificate MUST NOT include more than one instance of a particular extension."
for j in 0 ..< i {
if bytes.equal(cert.extensions[j].oid, oid) {
return .Duplicate_Extension
}
}
cert.extensions[i] = Extension {
oid = oid,
critical = critical,
value = value,
}
if herr := _parse_known_extension(cert, oid, critical, value, allocator); herr != .None {
return herr
}
}
return .None
}
@(private)
_parse_known_extension :: proc(
cert: ^Certificate,
oid: []byte,
critical: bool,
value: []byte,
allocator := context.allocator,
) -> Error {
vr: asn1.Cursor
asn1.cursor_init(&vr, value)
switch {
case bytes.equal(oid, _OID_EXT_BASIC_CONSTRAINTS):
// BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER OPTIONAL }
bc, err := asn1.read_sequence(&vr)
if err != .None || asn1.done(&vr) != .None {
return .Invalid_Extension
}
if !asn1.is_empty(&bc) {
tag, perr := asn1.peek_tag(&bc)
if perr != .None {
return .Invalid_Extension
}
if tag == asn1.universal(.Boolean) {
ca, berr := asn1.read_boolean(&bc)
if berr != .None {
return .Invalid_Extension
}
cert.is_ca = ca
}
}
if !asn1.is_empty(&bc) {
depth, derr := asn1.read_i64(&bc)
if derr != .None || depth < 0 {
return .Invalid_Extension
}
cert.max_path_len = int(depth)
}
if asn1.done(&bc) != .None {
return .Invalid_Extension
}
cert.basic_constraints_valid = true
case bytes.equal(oid, _OID_EXT_KEY_USAGE):
bits, unused, err := asn1.read_bit_string(&vr)
if err != .None || asn1.done(&vr) != .None {
return .Invalid_Extension
}
total := len(bits) * 8 - unused
usage: Key_Usage
for bit in Key_Usage_Bit {
i := int(bit)
if i >= total {
continue
}
if bits[i / 8] & (0x80 >> uint(i % 8)) != 0 {
usage += {bit}
}
}
cert.key_usage = usage
cert.has_key_usage = true
case bytes.equal(oid, _OID_EXT_EXT_KEY_USAGE):
seq, err := asn1.read_sequence(&vr)
if err != .None || asn1.done(&vr) != .None {
return .Invalid_Extension
}
for !asn1.is_empty(&seq) {
purpose, perr := asn1.read_oid(&seq)
if perr != .None {
return .Invalid_Extension
}
switch {
case bytes.equal(purpose, _OID_EKU_SERVER_AUTH):
cert.ext_key_usage += {.Server_Auth}
case bytes.equal(purpose, _OID_EKU_CLIENT_AUTH):
cert.ext_key_usage += {.Client_Auth}
case bytes.equal(purpose, _OID_EKU_CODE_SIGNING):
cert.ext_key_usage += {.Code_Signing}
case bytes.equal(purpose, _OID_EKU_EMAIL_PROTECTION):
cert.ext_key_usage += {.Email_Protection}
case bytes.equal(purpose, _OID_EKU_TIME_STAMPING):
cert.ext_key_usage += {.Time_Stamping}
case bytes.equal(purpose, _OID_EKU_OCSP_SIGNING):
cert.ext_key_usage += {.OCSP_Signing}
case bytes.equal(purpose, _OID_EKU_ANY):
cert.ext_key_usage += {.Any}
case:
cert.eku_has_unknown = true
}
}
cert.has_ext_key_usage = true
case bytes.equal(oid, _OID_EXT_SAN):
return _parse_san(cert, &vr, allocator)
case bytes.equal(oid, _OID_EXT_SUBJECT_KEY_ID):
ski, err := asn1.read_octet_string(&vr)
if err != .None || asn1.done(&vr) != .None {
return .Invalid_Extension
}
cert.subject_key_id = ski
case bytes.equal(oid, _OID_EXT_AUTHORITY_KEY_ID):
// AuthorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] IMPLICIT OCTET STRING OPTIONAL, ... }
aki, err := asn1.read_sequence(&vr)
if err != .None || asn1.done(&vr) != .None {
return .Invalid_Extension
}
if !asn1.is_empty(&aki) {
tag, perr := asn1.peek_tag(&aki)
if perr != .None {
return .Invalid_Extension
}
if tag == asn1.context_specific(0, false) {
kid, kerr := asn1.expect(&aki, tag)
if kerr != .None {
return .Invalid_Extension
}
cert.authority_key_id = kid
}
}
// authorityCertIssuer [1] and authorityCertSerialNumber [2] are intentionally not decoded: keyIdentifier is the form used in
// practice, and AKI is only a path-building hint (issuers are matched by DN + signature), so the other fields carry no
// validation weight here.
case:
if critical {
cert.unhandled_critical = true
}
}
return .None
}
// GeneralNames ::= SEQUENCE OF GeneralName; we extract dNSName ([2] IA5String) and iPAddress ([7] OCTET STRING).
@(private)
_parse_san :: proc(cert: ^Certificate, vr: ^asn1.Cursor, allocator := context.allocator) -> Error {
names, err := asn1.read_sequence(vr)
if err != .None || asn1.done(vr) != .None {
return .Invalid_Extension
}
dns_count, ip_count := 0, 0
{
tmp := names
for !asn1.is_empty(&tmp) {
tag, content, gerr := asn1.read_any(&tmp)
if gerr != .None || tag.class != .Context_Specific {
return .Invalid_Extension
}
switch tag.number {
case 2:
dns_count += 1
case 7:
if len(content) != 4 && len(content) != 16 {
return .Invalid_Extension
}
ip_count += 1
}
}
}
// On allocation failure the caller (parse) unwinds every table via destroy.
if dns_count > 0 {
dns_table, derr := make([]string, dns_count, allocator)
if derr != nil {
return .Allocation_Failed
}
cert.dns_names = dns_table
}
if ip_count > 0 {
ip_table, ierr := make([][]byte, ip_count, allocator)
if ierr != nil {
return .Allocation_Failed
}
cert.ip_addresses = ip_table
}
di, ii := 0, 0
for !asn1.is_empty(&names) {
tag, content, gerr := asn1.read_any(&names)
if gerr != .None {
return .Invalid_Extension
}
switch tag.number {
case 2:
cert.dns_names[di] = string(content)
di += 1
case 7:
cert.ip_addresses[ii] = content
ii += 1
}
}
return .None
}

View File

@@ -0,0 +1,485 @@
package x509
import "core:bytes"
import "core:crypto/ecdsa"
import "core:crypto/ed25519"
import "core:crypto/hash"
import "core:net"
import "core:strings"
import "core:time"
// valid_at returns true if and only if (⟺) `now` falls within the
// certificate's validity window (inclusive on both ends, per RFC 5280
// section 4.1.2.5). Obtain `now` from e.g. time.now().
@(require_results)
valid_at :: proc "contextless" (cert: ^Certificate, now: time.Time) -> bool {
return time.diff(cert.not_before, now) >= 0 && time.diff(now, cert.not_after) >= 0
}
// verify_hostname checks `host` against the certificate's subject
// alternative names per RFC 6125:
//
// - IP-literal hosts match iPAddress SANs by byte equality only.
// - DNS hosts match dNSName SANs case-insensitively; one wildcard is
// permitted as the ENTIRE left-most label of the SAN
// ("*.example.com" matches "a.example.com" but never
// "a.b.example.com", "example.com", or partial labels like
// "f*.example.com").
// - The legacy CommonName fallback is not implemented (deprecated
// since RFC 6125).
//
// Returns .None on match, .Hostname_Mismatch when SANs of the right
// kind exist but none match, and .No_SAN when the certificate carries
// no SAN of the queried kind.
@(require_results)
verify_hostname :: proc(cert: ^Certificate, host: string) -> Error {
hostname := strings.trim_suffix(host, ".")
// IP literal?
if addr := net.parse_address(hostname); addr != nil {
if len(cert.ip_addresses) == 0 {
return .No_SAN
}
addr_bytes: [16]byte
addr_len: int
switch a in addr {
case net.IP4_Address:
tmp := a
copy(addr_bytes[:], tmp[:])
addr_len = 4
case net.IP6_Address:
tmp := transmute([16]byte)a
copy(addr_bytes[:], tmp[:])
addr_len = 16
}
for san in cert.ip_addresses {
if bytes.equal(san, addr_bytes[:addr_len]) {
return .None
}
}
return .Hostname_Mismatch
}
if len(cert.dns_names) == 0 {
return .No_SAN
}
for san in cert.dns_names {
if _match_hostname(san, hostname) {
return .None
}
}
return .Hostname_Mismatch
}
// _match_hostname implements the RFC 6125 section 6.4.3 subset described on verify_hostname.
@(private)
_match_hostname :: proc(pattern, host: string) -> bool {
p := strings.trim_suffix(pattern, ".")
if len(p) == 0 || len(host) == 0 {
return false
}
if !strings.has_prefix(p, "*.") {
return strings.equal_fold(p, host)
}
// Wildcard: "*." + base. The host's first label is consumed by the wildcard; the
// remainder must equal the base, and the wildcard must not swallow more than one label.
base := p[2:]
dot := strings.index_byte(host, '.')
if dot < 1 {
// No label boundary (or empty first label); a bare host can't match a wildcard.
return false
}
rest := host[dot + 1:]
if len(rest) == 0 {
return false
}
// The base itself may not contain another wildcard.
if strings.index_byte(base, '*') >= 0 {
return false
}
return strings.equal_fold(base, rest)
}
// ============================================================
// Signature verification and chain (path) validation.
//
// verify_signature checks that `cert`'s signature was produced by the
// private key matching `issuer`'s public key, over cert.raw_tbs (the
// signed TBSCertificate). It checks ONLY the cryptographic signature —
// not validity periods, names, basic constraints, or that `issuer` is
// actually authorized to issue `cert`; verify_chain does all of that.
//
// Returns .None on a good signature, .Signature_Invalid on a bad one,
// and .Unsupported_Algorithm when the signature algorithm (RSA, ECDSA
// P-521) or the issuer key type is not implemented here.
@(require_results)
verify_signature :: proc(cert: ^Certificate, issuer: ^Certificate) -> Error {
#partial switch cert.signature_algorithm {
case .RSA_SHA1, .RSA_SHA256, .RSA_SHA384, .RSA_SHA512, .RSA_PSS:
return .Unsupported_Algorithm
case .ECDSA_SHA256, .ECDSA_SHA384, .ECDSA_SHA512:
curve: ecdsa.Curve
#partial switch issuer.public_key_algorithm {
case .ECDSA_P256:
curve = .SECP256R1
case .ECDSA_P384:
curve = .SECP384R1
case:
// P-521 (unsupported by the verifier) or a non-EC issuer key paired with an ECDSA signature.
return .Unsupported_Algorithm
}
h := _hash_for_ecdsa(cert.signature_algorithm)
pub: ecdsa.Public_Key
defer ecdsa.public_key_clear(&pub)
if !ecdsa.public_key_set_bytes(&pub, curve, issuer.ec_point) {
return .Signature_Invalid
}
if !ecdsa.verify_asn1(&pub, h, cert.raw_tbs, cert.signature) {
return .Signature_Invalid
}
return .None
case .Ed25519:
if issuer.public_key_algorithm != .Ed25519 {
return .Unsupported_Algorithm
}
pub: ed25519.Public_Key
defer ed25519.public_key_clear(&pub)
if !ed25519.public_key_set_bytes(&pub, issuer.ec_point) {
return .Signature_Invalid
}
if !ed25519.verify(&pub, cert.raw_tbs, cert.signature) {
return .Signature_Invalid
}
return .None
}
return .Unsupported_Algorithm
}
@(private)
_hash_for_ecdsa :: proc "contextless" (s: Signature_Algorithm) -> hash.Algorithm {
#partial switch s {
case .ECDSA_SHA384:
return .SHA384
case .ECDSA_SHA512:
return .SHA512
}
return .SHA256
}
// Verify_Options parameterizes verify_chain.
Verify_Options :: struct {
// Trust anchors. A chain is accepted iff it terminates at one of
// these (matched by name + signature, as ordinary issuers). Usually
// self-signed roots, but any certificate trusted a priori works.
roots: []^Certificate,
// Untrusted intermediates available to bridge the leaf to a root.
// Order does not matter; verify_chain searches them.
intermediates: []^Certificate,
// Reference time for every certificate's validity window.
current_time: time.Time,
// If non-empty, the leaf must pass verify_hostname for this name.
dns_name: string,
// If set, the leaf's ExtKeyUsage must permit this purpose (a leaf
// with no EKU extension is unrestricted and always passes). TLS
// clients pass .Server_Auth.
required_eku: Maybe(EKU_Bit),
}
// _MAX_CHAIN_DEPTH bounds path search depth, to stop cycles among
// mutually-issuing intermediates.
@(private)
_MAX_CHAIN_DEPTH :: 10
// _MAX_SIG_CHECKS caps the total number of signature verifications a
// single verify_chain may perform across its entire path search.
// Guard for path-building denial of service, RFC 4158 section 2.4.2.
@(private)
_MAX_SIG_CHECKS :: 100
// verify_chain builds and validates a certificate path from `leaf` to
// one of opts.roots, using opts.intermediates to bridge the gap. On
// success it returns the verified chain leaf-first (chain[0] == leaf,
// chain[len-1] is the trust anchor); the slice is allocated & freed.
//
// Each non-anchor certificate in the path is checked for:
// - validity at opts.current_time;
// - Signature verifies against the next certificate's key;
// - Name chaining (issuer DN == subject DN);
// For every intermediate issuer:
// - It is a CA (basicConstraints);
// - Is within pathLenConstraint (counting only non-self-issued
// intermediates, per RFC 5280 section 6.1.4);
// - if it declares KeyUsage: permits keyCertSign.
// A certificate carrying a critical extension this package does not
// interpret fails the path closed (see .Unhandled_Critical_Extension).
// When opts.dns_name is set the leaf must pass verify_hostname; when
// opts.required_eku is set the leaf AND every intermediate must permit
// that purpose (e.g. an email-only sub-CA cannot issue a TLS server leaf).
//
// The trust anchor is checked like any issuer (validity, CA /
// keyCertSign, pathLenConstraint, no uninterpreted critical extension),
// except that its own self-signature is not re-verified, since it is
// trusted a priori. An expired or malformed root is therefore rejected;
// resilience to that comes from the search trying every other available
// anchor and intermediate.
//
@(require_results)
verify_chain :: proc(
leaf: ^Certificate,
opts: Verify_Options,
allocator := context.allocator,
) -> (
chain: []^Certificate,
err: Error,
) {
// Leaf-definitive checks
if leaf.unhandled_critical {
return nil, .Unhandled_Critical_Extension
}
if verr := _check_validity(leaf, opts.current_time); verr != .None {
return nil, verr
}
// RSA is identifiable from the leaf's signature algorithm OID, report correctly
if leaf.signature_algorithm == .RSA_SHA1 ||
leaf.signature_algorithm == .RSA_SHA256 ||
leaf.signature_algorithm == .RSA_SHA384 ||
leaf.signature_algorithm == .RSA_SHA512 ||
leaf.signature_algorithm == .RSA_PSS {
return nil, .Unsupported_Algorithm
}
if opts.dns_name != "" {
if herr := verify_hostname(leaf, opts.dns_name); herr != .None {
return nil, herr
}
}
// Reserve the whole path up front: with capacity in hand, the appends below should never reallocate, so single point of OOM risk
acc, aerr := make([dynamic]^Certificate, 0, _MAX_CHAIN_DEPTH + 1, allocator)
if aerr != nil {
return nil, .Allocation_Failed
}
append(&acc, leaf)
saw_unsupported := false
saw_eku_reject := false
opts_local := opts
budget := _MAX_SIG_CHECKS
if _build_to_anchor(leaf, &opts_local, &acc, 0, &saw_unsupported, &saw_eku_reject, &budget) {
// Success: caller owns the returned chain.
// EKU nesting (leaf + every intermediate permits opts.required_eku)
// was enforced as a usability gate during the search itself, so any
// path returned here already satisfies it.
return acc[:], .None
}
delete(acc)
// An unimplemented signature algorithm is a hard capability gap and
// outranks the EKU policy mismatch; both outrank the generic failure.
switch {
case saw_unsupported:
return nil, .Unsupported_Algorithm
case saw_eku_reject:
return nil, .Incompatible_Usage
case:
return nil, .Unknown_Authority
}
}
// Performs a depth-first search for a path from `cert` up to a trust anchor.
// `acc` holds the chain so far, leaf-first and including `cert`; on success
// the matched issuers (ending at the anchor) are appended. `depth` is the
// recursion depth and len(acc) - 1 is the number of intermediates already
// below the next issuer (used for pathLenConstraint). `budget` is the shared
// remaining signature-verification allowance (see _MAX_SIG_CHECKS); when it is
// exhausted the search stops. `saw_unsupported` / `saw_eku_reject` are
// set when a branch was abandoned only because the signature algorithm
// was unimplemented, or because a cert failed the required-EKU check, so
// verify_chain can report the more specific error when no path is found.
@(private)
_build_to_anchor :: proc(
cert: ^Certificate,
opts: ^Verify_Options,
acc: ^[dynamic]^Certificate,
depth: int,
saw_unsupported: ^bool,
saw_eku_reject: ^bool,
budget: ^int,
) -> (found: bool) {
if depth >= _MAX_CHAIN_DEPTH {
return false
}
// EKU nesting: when the caller requires a purpose, the leaf and every
// intermediate must permit it. `cert` here is always the leaf (depth 0)
// or an intermediate being extended through, so anchors stay exempt.
// Enforcing EKU as a usability gate lets the search backtrack and try an
// alternative issuer that does permit the purpose (two same-subject/same-key
// intermediates can differ in EKU); a post-build filter would instead commit
// to whichever path was found first and reject the whole verification.
if eku_ask, ok := opts.required_eku.?; ok {
if !_permits_eku(cert, eku_ask) {
saw_eku_reject^ = true
return false
}
}
// Non-self-issued intermediates already beneath the next issuer, for the pathLenConstraint check (RFC 5280 section 6.1.4)
below := _non_self_issued_below(acc)
// Prefer terminating at a trust anchor. The anchor must have issued `cert` (name chaining + signature) and,
// like any CA, be fit to act as an issuer: valid at `now`, a CA with keyCertSign, within its pathLenConstraint,
// no uninterpreted critical extension. Its own self-signature is NOT re-verified; it is trusted a priori.
for root in opts.roots {
if !_is_issuer_of(root, cert) || !_issuer_usable(root, opts.current_time, below) {
continue
}
if budget^ <= 0 {
return false
}
budget^ -= 1
#partial switch verify_signature(cert, root) {
case .None:
append(acc, root)
return true
case .Unsupported_Algorithm:
saw_unsupported^ = true
case:
// bad signature: not this anchor
}
}
// Otherwise extend through an untrusted intermediate and recurse.
for inter in opts.intermediates {
if inter == cert || _in_chain(acc, inter) {
continue
}
if !_is_issuer_of(inter, cert) || !_issuer_usable(inter, opts.current_time, below) {
continue
}
if budget^ <= 0 {
return false
}
budget^ -= 1
#partial switch verify_signature(cert, inter) {
case .None:
append(acc, inter)
if _build_to_anchor(inter, opts, acc, depth + 1, saw_unsupported, saw_eku_reject, budget) {
return true
}
pop(acc) // backtrack
case .Unsupported_Algorithm:
saw_unsupported^ = true
case:
// bad signature: not this issuer
}
}
return false
}
@(private)
_check_validity :: proc "contextless" (cert: ^Certificate, now: time.Time) -> Error {
if time.diff(now, cert.not_before) > 0 {
return .Not_Yet_Valid
}
if time.diff(cert.not_after, now) > 0 {
return .Expired
}
return .None
}
// Reports whether `issuer` could have issued `cert`: the issuer's
// subject DN must equal cert's issuer DN (RFC 5280 section 6.1
// name chaining, by binary DER comparison). The authority/subject key
// identifiers are NOT used as a filter, RFC 5280 section 4.2.1.1
// (and RFC 4158) make them a non-authoritative path-building hint.
@(private)
_is_issuer_of :: proc(issuer, cert: ^Certificate) -> bool {
return bytes.equal(issuer.raw_subject, cert.raw_issuer)
}
// Counts the non-self-issued intermediates already in the path below
// the next issuer, i.e. everything in `acc` except the leaf (index 0).
// A self-issued certificate (subject DN == issuer DN, used for CA key
// rollover) does not count against pathLenConstraint (RFC 5280 sections
// 4.2.1.9 and 6.1.4(l)).
@(private)
_non_self_issued_below :: proc(acc: ^[dynamic]^Certificate) -> int {
n := 0
// Skip the leaf at index 0; it is the end-entity, not an intermediate.
for i in 1 ..< len(acc) {
c := acc[i]
if !bytes.equal(c.raw_subject, c.raw_issuer) {
n += 1
}
}
return n
}
// Applies the RFC 5280 section 6.1.4 checks to an INTERMEDIATE issuer:
// a CA with (if asserted) keyCertSign, valid at `now`, within its
// pathLenConstraint given `below` non-self-issued intermediates beneath
// it, and with no uninterpreted critical extension.
@(private)
_issuer_usable :: proc(issuer: ^Certificate, now: time.Time, below: int) -> bool {
if issuer.unhandled_critical {
return false
}
// Name constraints are NOT decoded yet. RFC 5280 section 6.1.4(g)
// requires a validator that processes NC to enforce it regardless of
// criticality, so we fail to prevent escapements
if _has_extension(issuer, _OID_EXT_NAME_CONSTRAINTS) {
return false
}
if !issuer.basic_constraints_valid || !issuer.is_ca {
return false
}
if issuer.has_key_usage && .Key_Cert_Sign not_in issuer.key_usage {
return false
}
if issuer.max_path_len >= 0 && below > issuer.max_path_len {
return false
}
if _check_validity(issuer, now) != .None {
return false
}
return true
}
// Reports whether `cert` allows the given Extended Key Usage purpose:
// no EKU extension means unrestricted, anyExtendedKeyUsage
// permits everything, otherwise the purpose must be listed. Applied to
// the leaf and every intermediate for EKU nesting (see verify_chain).
@(private)
_permits_eku :: proc(cert: ^Certificate, ask: EKU_Bit) -> bool {
if !cert.has_ext_key_usage {
return true // no EKU extension: unrestricted
}
if .Any in cert.ext_key_usage {
return true
}
return ask in cert.ext_key_usage
}
@(private)
_has_extension :: proc(cert: ^Certificate, oid: []byte) -> bool {
for ext in cert.extensions {
if bytes.equal(ext.oid, oid) {
return true
}
}
return false
}
@(private)
_in_chain :: proc(acc: ^[dynamic]^Certificate, cert: ^Certificate) -> bool {
for c in acc {
if c == cert {
return true
}
}
return false
}

221
core/crypto/x509/x509.odin Normal file
View File

@@ -0,0 +1,221 @@
package x509
import "core:time"
Error :: enum {
None,
Malformed, // DER-level violation, structural mismatch, or trailing garbage.
Unsupported_Version, // Certificate version beyond v3.
Invalid_Validity, // notBefore/notAfter missing or unparseable.
Invalid_Extension, // A recognized extension's content didn't match its schema.
Duplicate_Extension, // The same extension OID appeared more than once (RFC 5280 section 4.2 forbids this).
Hostname_Mismatch, // Hostname verification: no SAN matched.
No_SAN, // Hostname verification: the certificate has no usable SANs of the queried kind.
Allocation_Failed, // Allocating the extension/SAN tables failed.
// --- verification (verify_signature / verify_chain) ---
Signature_Invalid, // A signature did not verify against the issuer's public key.
Unsupported_Algorithm, // The signature or public-key algorithm is recognized but not implemented here.
Not_Yet_Valid, // A certificate's notBefore is in the future relative to the supplied time.
Expired, // A certificate's notAfter is in the past relative to the supplied time.
Unknown_Authority, // (no issuer found, failed name chaining, validity, CA constraints, or signature verification).
Unhandled_Critical_Extension, // Failed to handle a critical extension, automatic rejection
Incompatible_Usage, // Lacks EKU, or EKU not authorized
}
// Signature_Algorithm covers the PKIX signature algorithms a client
// encounters in practice. RSA_PSS parameters are not interpreted; the
// raw AlgorithmIdentifier is preserved on the Certificate.
Signature_Algorithm :: enum {
Unknown,
RSA_SHA1, // obsolete; parsed for identification only
RSA_SHA256,
RSA_SHA384,
RSA_SHA512,
RSA_PSS,
ECDSA_SHA256,
ECDSA_SHA384,
ECDSA_SHA512,
Ed25519,
}
// Public_Key_Algorithm identifies the certificate's subject public key
// type. Unknown covers key algorithms (or EC curves) this package does
// not decode; the SubjectPublicKeyInfo bytes remain available in
// raw_spki.
Public_Key_Algorithm :: enum {
Unknown,
RSA,
ECDSA_P256,
ECDSA_P384,
ECDSA_P521,
Ed25519,
}
// Key_Usage bits per RFC 5280 section 4.2.1.3
Key_Usage_Bit :: enum u16 {
Digital_Signature = 0,
Content_Commitment = 1,
Key_Encipherment = 2,
Data_Encipherment = 3,
Key_Agreement = 4,
Key_Cert_Sign = 5,
CRL_Sign = 6,
Encipher_Only = 7,
Decipher_Only = 8,
}
// Key_Usage is the decoded KeyUsage extension bit set.
Key_Usage :: bit_set[Key_Usage_Bit;u16]
// Extended key usage purposes (RFC 5280 section 4.2.1.12) recognized by name;
// unrecognized purposes set `eku_has_unknown`.
EKU_Bit :: enum u8 {
Server_Auth,
Client_Auth,
Code_Signing,
Email_Protection,
Time_Stamping,
OCSP_Signing,
Any,
}
// Ext_Key_Usage is the decoded set of recognized ExtKeyUsage purposes;
// unrecognized purposes set Certificate.eku_has_unknown.
Ext_Key_Usage :: bit_set[EKU_Bit;u8]
// Extension is one raw entry from the TBS extensions list. `oid` is
// the OID content octets; `value` the extnValue OCTET STRING content.
Extension :: struct {
oid: []byte,
critical: bool,
value: []byte,
}
// Certificate is a parsed X.509 v3 certificate.
// Byte-slice fields are views into the input DER (which must outlive the Certificate)
// The dns_names / ip_addresses / extensions slices are *allocated* (their elements still view the DER) and released by destroy.
Certificate :: struct {
// Raw views into the input DER.
raw: []byte, // the whole Certificate element
raw_tbs: []byte, // TBSCertificate, header included, the signed bytes
raw_issuer: []byte, // issuer Name element (RFC 5280 binary comparison)
raw_subject: []byte, // subject Name element
raw_spki: []byte, // SubjectPublicKeyInfo element; hash for tls-server-end-point / SPKI pinning
version: int, // 1, 2, or 3
// Certificate serial number as the raw DER INTEGER content (minimal two's-complement).
// It is an opaque identifier, compare and display by these bytes. A positive serial whose top
// bit is set carries a leading 0x00 sign octet (as openssl shows it); a serial of 0 is the single octet {0x00}. RFC 5280 requires
// serials to be positive and <= 20 octets, but non-conformant (negative, zero, or over-long) serials are preserved
serial: []byte,
signature_algorithm: Signature_Algorithm,
signature_oid: []byte, // OID content octets
signature: []byte, // signatureValue payload (whole octets)
// Validity bounds. time.Time is i64 nanoseconds and tops out near year 2262; X.509 dates beyond that (notably the RFC 5280
// "99991231235959Z" no-expiration sentinel) saturate to that bound at parse time, so they compare as "effectively never expires"
// rather than overflowing. See asn1's _time_from_unix.
not_before: time.Time,
not_after: time.Time,
public_key_algorithm: Public_Key_Algorithm,
// RSA: modulus and exponent magnitudes.
rsa_n: []byte,
rsa_e: []byte,
// ECDSA: the uncompressed point (0x04 || X || Y); Ed25519: the 32-byte key.
ec_point: []byte,
// BasicConstraints (basic_constraints_valid reports presence).
basic_constraints_valid: bool,
is_ca: bool,
max_path_len: int, // -1 when absent
has_key_usage: bool,
key_usage: Key_Usage,
has_ext_key_usage: bool,
ext_key_usage: Ext_Key_Usage,
eku_has_unknown: bool,
subject_key_id: []byte,
authority_key_id: []byte,
dns_names: []string, // ALLOCATED
ip_addresses: [][]byte, // ALLOCATED
// Every extension, in order, including ones this package does not interpret.
extensions: []Extension, // ALLOCATED
// True if a critical extension other than the ones interpreted
// here was present. RFC 5280 requires a relying party to reject
// such a certificate at validation time; parsing still succeeds so
// the caller can inspect.
//
// The specific unhandled OIDs are recoverable by walking `extensions`
// for entries with `critical = true` whose OID is none of the handled
// ones (_OID_EXT_*).
unhandled_critical: bool,
}
destroy :: proc(cert: ^Certificate, allocator := context.allocator) {
delete(cert.dns_names, allocator)
delete(cert.ip_addresses, allocator)
delete(cert.extensions, allocator)
cert^ = {}
}
// PKIX object identifiers as DER content octets, for direct comparison against asn1.read_oid results.
@(rodata, private)
_OID_SIG_RSA_SHA1 := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05} // sha1WithRSAEncryption (1.2.840.113549.1.1.5)
@(rodata, private)
_OID_SIG_RSA_SHA256 := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B} // sha256WithRSAEncryption (1.2.840.113549.1.1.11)
@(rodata, private)
_OID_SIG_RSA_SHA384 := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C} // sha384WithRSAEncryption (1.2.840.113549.1.1.12)
@(rodata, private)
_OID_SIG_RSA_SHA512 := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D} // sha512WithRSAEncryption (1.2.840.113549.1.1.13)
@(rodata, private)
_OID_SIG_RSA_PSS := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0A} // id-RSASSA-PSS (1.2.840.113549.1.1.10)
@(rodata, private)
_OID_SIG_ECDSA_SHA256 := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02} // ecdsa-with-SHA256 (1.2.840.10045.4.3.2)
@(rodata, private)
_OID_SIG_ECDSA_SHA384 := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03} // ecdsa-with-SHA384 (1.2.840.10045.4.3.3)
@(rodata, private)
_OID_SIG_ECDSA_SHA512 := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04} // ecdsa-with-SHA512 (1.2.840.10045.4.3.4)
@(rodata, private)
_OID_ED25519 := []byte{0x2B, 0x65, 0x70} // id-Ed25519 (1.3.101.112), RFC 8410
@(rodata, private)
_OID_KEY_RSA := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01} // rsaEncryption (1.2.840.113549.1.1.1)
@(rodata, private)
_OID_KEY_EC := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01} // id-ecPublicKey (1.2.840.10045.2.1)
@(rodata, private)
_OID_CURVE_P256 := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07} // secp256r1 (1.2.840.10045.3.1.7)
@(rodata, private)
_OID_CURVE_P384 := []byte{0x2B, 0x81, 0x04, 0x00, 0x22} // secp384r1 (1.3.132.0.34)
@(rodata, private)
_OID_CURVE_P521 := []byte{0x2B, 0x81, 0x04, 0x00, 0x23} // secp521r1 (1.3.132.0.35)
@(rodata, private)
_OID_EXT_SUBJECT_KEY_ID := []byte{0x55, 0x1D, 0x0E} // id-ce-subjectKeyIdentifier (2.5.29.14)
@(rodata, private)
_OID_EXT_KEY_USAGE := []byte{0x55, 0x1D, 0x0F} // id-ce-keyUsage (2.5.29.15)
@(rodata, private)
_OID_EXT_SAN := []byte{0x55, 0x1D, 0x11} // id-ce-subjectAltName (2.5.29.17)
@(rodata, private)
_OID_EXT_BASIC_CONSTRAINTS := []byte{0x55, 0x1D, 0x13} // id-ce-basicConstraints (2.5.29.19)
@(rodata, private)
_OID_EXT_NAME_CONSTRAINTS := []byte{0x55, 0x1D, 0x1E} // id-ce-nameConstraints (2.5.29.30)
@(rodata, private)
_OID_EXT_AUTHORITY_KEY_ID := []byte{0x55, 0x1D, 0x23} // id-ce-authorityKeyIdentifier (2.5.29.35)
@(rodata, private)
_OID_EXT_EXT_KEY_USAGE := []byte{0x55, 0x1D, 0x25} // id-ce-extKeyUsage (2.5.29.37)
@(rodata, private)
_OID_EKU_ANY := []byte{0x55, 0x1D, 0x25, 0x00} // anyExtendedKeyUsage (2.5.29.37.0)
@(rodata, private)
_OID_EKU_SERVER_AUTH := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01} // id-kp-serverAuth (1.3.6.1.5.5.7.3.1)
@(rodata, private)
_OID_EKU_CLIENT_AUTH := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02} // id-kp-clientAuth (1.3.6.1.5.5.7.3.2)
@(rodata, private)
_OID_EKU_CODE_SIGNING := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x03} // id-kp-codeSigning (1.3.6.1.5.5.7.3.3)
@(rodata, private)
_OID_EKU_EMAIL_PROTECTION := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x04} // id-kp-emailProtection (1.3.6.1.5.5.7.3.4)
@(rodata, private)
_OID_EKU_TIME_STAMPING := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x08} // id-kp-timeStamping (1.3.6.1.5.5.7.3.8)
@(rodata, private)
_OID_EKU_OCSP_SIGNING := []byte{0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x09} // id-kp-OCSPSigning (1.3.6.1.5.5.7.3.9)

View File

@@ -0,0 +1,72 @@
package asn1
// Tag class from the identifier octet (X.690 section 8.1.2.2).
Class :: enum u8 {
Universal = 0,
Application = 1,
Context_Specific = 2,
Private = 3,
}
// Tag_Number enumerates the UNIVERSAL tag numbers relevant to DER as
// used by PKIX. Context-specific tags carry their number directly in
// Tag.number.
Tag_Number :: enum u32 {
Boolean = 1,
Integer = 2,
Bit_String = 3,
Octet_String = 4,
Null = 5,
Object_Identifier = 6,
Enumerated = 10,
UTF8_String = 12,
Sequence = 16,
Set = 17,
Numeric_String = 18,
Printable_String = 19,
Teletex_String = 20,
IA5_String = 22,
UTC_Time = 23,
Generalized_Time = 24,
Visible_String = 26,
BMP_String = 30,
}
// Tag is a decoded identifier octet (plus high-tag-number form).
Tag :: struct {
class: Class,
constructed: bool,
number: u32,
}
Error :: enum {
None,
Truncated, // The element (or its header) extends past the end of the input.
Invalid_Tag, // Malformed identifier octets (non-minimal high-tag-number form, or a tag number that overflows u32).
Invalid_Length, // Indefinite length, non-minimal length encoding, or a length beyond what this implementation supports.
Unexpected_Tag, // An expect_*/read_* procedure found a different tag than required.
Invalid_Boolean, // BOOLEAN content was not exactly one octet of 0x00 or 0xFF.
Invalid_Integer, // INTEGER content was empty or not minimally encoded.
Integer_Overflow, // INTEGER does not fit the requested machine type.
Negative_Integer, // INTEGER was negative where an unsigned value is required.
Invalid_Bit_String, // BIT STRING content violated X.690 sections 8.6/11.2 (bad unused-bit count, non-zero padding bits, or unused bits where none are permitted).
Invalid_Null, // NULL with non-empty content.
Invalid_Object_Identifier, // OBJECT IDENTIFIER content was empty or not minimally encoded.
Invalid_Time, // UTCTime/GeneralizedTime outside the RFC 5280 DER profile (YYMMDDHHMMSSZ / YYYYMMDDHHMMSSZ, Zulu only, seconds present, no fractional seconds), or an impossible date.
Leftover_Bytes, // done() was called with input remaining.
// An OBJECT IDENTIFIER arc exceeds u64. Arc magnitude is unbounded per X.660 (e.g. 2.25 UUID-derived OIDs carry a 128-bit arc), so
// this is a representation limit of oid_components/oid_to_string, not a malformed input; compare such OIDs by their raw bytes.
Arc_Overflow,
Allocation_Failed, // OOM
Buffer_Too_Small, // encode: the destination slice is shorter than encoded_len.
}
// universal builds a UNIVERSAL-class tag.
universal :: proc "contextless" (number: Tag_Number, constructed := false) -> Tag {
return Tag{class = .Universal, constructed = constructed, number = u32(number)}
}
// context_specific builds a CONTEXT-SPECIFIC-class tag ("[n]").
context_specific :: proc "contextless" (number: u32, constructed := true) -> Tag {
return Tag{class = .Context_Specific, constructed = constructed, number = number}
}

View File

@@ -0,0 +1,624 @@
package asn1
import "core:strings"
import dt "core:time"
// Cursor is a position into DER input, advanced by the read_* procs.
Cursor :: struct {
data: []byte,
pos: int,
}
// Points a Cursor at `data` and rewinds it to the start.
cursor_init :: proc "contextless" (r: ^Cursor, data: []byte) {
r.data = data
r.pos = 0
}
// Returns the number of unconsumed bytes.
remaining :: proc "contextless" (r: ^Cursor) -> int {
return len(r.data) - r.pos
}
// Reports whether the Cursor has been fully consumed.
is_empty :: proc "contextless" (r: ^Cursor) -> bool {
return r.pos >= len(r.data)
}
// Returns Leftover_Bytes if input remains. DER structures are exact: every SEQUENCE walk should end with done() on its sub-cursor.
done :: proc "contextless" (r: ^Cursor) -> Error {
if r.pos < len(r.data) {
return .Leftover_Bytes
}
return .None
}
// Reads one complete TLV element of any tag, returning the tag and a view of the content octets.
read_any :: proc "contextless" (r: ^Cursor) -> (tag: Tag, content: []byte, err: Error) {
tag, err = _read_tag(r)
if err != .None {
return
}
length: int
length, err = _read_length(r)
if err != .None {
return
}
if length > remaining(r) {
err = .Truncated
return
}
content = r.data[r.pos:r.pos + length]
r.pos += length
return
}
// Decodes the next element's tag without consuming anything.
peek_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
tmp := r^
return _read_tag(&tmp)
}
// Consumes one complete element of any tag.
skip :: proc "contextless" (r: ^Cursor) -> Error {
_, _, err := read_any(r)
return err
}
// Reads one element and requires its tag to match exactly.
expect :: proc "contextless" (r: ^Cursor, tag: Tag) -> (content: []byte, err: Error) {
got: Tag
got, content, err = read_any(r)
if err != .None {
return
}
if got != tag {
err = .Unexpected_Tag
}
return
}
// Enters a SEQUENCE, returning a sub-cursor over its content.
read_sequence :: proc "contextless" (r: ^Cursor) -> (seq: Cursor, err: Error) {
content, eerr := expect(r, universal(.Sequence, true))
if eerr != .None {
return {}, eerr
}
return Cursor{data = content}, .None
}
// Enters a SET, returning a sub-cursor over its content.
// NOTE: DER requires SET OF contents to be sorted; this cursor does
// not verify ordering, consumers that care (none in PKIX cert parsing) must check.
read_set :: proc "contextless" (r: ^Cursor) -> (set: Cursor, err: Error) {
content, eerr := expect(r, universal(.Set, true))
if eerr != .None {
return {}, eerr
}
return Cursor{data = content}, .None
}
// Handles `[number] EXPLICIT ... OPTIONAL`: if the next element is the given
// constructed context-specific tag, it is consumed and a sub-cursor over its
// content returned with present=true. Otherwise nothing is consumed.
read_explicit :: proc "contextless" (r: ^Cursor, number: u32) -> (inner: Cursor, present: bool, err: Error) {
if is_empty(r) {
return {}, false, .None
}
tag, perr := peek_tag(r)
if perr != .None {
return {}, false, perr
}
if tag != context_specific(number, true) {
return {}, false, .None
}
content, eerr := expect(r, tag)
if eerr != .None {
return {}, false, eerr
}
return Cursor{data = content}, true, .None
}
// Reads a BOOLEAN. DER: exactly one octet, 0x00 or 0xFF.
read_boolean :: proc "contextless" (r: ^Cursor) -> (value: bool, err: Error) {
content, eerr := expect(r, universal(.Boolean))
if eerr != .None {
return false, eerr
}
if len(content) != 1 {
return false, .Invalid_Boolean
}
switch content[0] {
case 0x00:
return false, .None
case 0xFF:
return true, .None
}
return false, .Invalid_Boolean
}
// Reads an INTEGER and returns the validated, minimally-encoded two's-complement content octets.
read_integer_bytes :: proc "contextless" (r: ^Cursor) -> (content: []byte, err: Error) {
content, err = expect(r, universal(.Integer))
if err != .None {
return
}
err = _check_integer(content)
return
}
// Reads an INTEGER that must fit in an i64.
read_i64 :: proc "contextless" (r: ^Cursor) -> (value: i64, err: Error) {
content, ierr := read_integer_bytes(r)
if ierr != .None {
return 0, ierr
}
if len(content) > 8 {
return 0, .Integer_Overflow
}
if content[0] & 0x80 != 0 {
value = -1 // sign-extend
}
for b in content {
value = value << 8 | i64(b)
}
return value, .None
}
// Reads a non-negative INTEGER and returns its magnitude octets with any leading 0x00
// sign octet stripped, the shape RSA moduli, public exponents, and certificate serials
// are consumed in.
read_unsigned_integer_bytes :: proc "contextless" (r: ^Cursor) -> (magnitude: []byte, err: Error) {
content, ierr := read_integer_bytes(r)
if ierr != .None {
return nil, ierr
}
if content[0] & 0x80 != 0 {
return nil, .Negative_Integer
}
if len(content) > 1 && content[0] == 0x00 {
content = content[1:]
}
return content, .None
}
// Reads a BIT STRING, returning the payload octets and the count of unused trailing bits
// in the final octet. DER: primitive form only, unused count 0..7 (0 if the payload is
// empty), and the unused bits themselves must be zero.
read_bit_string :: proc "contextless" (r: ^Cursor) -> (bits: []byte, unused: int, err: Error) {
content, eerr := expect(r, universal(.Bit_String))
if eerr != .None {
return nil, 0, eerr
}
if len(content) < 1 {
return nil, 0, .Invalid_Bit_String
}
unused = int(content[0])
bits = content[1:]
if unused > 7 {
return nil, 0, .Invalid_Bit_String
}
if len(bits) == 0 && unused != 0 {
return nil, 0, .Invalid_Bit_String
}
if unused > 0 {
mask := byte(1 << uint(unused)) - 1
if bits[len(bits) - 1] & mask != 0 {
return nil, 0, .Invalid_Bit_String
}
}
return bits, unused, .None
}
// Reads a BIT STRING that must be a whole number of octets (unused == 0),
// the only form PKIX uses for SubjectPublicKeyInfo keys and signature values.
read_bit_string_octets :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
bits, unused, berr := read_bit_string(r)
if berr != .None {
return nil, berr
}
if unused != 0 {
return nil, .Invalid_Bit_String
}
return bits, .None
}
// Reads an OCTET STRING (primitive form only).
read_octet_string :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
return expect(r, universal(.Octet_String))
}
// Reads a NULL (content must be empty).
read_null :: proc "contextless" (r: ^Cursor) -> Error {
content, err := expect(r, universal(.Null))
if err != .None {
return err
}
if len(content) != 0 {
return .Invalid_Null
}
return .None
}
// Reads an OBJECT IDENTIFIER and returns a view of its content octets,
// validated for minimal base-128 encoding. The validation is structural
// only: arc magnitude is unbounded per X.660, so PKIX consumers should
// compare these bytes directly against known-OID constants.
// oid_components/oid_to_string decode arcs when needed, reporting
// Arc_Overflow for arcs beyond u64.
read_oid :: proc "contextless" (r: ^Cursor) -> (raw: []byte, err: Error) {
raw, err = expect(r, universal(.Object_Identifier))
if err != .None {
return
}
if len(raw) == 0 {
return nil, .Invalid_Object_Identifier
}
// Validate: each subidentifier is base-128 with minimal encoding
// (no 0x80 lead octet) and terminates (last octet has bit 8 clear).
expect_start := true
for b in raw {
if expect_start && b == 0x80 {
return nil, .Invalid_Object_Identifier
}
expect_start = b & 0x80 == 0
}
if !expect_start {
return nil, .Invalid_Object_Identifier
}
return raw, .None
}
// Times are returned as core:time.Time. time.Time is i64 nanoseconds
// and so tops out near year 2262, while UTCTime/GeneralizedTime reach
// year 9999; dates beyond what time.Time can hold (notably RFC 5280's
// "99991231235959Z" no-well-defined-expiration sentinel) saturate to
// time.Time's bound rather than erroring, so a far-future cert still
// parses and reads as "effectively never expires". See _time_from_unix.
// read_utc_time reads a UTCTime in the RFC 5280 DER profile:
// "YYMMDDHHMMSSZ", with the sliding century window (00-49 → 20xx,
// 50-99 → 19xx).
read_utc_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
content, eerr := expect(r, universal(.UTC_Time))
if eerr != .None {
return {}, eerr
}
if len(content) != 13 || content[12] != 'Z' {
return {}, .Invalid_Time
}
yy, ok := _two_digits(content[0:2])
if !ok {
return {}, .Invalid_Time
}
year := 2000 + yy
if yy >= 50 {
year = 1900 + yy
}
secs := _unix_from_fields(year, content[2:12]) or_return
return _time_from_unix(secs), .None
}
// Reads a GeneralizedTime in the RFC 5280 DER profile: "YYYYMMDDHHMMSSZ", Zulu only, no fractional seconds.
read_generalized_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
content, eerr := expect(r, universal(.Generalized_Time))
if eerr != .None {
return {}, eerr
}
if len(content) != 15 || content[14] != 'Z' {
return {}, .Invalid_Time
}
hi, ok1 := _two_digits(content[0:2])
lo, ok2 := _two_digits(content[2:4])
if !ok1 || !ok2 {
return {}, .Invalid_Time
}
secs := _unix_from_fields(hi * 100 + lo, content[4:14]) or_return
return _time_from_unix(secs), .None
}
// Reads either time form, PKIX Validity uses UTCTime for dates through 2049 and GeneralizedTime from 2050 on.
read_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
tag, perr := peek_tag(r)
if perr != .None {
return {}, perr
}
if tag == universal(.Generalized_Time) {
return read_generalized_time(r)
}
return read_utc_time(r)
}
// OBJECT IDENTIFIER helpers (allocating).
// Decodes validated OID content octets (from read_oid) into their integer arcs,
// e.g. {1, 2, 840, 113549, 1, 1, 1}. Arcs beyond u64 (legal per X.660, see
// Arc_Overflow) are not representable; compare such OIDs by their raw bytes instead.
oid_components :: proc(raw: []byte, allocator := context.allocator) -> (arcs: []u64, err: Error) {
if len(raw) == 0 {
return nil, .Invalid_Object_Identifier
}
count := 1 // the first octet encodes two arcs
for b in raw {
if b & 0x80 == 0 {
count += 1
}
}
out, merr := make([]u64, count, allocator)
if merr != nil {
return nil, .Allocation_Failed
}
idx := 0
acc: u64 = 0
first := true
for b in raw {
if acc > max(u64) >> 7 {
delete(out, allocator)
return nil, .Arc_Overflow
}
acc = acc << 7 | u64(b & 0x7F)
if b & 0x80 != 0 {
continue
}
if first {
// X.690 section 8.19.4: the first subidentifier encodes the first
// two arcs as arc1*40 + arc2 (arc1 limited to 0..2; arc2
// unbounded only when arc1 == 2).
switch {
case acc < 40:
out[idx] = 0
out[idx + 1] = acc
case acc < 80:
out[idx] = 1
out[idx + 1] = acc - 40
case:
out[idx] = 2
out[idx + 1] = acc - 80
}
idx += 2
first = false
} else {
out[idx] = acc
idx += 1
}
acc = 0
}
return out, .None
}
// Renders OID content octets in dotted-decimal form ("1.2.840.113549.1.1.1") for diagnostics.
// The arcs are streamed directly into the result; the only allocation is the returned string.
oid_to_string :: proc(raw: []byte, allocator := context.allocator) -> (str: string, err: Error) {
if len(raw) == 0 {
return "", .Invalid_Object_Identifier
}
sb: strings.Builder
if _, berr := strings.builder_init(&sb, allocator); berr != nil {
return "", .Allocation_Failed
}
defer if err != .None {
strings.builder_destroy(&sb)
}
// Builder writes swallow allocator failures, so tally the written
// vs expected lengths and treat any shortfall as an allocation
// failure (the same defense pem.encode uses).
written, expected := 0, 0
acc: u64 = 0
first := true
for b in raw {
if acc > max(u64) >> 7 {
err = .Arc_Overflow
return "", err
}
acc = acc << 7 | u64(b & 0x7F)
if b & 0x80 != 0 {
continue
}
if first {
// See oid_components for the X.690 section 8.19.4 split of the first subidentifier.
arc1, arc2: u64
switch {
case acc < 40:
arc1, arc2 = 0, acc
case acc < 80:
arc1, arc2 = 1, acc - 40
case:
arc1, arc2 = 2, acc - 80
}
written += strings.write_u64(&sb, arc1)
written += strings.write_byte(&sb, '.')
written += strings.write_u64(&sb, arc2)
expected += _decimal_len(arc1) + 1 + _decimal_len(arc2)
first = false
} else {
written += strings.write_byte(&sb, '.')
written += strings.write_u64(&sb, acc)
expected += 1 + _decimal_len(acc)
}
acc = 0
}
if written != expected {
err = .Allocation_Failed
return "", err
}
return strings.to_string(sb), .None
}
@(private)
_decimal_len :: proc "contextless" (v: u64) -> (n: int) {
n = 1
x := v
for x >= 10 {
x /= 10
n += 1
}
return n
}
@(private)
_read_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
if is_empty(r) {
return {}, .Truncated
}
b := r.data[r.pos]
r.pos += 1
tag.class = Class(b >> 6)
tag.constructed = b & 0x20 != 0
number := u32(b & 0x1F)
if number != 0x1F {
tag.number = number
return tag, .None
}
// High-tag-number form (X.690 section 8.1.2.4): base-128, minimal (first
// octet may not be 0x80), and the resulting number must be >= 31.
number = 0
for i := 0; ; i += 1 {
if is_empty(r) {
return {}, .Truncated
}
nb := r.data[r.pos]
r.pos += 1
if i == 0 && nb == 0x80 {
return {}, .Invalid_Tag
}
if number > (max(u32) >> 7) {
return {}, .Invalid_Tag
}
number = number << 7 | u32(nb & 0x7F)
if nb & 0x80 == 0 {
break
}
}
if number < 0x1F {
return {}, .Invalid_Tag
}
tag.number = number
return tag, .None
}
@(private)
_read_length :: proc "contextless" (r: ^Cursor) -> (length: int, err: Error) {
if is_empty(r) {
return 0, .Truncated
}
b := r.data[r.pos]
r.pos += 1
if b & 0x80 == 0 {
return int(b), .None
}
n := int(b & 0x7F)
if n == 0 {
// 0x80: indefinite length, BER only.
return 0, .Invalid_Length
}
if n > 4 {
// Lengths beyond 2^31 are not plausible inputs here.
return 0, .Invalid_Length
}
if remaining(r) < n {
return 0, .Truncated
}
value := 0
for i in 0 ..< n {
value = value << 8 | int(r.data[r.pos + i])
}
r.pos += n
// DER minimality: no leading zero octet, and the long form may only be used for lengths >= 128.
if r.data[r.pos - n] == 0 || value < 0x80 {
return 0, .Invalid_Length
}
if value < 0 {
return 0, .Invalid_Length
}
return value, .None
}
// Enforces X.690 section 8.3: at least one octet, and minimal (the first nine bits may not be all-zero or all-one).
@(private)
_check_integer :: proc "contextless" (content: []byte) -> Error {
switch len(content) {
case 0:
return .Invalid_Integer
case 1:
return .None
}
if content[0] == 0x00 && content[1] & 0x80 == 0 {
return .Invalid_Integer
}
if content[0] == 0xFF && content[1] & 0x80 != 0 {
return .Invalid_Integer
}
return .None
}
@(private)
_two_digits :: proc "contextless" (b: []byte) -> (value: int, ok: bool) {
if b[0] < '0' || b[0] > '9' || b[1] < '0' || b[1] > '9' {
return 0, false
}
return int(b[0] - '0') * 10 + int(b[1] - '0'), true
}
// Converts Unix seconds to a time.Time, saturating at time.Time's
// representable bounds. time.Time counts i64 nanoseconds, so it tops
// out near year 2262; a far-future X.509 date (notably RFC 5280's
// "99991231235959Z" no-expiration sentinel) saturates to that bound
// rather than overflowing, and so reads as "effectively never".
@(private)
_time_from_unix :: proc "contextless" (secs: i64) -> dt.Time {
NS_PER_SEC :: i64(1_000_000_000)
if secs > max(i64) / NS_PER_SEC {
return dt.Time{_nsec = max(i64)}
}
if secs < min(i64) / NS_PER_SEC {
return dt.Time{_nsec = min(i64)}
}
return dt.Time{_nsec = secs * NS_PER_SEC}
}
// Converts a year plus "MMDDHHMMSS" into seconds since the Unix epoch,
// validating field ranges. Computed directly (via the civil-date algorithm
// below) rather than through time.Time so the whole year 1..9999 range is
// computable before _time_from_unix decides how to represent it.
@(private)
_unix_from_fields :: proc "contextless" (year: int, fields: []byte) -> (unix_seconds: i64, err: Error) {
month, mo_ok := _two_digits(fields[0:2])
day, d_ok := _two_digits(fields[2:4])
hour, h_ok := _two_digits(fields[4:6])
minute, min_ok := _two_digits(fields[6:8])
second, s_ok := _two_digits(fields[8:10])
if !mo_ok || !d_ok || !h_ok || !min_ok || !s_ok {
return 0, .Invalid_Time
}
if month < 1 || month > 12 || day < 1 || day > 31 {
return 0, .Invalid_Time
}
if hour > 23 || minute > 59 || second > 59 {
return 0, .Invalid_Time
}
days := _days_from_civil(i64(year), month, day)
return days * 86400 + i64(hour) * 3600 + i64(minute) * 60 + i64(second), .None
}
// Returns the number of days since 1970-01-01 for a proleptic-Gregorian date
// (Ref: http://howardhinnant.github.io/date_algorithms.html#days_from_civil).
// Exact for any representable year; no epoch-range limit.
@(private)
_days_from_civil :: proc "contextless" (y: i64, m, d: int) -> i64 {
yy := y - (m <= 2 ? 1 : 0)
era := (yy >= 0 ? yy : yy - 399) / 400
yoe := yy - era * 400 // [0, 399]
doy := i64((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1) // [0, 365]
doe := yoe * 365 + yoe / 4 - yoe / 100 + doy // [0, 146096]
return era * 146097 + doe - 719468
}

View File

@@ -0,0 +1,29 @@
/*
Strict DER (Distinguished Encoding Rules) reader and writer for the PKIX
subset of ASN.1, the substrate for X.509 certificates and related structures.
Reader: a `Cursor` over the input; `read_*` procs return VIEWS into it (only
`oid_components` / `oid_to_string` take an allocator). The input must outlive
the results.
Writer: build a declarative tree of `Value` nodes with the constructors, then
`encoded_len` + `encode` (no allocation, into a caller buffer) or `marshal`
(one allocation). Constructors BORROW their inputs, so build and encode
within one expression (or back children with a slice that outlives the call);
the encoded output is self-contained and aliases nothing.
Scope & limitations:
- DER only (no BER/CER); strict (minimal lengths, minimal integers, ...).
- PKIX subset: no typed readers for STRING/REAL/... (walk with `read_any`);
the writer emits low-tag-number identifiers only (tag number <= 30).
Times use core:time.Time per the RFC 5280 DER profile (Zulu, seconds present,
no fractional seconds). time.Time is i64 nanoseconds (tops out near year 2262)
while UTCTime/GeneralizedTime reach 9999; on read, dates beyond that saturate.
See:
- [[ https://www.itu.int/rec/T-REC-X.690 ]]
- [[ https://www.rfc-editor.org/rfc/rfc5280 ]]
*/
package asn1

View File

@@ -0,0 +1,498 @@
package asn1
/*
DER (Distinguished Encoding Rules) writer, the inverse of the cursor
reader, and the encoding substrate for signatures, keys, and certificates.
The model is declarative: build a tree of `Value` nodes with the
constructors below, then turn it into bytes with `encoded_len` + `encode`
(no allocation, into a caller buffer) or `marshal` (one allocation, owned
slice). A SEQUENCE/SET simply holds its children, so length is discovered
by a measure pass rather than back-patched, and `set` can sort its
children (DER SET OF ordering) without disturbing this surface, both are
additive.
Zero-copy, with a lifetime caveat: the constructors BORROW their byte and
child inputs (no copies), so a Value tree is only valid while those inputs
live. In practice build and encode the tree within one expression, or back
its children with a slice/array that outlives the encode call:
// one expression (inputs r, s outlive the call):
out := marshal(sequence({integer_unsigned(r), integer_unsigned(s)})) or_return
DER is canonical by construction: definite minimal-length headers, minimal
INTEGER magnitudes with the sign octet inserted only when required. The
writer emits low-tag-number identifiers only (tag number <= 30), which
covers all of PKIX; high-tag-number form is a future addition.
See:
- [[ https://www.itu.int/rec/T-REC-X.690 ]]
*/
import dt "core:time"
// Selects how a Value's content octets are produced when encoding.
@(private)
_Form :: enum u8 {
Primitive, // content holds the exact content octets, emitted verbatim.
Constructed, // children holds the sub-values, emitted in order.
Integer_Magnitude, // content holds an unsigned big-endian magnitude; DER INTEGER rules are applied on emit.
Bit_String_Octets, // content holds whole-octet payload; a leading 0x00 unused-bits count is added on emit.
Bit_String_Wrapped, // children's DER becomes the payload, behind the 0x00 unused-bits count.
Time, // _when is formatted to UTCTime/GeneralizedTime per the tag on emit.
Raw, // content is a complete pre-encoded element, emitted verbatim (no added tag/length).
}
// Value is a node in a to-be-encoded DER tree. Construct it with the
// helpers below rather than by hand; the fields are an implementation
// detail. The byte/child inputs are borrowed (see the package lifetime note).
Value :: struct {
tag: Tag,
form: _Form,
content: []byte,
children: []Value,
_when: dt.Time, // meaningful only when form == .Time: the instant to format on emit
}
@(rodata, private)
_BOOL_FALSE := []byte{0x00}
@(rodata, private)
_BOOL_TRUE := []byte{0xFF}
// Builds a primitive value with `tag` and the exact `content`
// octets (emitted verbatim). The caller owns canonical-form correctness.
primitive :: proc "contextless" (tag: Tag, content: []byte) -> Value {
return Value{tag = tag, form = .Primitive, content = content}
}
// Builds a value whose encoding IS `encoded` verbatim, a complete
// already-DER-encoded element spliced in as-is (no tag/length added). The
// composition primitive for nesting an independently-marshalled structure
// (a signed CertificationRequestInfo, a pre-built TBSCertificate) inside a
// parent without re-encoding it.
raw :: proc "contextless" (encoded: []byte) -> Value {
return Value{form = .Raw, content = encoded}
}
// Builds a BOOLEAN (DER: 0x00 / 0xFF).
boolean :: proc "contextless" (v: bool) -> Value {
return Value{tag = universal(.Boolean), form = .Primitive, content = v ? _BOOL_TRUE : _BOOL_FALSE}
}
// Builds an INTEGER from an unsigned big-endian magnitude: leading zero
// octets are dropped (minimal encoding) and a single 0x00 sign octet is
// inserted when the top bit would otherwise read as negative. An empty
// or all-zero magnitude encodes as 0. This is the shape RSA moduli /
// exponents and certificate serials are written in, and the inverse of
// read_unsigned_integer_bytes.
integer_unsigned :: proc "contextless" (magnitude: []byte) -> Value {
return Value{tag = universal(.Integer), form = .Integer_Magnitude, content = magnitude}
}
// Builds an INTEGER from content octets that are ALREADY a minimal
// two's-complement encoding (e.g. a serial preserved verbatim from
// read_integer_bytes). No normalization is applied.
integer_raw :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Integer), form = .Primitive, content = content}
}
// Builds an OCTET STRING wrapping `content`.
octet_string :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Octet_String), form = .Primitive, content = content}
}
// Builds an OCTET STRING whose content is the DER encoding of `children`,
// the form X.509 Extension.extnValue uses to carry an extension's value. The
// OCTET STRING stays primitive (0x04); its content is just the children's
// concatenated DER, so this reuses the constructed-content machinery without
// a wrapper octet.
octet_string_wrap :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Octet_String), form = .Constructed, children = children}
}
// Builds a SEQUENCE from its sub-values (borrowed).
sequence :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Sequence, true), form = .Constructed, children = children}
}
// Builds a SET from its sub-values, emitted in the given order. DER SET OF
// requires components sorted by their encoding (X.690 section 11.6); this
// constructor does NOT sort, so a SET OF with more than one element must be
// given pre-sorted (a single-element RDN, the common PKIX case, is trivially
// ordered).
set :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Set, true), form = .Constructed, children = children}
}
// Builds a SET OF from its sub-values, sorted into the DER canonical order
// (X.690 section 11.6: component encodings ascending, shorter padded with
// trailing 0-octets). Unlike the other constructors this one ALLOCATES, it
// must encode each child to compare them, and it sorts `children` in place;
// the returned Value then borrows that reordered slice as usual. Scratch is
// taken from and released to `allocator`. 0/1-element sets need no work.
@(require_results)
set_of :: proc(children: []Value, allocator := context.allocator) -> (value: Value, err: Error) {
n := len(children)
if n <= 1 {
return set(children), .None
}
encs, merr := make([][]byte, n, allocator)
if merr != nil {
return {}, .Allocation_Failed
}
defer {
for e in encs {
delete(e, allocator)
}
delete(encs, allocator)
}
for i in 0 ..< n {
encs[i] = marshal(children[i], allocator) or_return
}
// Insertion sort (n is small for a SET OF) keying children on their encodings.
for i in 1 ..< n {
for j := i; j > 0 && _der_less(encs[j], encs[j - 1]); j -= 1 {
encs[j], encs[j - 1] = encs[j - 1], encs[j]
children[j], children[j - 1] = children[j - 1], children[j]
}
}
return set(children), .None
}
// Compares two encodings as octet strings with the shorter padded at its
// trailing end with 0-octets (X.690 section 11.6 SET OF ordering).
@(private)
_der_less :: proc "contextless" (a, b: []byte) -> bool {
n := min(len(a), len(b))
for i in 0 ..< n {
if a[i] != b[i] {
return a[i] < b[i]
}
}
if len(a) < len(b) {
for i in n ..< len(b) {
if b[i] != 0x00 {
return true // a's zero padding is below b's non-zero tail
}
}
return false
}
for i in n ..< len(a) {
if a[i] != 0x00 {
return false // a's non-zero tail is above b's zero padding
}
}
return false
}
// Builds a NULL (empty content).
null :: proc "contextless" () -> Value {
return Value{tag = universal(.Null), form = .Primitive, content = nil}
}
// Builds an OBJECT IDENTIFIER from already-encoded content octets (the form
// PKIX OIDs are held and compared in, e.g. the package's known-OID
// constants). The content is emitted verbatim; the caller owns its validity.
object_identifier :: proc "contextless" (content: []byte) -> Value {
return Value{tag = universal(.Object_Identifier), form = .Primitive, content = content}
}
// Builds a BIT STRING from a whole-octet payload (unused-bits count 0), the
// only form PKIX uses for SubjectPublicKeyInfo keys and signature values.
bit_string_octets :: proc "contextless" (payload: []byte) -> Value {
return Value{tag = universal(.Bit_String), form = .Bit_String_Octets, content = payload}
}
// Builds a BIT STRING (whole octets) whose payload is the DER encoding of
// `children`, the form SubjectPublicKeyInfo uses to carry an RSAPublicKey
// SEQUENCE inside the subjectPublicKey bit string.
bit_string_wrap :: proc "contextless" (children: []Value) -> Value {
return Value{tag = universal(.Bit_String), form = .Bit_String_Wrapped, children = children}
}
// Builds a UTCTime ("YYMMDDHHMMSSZ", RFC 5280 DER profile). Appropriate for
// instants in 1950..=2049; the sliding-window century is what the reader
// (read_utc_time) decodes, so use generalized_time outside that range.
utc_time :: proc "contextless" (at: dt.Time) -> Value {
return Value{tag = universal(.UTC_Time), form = .Time, _when = at}
}
// Builds a GeneralizedTime ("YYYYMMDDHHMMSSZ", RFC 5280 DER profile: Zulu,
// seconds present, no fractional part). The inverse of read_generalized_time.
generalized_time :: proc "contextless" (at: dt.Time) -> Value {
return Value{tag = universal(.Generalized_Time), form = .Time, _when = at}
}
// 2050-01-01T00:00:00Z: the RFC 5280 boundary between the two time forms.
@(private)
_UNIX_2050 :: i64(2_524_608_000)
// Builds a UTCTime or GeneralizedTime, auto-selecting the form per the RFC
// 5280 profile: UTCTime for instants in 1950..=2049, GeneralizedTime from
// 2050 on. (X.509 validity dates are written this way.)
time :: proc "contextless" (at: dt.Time) -> Value {
if dt.to_unix_seconds(at) < _UNIX_2050 {
return utc_time(at)
}
return generalized_time(at)
}
// Builds a primitive [number] IMPLICIT value carrying raw content octets,
// e.g. AuthorityKeyIdentifier's keyIdentifier [0] IMPLICIT OCTET STRING.
context_primitive :: proc "contextless" (number: u32, content: []byte) -> Value {
return Value{tag = context_specific(number, false), form = .Primitive, content = content}
}
// Builds a constructed [number] EXPLICIT wrapper around the given sub-values,
// e.g. TBSCertificate's version [0] EXPLICIT INTEGER.
context_explicit :: proc "contextless" (number: u32, children: []Value) -> Value {
return Value{tag = context_specific(number, true), form = .Constructed, children = children}
}
// Returns the exact number of bytes encode/marshal will write
// for `v`, including its identifier and length octets.
encoded_len :: proc(v: Value) -> int {
if v.form == .Raw {
return len(v.content) // already a complete element
}
clen := _content_len(v)
return _tag_len(v.tag) + _length_len(clen) + clen
}
// Writes the DER encoding of `v` into `dst` and returns the number
// of bytes written. `dst` must be at least encoded_len(v) bytes; if it is
// shorter, nothing is written and Buffer_Too_Small is returned.
encode :: proc(v: Value, dst: []byte) -> (n: int, err: Error) {
need := encoded_len(v)
if len(dst) < need {
return 0, .Buffer_Too_Small
}
_emit(v, dst[:need])
return need, .None
}
// Encodes `v` into a freshly allocated slice the caller owns.
marshal :: proc(v: Value, allocator := context.allocator) -> (out: []byte, err: Error) {
n := encoded_len(v)
buf, merr := make([]byte, n, allocator)
if merr != nil {
return nil, .Allocation_Failed
}
_emit(v, buf)
return buf, .None
}
@(private)
_content_len :: proc(v: Value) -> int {
switch v.form {
case .Primitive:
return len(v.content)
case .Integer_Magnitude:
start, pad := _int_shape(v.content)
if start == len(v.content) {
return 1 // zero encodes as a single 0x00 octet
}
return (len(v.content) - start) + (pad ? 1 : 0)
case .Bit_String_Octets:
return 1 + len(v.content) // leading unused-bits octet (0x00)
case .Bit_String_Wrapped:
total := 1 // leading unused-bits octet (0x00)
for child in v.children {
total += encoded_len(child)
}
return total
case .Time:
return _time_content_len(v.tag)
case .Constructed:
total := 0
for child in v.children {
total += encoded_len(child)
}
return total
case .Raw:
return len(v.content) // handled in encoded_len; here for exhaustiveness
}
return 0
}
// Writes v's complete encoding so that it ENDS at dst[len(dst)], i.e. into
// the tail of dst, and returns the bytes written. `dst` is the exactly-sized
// region this node may occupy (encoded_len(v) == len(dst) at the top call).
//
// Emitting back-to-front means a constructed node's content length falls out
// of where its children landed (no second measure pass): encoded_len does the
// single O(n) sizing pass, this does the single O(n) write pass. DER wants to
// be written this way — it is the tree generalization of the fixed-buffer
// trick in crypto/ecdsa's hand-rolled encoder.
@(private)
_emit :: proc(v: Value, dst: []byte) -> int {
if v.form == .Raw {
n := len(v.content)
copy(dst[len(dst) - n:], v.content)
return n
}
end := len(dst)
switch v.form {
case .Raw: // handled by the early return above
case .Primitive:
end -= len(v.content)
copy(dst[end:], v.content)
case .Integer_Magnitude:
start, pad := _int_shape(v.content)
if start == len(v.content) {
end -= 1
dst[end] = 0x00 // zero -> single 0x00 octet
} else {
body := v.content[start:]
end -= len(body)
copy(dst[end:], body)
if pad {
end -= 1
dst[end] = 0x00 // sign octet so the value reads as non-negative
}
}
case .Bit_String_Octets:
end -= len(v.content)
copy(dst[end:], v.content)
end -= 1
dst[end] = 0x00 // unused-bits count: whole octets
case .Bit_String_Wrapped:
for i := len(v.children) - 1; i >= 0; i -= 1 {
end -= _emit(v.children[i], dst[:end])
}
end -= 1
dst[end] = 0x00 // unused-bits count: whole octets
case .Time:
tmp: [15]byte // GeneralizedTime is the longest form
n := _format_time(tmp[:], v._when, v.tag.number == u32(Tag_Number.Generalized_Time))
end -= n
copy(dst[end:], tmp[:n])
case .Constructed:
for i := len(v.children) - 1; i >= 0; i -= 1 {
end -= _emit(v.children[i], dst[:end])
}
}
clen := len(dst) - end // content length, read off the cursor — never recomputed
tmp: [9]byte // identifier byte + up to 8 length octets covers any int length
lw := _write_length(tmp[:], clen)
end -= lw
copy(dst[end:], tmp[:lw])
end -= 1
dst[end] = _tag_byte(v.tag)
return len(dst) - end
}
// UTCTime content is "YYMMDDHHMMSSZ" (13 octets); GeneralizedTime is
// "YYYYMMDDHHMMSSZ" (15). The content length is fixed by the tag, so it is
// known for the measure pass without formatting.
@(private)
_time_content_len :: proc "contextless" (tag: Tag) -> int {
return tag.number == u32(Tag_Number.Generalized_Time) ? 15 : 13
}
// Formats `at` into dst as the RFC 5280 DER time profile (Zulu, seconds
// present, no fractional part) and returns the bytes written: 15 for
// GeneralizedTime ("YYYYMMDDHHMMSSZ"), 13 for UTCTime ("YYMMDDHHMMSSZ", with
// the low two year digits). dst must hold the full width.
@(private)
_format_time :: proc "contextless" (dst: []byte, at: dt.Time, generalized: bool) -> int {
// time_to_datetime decomposes the instant into UTC calendar fields; it only
// fails outside time.Time's range, which an in-range time.Time can't reach.
c, _ := dt.time_to_datetime(at)
p := 0
if generalized {
p += _write_digits(dst[p:], int(c.year), 4)
} else {
p += _write_digits(dst[p:], int(c.year % 100), 2)
}
p += _write_digits(dst[p:], int(c.month), 2)
p += _write_digits(dst[p:], int(c.day), 2)
p += _write_digits(dst[p:], int(c.hour), 2)
p += _write_digits(dst[p:], int(c.minute), 2)
p += _write_digits(dst[p:], int(c.second), 2)
dst[p] = 'Z'
p += 1
return p
}
// Writes `value` as exactly `width` zero-padded decimal digits.
@(private)
_write_digits :: proc "contextless" (dst: []byte, value, width: int) -> int {
x := value
for i := width - 1; i >= 0; i -= 1 {
dst[i] = byte('0' + x % 10)
x /= 10
}
return width
}
// Reports the index of the first significant magnitude octet
// (== len(mag) when the value is zero) and whether a 0x00 sign octet must
// be prepended so the result reads as non-negative.
@(private)
_int_shape :: proc "contextless" (mag: []byte) -> (start: int, pad: bool) {
start = 0
for start < len(mag) && mag[start] == 0x00 {
start += 1
}
if start == len(mag) {
return start, false
}
return start, mag[start] & 0x80 != 0
}
// _length_len / _write_length are the inverse of _read_length: definite
// form, minimal (short form below 128, otherwise the fewest octets).
@(private)
_length_len :: proc "contextless" (length: int) -> int {
if length < 0x80 {
return 1
}
n := 1
v := length
for v > 0 {
v >>= 8
n += 1
}
return n
}
@(private)
_write_length :: proc "contextless" (dst: []byte, length: int) -> int {
if length < 0x80 {
dst[0] = byte(length)
return 1
}
nbytes := 0
v := length
for v > 0 {
v >>= 8
nbytes += 1
}
dst[0] = 0x80 | byte(nbytes)
for i in 0 ..< nbytes {
dst[1 + i] = byte(length >> uint(8 * (nbytes - 1 - i)))
}
return 1 + nbytes
}
// _tag_len / _tag_byte are the inverse of _read_tag, low-tag-number form
// only (number <= 30); PKIX never needs high-tag-number identifiers.
@(private)
_tag_len :: proc "contextless" (tag: Tag) -> int {
return 1
}
@(private)
_tag_byte :: proc "contextless" (tag: Tag) -> byte {
assert_contextless(tag.number <= 30, "asn1: high-tag-number form is not supported by the writer")
b := byte(tag.class) << 6
if tag.constructed {
b |= 0x20
}
b |= byte(tag.number) & 0x1F
return b
}

View File

@@ -425,15 +425,21 @@ parse_hostname_or_endpoint :: proc(endpoint_str: string) -> (target: Host_Or_End
// Takes an endpoint string and returns its parts.
// Returns ok=false if port is not a number.
split_port :: proc(endpoint_str: string) -> (addr_or_host: string, port: int, ok: bool) {
// IP6 [addr_or_host]:port
if i := strings.last_index(endpoint_str, "]:"); i >= 0 {
addr_or_host = endpoint_str[1:i]
port, ok = strconv.parse_int(endpoint_str[i+2:], 10)
// IP6 [addr_or_host]:port — the bracketed form requires a leading
// '[', so only treat a closing "]:" as the port separator when it
// sits at index >= 1 (i.e. an opening '[' is actually present).
// Inputs that contain "]:" without an opening bracket fall through
// to the plain host/port handling below.
if len(endpoint_str) > 0 && endpoint_str[0] == '[' {
if i := strings.last_index(endpoint_str, "]:"); i >= 1 {
addr_or_host = endpoint_str[1:i]
port, ok = strconv.parse_int(endpoint_str[i+2:], 10)
if port > 65535 {
ok = false
if port > 65535 {
ok = false
}
return
}
return
}
if n := strings.count(endpoint_str, ":"); n == 1 {

View File

@@ -0,0 +1,133 @@
package test_core_x509
// Deterministic mutational fuzzing for the certificate parser, seeded
// (and reproducible) via the test runner's logged random seed.
//
// Invariants on every input: parse never panics, never reads outside
// the input (bounds checks + address sanitizer in CI), never leaks on
// error paths (the runner's tracking allocator), and on success every
// raw view lies within the input buffer.
import "core:crypto/x509"
import "core:math/rand"
import "core:testing"
FUZZ_MUTATE_ITERS :: 1500
FUZZ_RANDOM_ITERS :: 2048
// _check_views asserts that a successfully-parsed Certificate only
// references memory inside its input.
@(private="file")
_check_views :: proc(t: ^testing.T, cert: ^x509.Certificate, der: []byte) {
in_bounds :: proc(view: []byte, der: []byte) -> bool {
if len(view) == 0 {
return true
}
base := uintptr(raw_data(der))
view_start := uintptr(raw_data(view))
return view_start >= base && view_start + uintptr(len(view)) <= base + uintptr(len(der))
}
testing.expect(t, in_bounds(cert.raw, der), "raw out of bounds")
testing.expect(t, in_bounds(cert.raw_tbs, der), "raw_tbs out of bounds")
testing.expect(t, in_bounds(cert.raw_issuer, der), "raw_issuer out of bounds")
testing.expect(t, in_bounds(cert.raw_subject, der), "raw_subject out of bounds")
testing.expect(t, in_bounds(cert.raw_spki, der), "raw_spki out of bounds")
testing.expect(t, in_bounds(cert.serial, der), "serial out of bounds")
testing.expect(t, in_bounds(cert.signature, der), "signature out of bounds")
testing.expect(t, cert.version >= 1 && cert.version <= 3, "version range")
for name in cert.dns_names {
testing.expect(t, in_bounds(transmute([]byte)name, der), "dns name out of bounds")
}
for ip in cert.ip_addresses {
testing.expect(t, in_bounds(ip, der), "ip out of bounds")
testing.expect(t, len(ip) == 4 || len(ip) == 16, "ip width")
}
for ext in cert.extensions {
testing.expect(t, in_bounds(ext.oid, der), "ext oid out of bounds")
testing.expect(t, in_bounds(ext.value, der), "ext value out of bounds")
}
}
// Every single-bit flip of a real certificate: parse must fail cleanly
// or succeed with intact invariants.
@(test)
test_fuzz_bitflips :: proc(t: ^testing.T) {
buf := make([]byte, len(EC_DER))
defer delete(buf)
for i in 0 ..< len(EC_DER) {
for bit in 0 ..< uint(8) {
copy(buf, EC_DER)
buf[i] ~= 1 << bit
cert, err := x509.parse(buf)
if err == .None {
_check_views(t, &cert, buf)
x509.destroy(&cert)
}
}
}
}
// Random multi-byte mutations across all three fixtures.
@(test)
test_fuzz_mutations :: proc(t: ^testing.T) {
fixtures := [?][]byte{RSA_DER, EC_DER, ED_DER}
max_len := 0
for f in fixtures {
max_len = max(max_len, len(f))
}
buf := make([]byte, max_len)
defer delete(buf)
for _ in 0 ..< FUZZ_MUTATE_ITERS {
fixture := rand.choice(fixtures[:])
input := buf[:len(fixture)]
copy(input, fixture)
// Mutate 1-16 positions, occasionally truncating instead —
// length-field damage is where TLV parsers historically break.
if rand.int_max(8) == 0 {
input = input[:rand.int_max(len(input) + 1)]
}
for _ in 0 ..< 1 + rand.int_max(16) {
if len(input) == 0 {
break
}
input[rand.int_max(len(input))] = byte(rand.uint32())
}
cert, err := x509.parse(input)
if err == .None {
_check_views(t, &cert, input)
x509.destroy(&cert)
}
}
}
// Pure noise: parse must reject (or, vanishingly unlikely, accept with
// invariants intact) without panicking.
@(test)
test_fuzz_random_garbage :: proc(t: ^testing.T) {
buf: [128]byte
for _ in 0 ..< FUZZ_RANDOM_ITERS {
n := rand.int_max(len(buf) + 1)
input := buf[:n]
for i in 0 ..< n {
input[i] = byte(rand.uint32())
}
// Bias towards the outer shape so mutation reaches the TBS walk.
if n > 4 && rand.int_max(2) == 0 {
input[0] = 0x30
input[1] = byte(rand.int_max(n))
}
cert, err := x509.parse(input)
if err == .None {
_check_views(t, &cert, input)
x509.destroy(&cert)
}
}
}

View File

@@ -0,0 +1,138 @@
package test_core_x509
// Out-of-memory robustness: parse must surface Allocation_Failed
// cleanly and leak nothing when any one of its table allocations
// fails. A failing allocator wraps a tracking allocator; sweeping the
// fail point across every allocation site (and verifying zero leaked
// blocks after each) exercises every OOM path and its unwind.
import "base:runtime"
import "core:mem"
import "core:crypto/x509"
import "core:testing"
import "core:time"
// Failing_Allocator passes through to a backing allocator but returns
// Out_Of_Memory on the (fail_at)-th allocation request, counting only
// the allocating modes.
@(private="file")
Failing_Allocator :: struct {
backing: runtime.Allocator,
count: int,
fail_at: int, // -1 = never fail
}
@(private="file")
failing_allocator_proc :: proc(
data: rawptr, mode: runtime.Allocator_Mode,
size, alignment: int, old_memory: rawptr, old_size: int,
loc := #caller_location,
) -> ([]byte, runtime.Allocator_Error) {
fa := (^Failing_Allocator)(data)
#partial switch mode {
case .Alloc, .Alloc_Non_Zeroed, .Resize, .Resize_Non_Zeroed:
if fa.count == fa.fail_at {
fa.count += 1
return nil, .Out_Of_Memory
}
fa.count += 1
}
return fa.backing.procedure(fa.backing.data, mode, size, alignment, old_memory, old_size, loc)
}
@(private="file")
failing_allocator :: proc(fa: ^Failing_Allocator) -> runtime.Allocator {
return {procedure = failing_allocator_proc, data = fa}
}
@(test)
test_oom_parse_sweep :: proc(t: ^testing.T) {
// A cert that drives all three table allocations (extensions, DNS
// SANs, IP SANs): the EC fixture has SANs + KU + EKU + BC.
der := EC_DER
// First, learn how many allocations a clean parse makes.
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
cert, err := x509.parse(der, failing_allocator(&fa))
testing.expect_value(t, err, x509.Error.None)
x509.destroy(&cert, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 3, "expected at least 3 allocation sites")
testing.expect_value(t, len(track.allocation_map), 0)
}
// Now fail at each allocation in turn; every one must yield a clean
// Allocation_Failed and leak nothing.
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
cert, err := x509.parse(der, failing_allocator(&fa))
if err == .None {
// This allocation site wasn't on the parse path for this
// input; clean up and move on.
x509.destroy(&cert, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed,
"fail_at=%d: expected Allocation_Failed, got %v", k, err)
}
// Either way, parse must own no memory afterwards (its failure
// path calls destroy internally; the success path we cleaned).
testing.expectf(t, len(track.allocation_map) == 0,
"fail_at=%d: %d block(s) leaked", k, len(track.allocation_map))
}
}
// verify_chain allocates exactly one block (the chain buffer). Failing
// it must yield Allocation_Failed and leak nothing. Certificates are
// parsed with the real allocator; only verify_chain gets the failing one.
@(test)
test_oom_verify_chain :: proc(t: ^testing.T) {
leaf, _ := x509.parse(EC_CHAIN_LEAF); defer x509.destroy(&leaf)
inter, _ := x509.parse(EC_CHAIN_INTER); defer x509.destroy(&inter)
root, _ := x509.parse(EC_CHAIN_ROOT); defer x509.destroy(&root)
opts := x509.Verify_Options{
roots = {&root},
intermediates = {&inter},
current_time = time.unix(CHAIN_NOW, 0),
}
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
chain, err := x509.verify_chain(&leaf, opts, failing_allocator(&fa))
testing.expect_value(t, err, x509.Error.None)
delete(chain, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 1, "verify_chain should allocate at least once")
testing.expect_value(t, len(track.allocation_map), 0)
}
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
chain, err := x509.verify_chain(&leaf, opts, failing_allocator(&fa))
if err == .None {
delete(chain, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed,
"fail_at=%d: expected Allocation_Failed, got %v", k, err)
}
testing.expectf(t, len(track.allocation_map) == 0,
"fail_at=%d: %d block(s) leaked", k, len(track.allocation_map))
}
}

View File

@@ -0,0 +1,661 @@
package test_core_x509
// Fixtures generated with openssl 3.x (see testdata/):
//
// openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
// -subj "/C=US/O=Odin Test/CN=localhost" \
// -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
// -addext "basicConstraints=critical,CA:TRUE"
//
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
// -sha256 -days 3650 -nodes -subj "/CN=example.com" \
// -addext "subjectAltName=DNS:example.com,DNS:*.example.com,IP:2001:db8::1" \
// -addext "extendedKeyUsage=serverAuth" \
// -addext "keyUsage=critical,digitalSignature" \
// -addext "basicConstraints=critical,CA:FALSE"
//
// openssl req -x509 -newkey ed25519 -days 3650 -nodes -subj "/CN=ed.test"
//
// Field expectations below were cross-checked against
// `openssl x509 -noout -text`.
import "core:crypto/x509"
import "core:testing"
import "core:time"
RSA_DER := #load("testdata/rsa.der")
EC_DER := #load("testdata/ec.der")
ED_DER := #load("testdata/ed.der")
// Chains for verify_chain (generated by testdata/gen_chains.sh):
// P-256: chain_ec_root -> chain_ec_inter (pathlen:0) -> chain_ec_leaf
// (CN=leaf.example.com, EKU serverAuth); chain_ec_other_root is
// an unrelated anchor for the wrong-root case.
// Ed25519: chain_ed_root -> chain_ed_leaf (CN=ed-leaf.example.com).
EC_CHAIN_ROOT := #load("testdata/chain_ec_root.der")
EC_CHAIN_INTER := #load("testdata/chain_ec_inter.der")
EC_CHAIN_LEAF := #load("testdata/chain_ec_leaf.der")
EC_OTHER_ROOT := #load("testdata/chain_ec_other_root.der")
ED_CHAIN_ROOT := #load("testdata/chain_ed_root.der")
ED_CHAIN_LEAF := #load("testdata/chain_ed_leaf.der")
// Expired root (validity 2010-2015) that signed a still-valid leaf;
// RFC 5280 section 6.1 does not validate the trust anchor itself.
EC_EXPIRED_ROOT := #load("testdata/chain_ec_expired_root.der")
EC_EXP_LEAF := #load("testdata/chain_ec_expleaf.der")
// Negative path-validation fixtures: each chain trips one verify_chain
// rejection. neg_root signs the *_inter certs; pl_a -> pl_b -> pl_leaf
// chains deeper to violate pathlen:0 on pl_a.
NEG_ROOT := #load("testdata/neg_root.der")
NEG_NOKCS_INTER := #load("testdata/neg_nokcs_inter.der") // CA, no keyCertSign
NEG_NOKCS_LEAF := #load("testdata/neg_nokcs_leaf.der")
NEG_NOTCA_INTER := #load("testdata/neg_notca_inter.der") // CA:FALSE
NEG_NOTCA_LEAF := #load("testdata/neg_notca_leaf.der")
NEG_EXPINTER := #load("testdata/neg_expinter_inter.der") // expired CA
NEG_EXPINTER_LEAF := #load("testdata/neg_expinter_leaf.der")
NEG_PL_A := #load("testdata/neg_pl_a.der") // pathlen:0
NEG_PL_B := #load("testdata/neg_pl_b.der")
NEG_PL_LEAF := #load("testdata/neg_pl_leaf.der")
NEG_EKU_INTER := #load("testdata/neg_eku_inter.der") // emailProtection CA
NEG_EKU_LEAF := #load("testdata/neg_eku_leaf.der") // serverAuth leaf
NEG_NC_INTER := #load("testdata/neg_nc_inter.der") // non-critical nameConstraints
NEG_NC_LEAF := #load("testdata/neg_nc_leaf.der") // name within permitted subtree
// EKU alternative path: two intermediates sharing a subject DN and key,
// one emailProtection-only, one serverAuth; the leaf chains through either.
EKU_ALT_BAD := #load("testdata/eku_alt_bad.der") // same key, emailProtection only
EKU_ALT_GOOD := #load("testdata/eku_alt_good.der") // same key, serverAuth
EKU_ALT_LEAF := #load("testdata/eku_alt_leaf.der") // serverAuth leaf
// Cross-signed cycle: A and B mutually cross-certify; trusting neither
// must terminate (no hang) and report Unknown_Authority.
CYC_A := #load("testdata/cyc_a.der")
CYC_B := #load("testdata/cyc_b.der")
CYC_A_BY_B := #load("testdata/cyc_a_by_b.der")
CYC_B_BY_A := #load("testdata/cyc_b_by_a.der")
CYC_LEAF := #load("testdata/cyc_leaf.der")
// Reference times relative to the chain fixtures (issued 2026-06-13,
// leaves valid ~10 years). 2027 is inside every window; 2020 precedes
// the leaf; 2040 is past the leaf (which expires first).
CHAIN_NOW :: i64(1798761600) // 2027-01-01Z
CHAIN_BEFORE :: i64(1577836800) // 2020-01-01Z
CHAIN_AFTER :: i64(2208988800) // 2040-01-01Z
// A real CA root (GoDaddy G2) carrying serial number 0 — a
// non-conformant but in-the-wild case surfaced by differential testing
// against Go's crypto/x509 over the system CA store.
SERIAL_ZERO_DER := #load("testdata/serial_zero.der")
// A cert with two subjectAltName extensions, from the x509-limbo
// corpus — RFC 5280 section 4.2 forbids duplicate extension OIDs.
DUP_EXTENSION_DER := #load("testdata/dup_extension.der")
@(test)
test_duplicate_extension :: proc(t: ^testing.T) {
cert, err := x509.parse(DUP_EXTENSION_DER)
if err == .None {
x509.destroy(&cert)
}
testing.expect_value(t, err, x509.Error.Duplicate_Extension)
}
@(test)
test_parse_rsa :: proc(t: ^testing.T) {
cert, err := x509.parse(RSA_DER)
defer x509.destroy(&cert)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, cert.version, 3)
testing.expect_value(t, cert.signature_algorithm, x509.Signature_Algorithm.RSA_SHA256)
testing.expect_value(t, cert.public_key_algorithm, x509.Public_Key_Algorithm.RSA)
// serial=074476066FA3819E72A324BC52CEF3920F0E7156 (20 octets).
testing.expect_value(t, len(cert.serial), 20)
testing.expect_value(t, cert.serial[0], u8(0x07))
testing.expect_value(t, cert.serial[19], u8(0x56))
// 2048-bit modulus, e = 65537.
testing.expect_value(t, len(cert.rsa_n), 256)
testing.expect_value(t, len(cert.rsa_e), 3)
testing.expect_value(t, cert.rsa_e[0], u8(0x01))
testing.expect_value(t, cert.rsa_e[2], u8(0x01))
// notBefore=Jun 12 18:53:09 2026 GMT.
testing.expect_value(t, time.to_unix_seconds(cert.not_before), i64(1781290389))
testing.expect_value(t, cert.basic_constraints_valid, true)
testing.expect_value(t, cert.is_ca, true)
testing.expect_value(t, cert.max_path_len, -1)
testing.expect_value(t, len(cert.dns_names), 1)
testing.expect_value(t, cert.dns_names[0], "localhost")
testing.expect_value(t, len(cert.ip_addresses), 1)
testing.expect_value(t, len(cert.ip_addresses[0]), 4)
testing.expect_value(t, cert.ip_addresses[0][0], u8(127))
// Self-signed: issuer bytes == subject bytes.
testing.expect(t, len(cert.raw_issuer) > 0)
testing.expect_value(t, string(cert.raw_issuer), string(cert.raw_subject))
// openssl always adds SKI; self-signed adds matching AKI.
testing.expect(t, len(cert.subject_key_id) > 0, "SKI expected")
testing.expect(t, !cert.unhandled_critical, "all criticals understood")
// The raw views must cover real ranges.
testing.expect_value(t, len(cert.raw), len(RSA_DER))
testing.expect(t, len(cert.raw_tbs) > 0)
testing.expect(t, len(cert.raw_spki) > 0)
testing.expect_value(t, len(cert.signature), 256)
}
@(test)
test_parse_ec :: proc(t: ^testing.T) {
cert, err := x509.parse(EC_DER)
defer x509.destroy(&cert)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, cert.signature_algorithm, x509.Signature_Algorithm.ECDSA_SHA256)
testing.expect_value(t, cert.public_key_algorithm, x509.Public_Key_Algorithm.ECDSA_P256)
testing.expect_value(t, len(cert.ec_point), 65)
testing.expect_value(t, cert.ec_point[0], u8(0x04))
testing.expect_value(t, time.to_unix_seconds(cert.not_after), i64(2096650389)) // notAfter Jun 9 18:53:09 2036 GMT
testing.expect_value(t, cert.basic_constraints_valid, true)
testing.expect_value(t, cert.is_ca, false)
testing.expect_value(t, cert.has_key_usage, true)
testing.expect(t, .Digital_Signature in cert.key_usage)
testing.expect(t, .Key_Cert_Sign not_in cert.key_usage)
testing.expect_value(t, cert.has_ext_key_usage, true)
testing.expect(t, .Server_Auth in cert.ext_key_usage)
testing.expect(t, .Client_Auth not_in cert.ext_key_usage)
testing.expect_value(t, cert.eku_has_unknown, false)
testing.expect_value(t, len(cert.dns_names), 2)
testing.expect_value(t, cert.dns_names[0], "example.com")
testing.expect_value(t, cert.dns_names[1], "*.example.com")
testing.expect_value(t, len(cert.ip_addresses), 1)
testing.expect_value(t, len(cert.ip_addresses[0]), 16)
testing.expect_value(t, cert.ip_addresses[0][0], u8(0x20))
testing.expect_value(t, cert.ip_addresses[0][1], u8(0x01))
}
@(test)
test_parse_ed25519 :: proc(t: ^testing.T) {
cert, err := x509.parse(ED_DER)
defer x509.destroy(&cert)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, cert.signature_algorithm, x509.Signature_Algorithm.Ed25519)
testing.expect_value(t, cert.public_key_algorithm, x509.Public_Key_Algorithm.Ed25519)
testing.expect_value(t, len(cert.ec_point), 32)
testing.expect_value(t, len(cert.signature), 64)
}
// Serial 0 decodes to the single octet {0x00}, not empty (the
// openssl-aligned convention; see Certificate.serial).
@(test)
test_serial_zero :: proc(t: ^testing.T) {
cert, err := x509.parse(SERIAL_ZERO_DER)
defer x509.destroy(&cert)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(cert.serial), 1)
testing.expect_value(t, cert.serial[0], u8(0x00))
testing.expect_value(t, cert.is_ca, true)
}
@(test)
test_valid_at :: proc(t: ^testing.T) {
cert, err := x509.parse(RSA_DER)
defer x509.destroy(&cert)
testing.expect_value(t, err, x509.Error.None)
INSIDE := time.unix(1893456000, 0) // 2030-01-01Z
BEFORE := time.unix(1577836800, 0) // 2020-01-01Z
AFTER := time.unix(2208988800, 0) // 2040-01-01Z
testing.expect_value(t, x509.valid_at(&cert, INSIDE), true)
testing.expect_value(t, x509.valid_at(&cert, BEFORE), false)
testing.expect_value(t, x509.valid_at(&cert, AFTER), false)
// Boundaries are inclusive.
testing.expect_value(t, x509.valid_at(&cert, cert.not_before), true)
testing.expect_value(t, x509.valid_at(&cert, cert.not_after), true)
}
@(test)
test_verify_hostname :: proc(t: ^testing.T) {
ec, err := x509.parse(EC_DER)
defer x509.destroy(&ec)
testing.expect_value(t, err, x509.Error.None)
// Exact and case-insensitive.
testing.expect_value(t, x509.verify_hostname(&ec, "example.com"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ec, "EXAMPLE.COM"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ec, "example.com."), x509.Error.None)
// Wildcard: exactly one left-most label.
testing.expect_value(t, x509.verify_hostname(&ec, "api.example.com"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ec, "API.Example.Com"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ec, "a.b.example.com"), x509.Error.Hostname_Mismatch)
testing.expect_value(t, x509.verify_hostname(&ec, "example.org"), x509.Error.Hostname_Mismatch)
testing.expect_value(t, x509.verify_hostname(&ec, "com"), x509.Error.Hostname_Mismatch)
// IPv6 SAN 2001:db8::1.
testing.expect_value(t, x509.verify_hostname(&ec, "2001:db8::1"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ec, "2001:db8::2"), x509.Error.Hostname_Mismatch)
// IPv4 against the RSA cert (127.0.0.1 SAN).
rsa, rerr := x509.parse(RSA_DER)
defer x509.destroy(&rsa)
testing.expect_value(t, rerr, x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&rsa, "127.0.0.1"), x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&rsa, "127.0.0.2"), x509.Error.Hostname_Mismatch)
testing.expect_value(t, x509.verify_hostname(&rsa, "localhost"), x509.Error.None)
// Ed cert has no SANs at all.
ed, eerr := x509.parse(ED_DER)
defer x509.destroy(&ed)
testing.expect_value(t, eerr, x509.Error.None)
testing.expect_value(t, x509.verify_hostname(&ed, "ed.test"), x509.Error.No_SAN)
}
// verify_hostname must accept any bytes in the host string and return a
// result (never fault), whatever odd punctuation the input contains. In
// normal use the host comes from the application, not the certificate;
// this just pins down robust behaviour on unusual inputs.
@(test)
test_verify_hostname_hostile_input :: proc(t: ^testing.T) {
ec, err := x509.parse(EC_DER)
defer x509.destroy(&ec)
testing.expect_value(t, err, x509.Error.None)
hostile := []string{
"]:80",
"]:",
"]",
"[",
"[]",
"::::",
":",
"]:a]:b",
"\x00\x01\x02",
"foo]:bar",
"[not-closed",
"*.*.*",
}
for h in hostile {
// Any result is acceptable; not panicking is the point.
_ = x509.verify_hostname(&ec, h)
}
}
// Every truncation of a real certificate must fail cleanly.
@(test)
test_truncation_sweep :: proc(t: ^testing.T) {
for n in 0 ..< len(EC_DER) {
cert, err := x509.parse(EC_DER[:n])
if err == .None {
x509.destroy(&cert)
}
testing.expect(t, err != .None, "truncated certificate must be rejected")
}
// Trailing garbage is rejected too.
{
grown := make([]byte, len(EC_DER) + 1)
defer delete(grown)
copy(grown, EC_DER)
_, err := x509.parse(grown)
testing.expect(t, err != .None, "trailing bytes must be rejected")
}
}
// ---- signature & chain verification ----
@(test)
test_verify_signature_ec :: proc(t: ^testing.T) {
leaf, e1 := x509.parse(EC_CHAIN_LEAF); defer x509.destroy(&leaf)
inter, e2 := x509.parse(EC_CHAIN_INTER); defer x509.destroy(&inter)
root, e3 := x509.parse(EC_CHAIN_ROOT); defer x509.destroy(&root)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
testing.expect_value(t, e3, x509.Error.None)
// Correct issuers verify; cross-pairings do not.
testing.expect_value(t, x509.verify_signature(&leaf, &inter), x509.Error.None)
testing.expect_value(t, x509.verify_signature(&inter, &root), x509.Error.None)
testing.expect_value(t, x509.verify_signature(&leaf, &root), x509.Error.Signature_Invalid)
}
@(test)
test_verify_signature_ed :: proc(t: ^testing.T) {
leaf, e1 := x509.parse(ED_CHAIN_LEAF); defer x509.destroy(&leaf)
root, e2 := x509.parse(ED_CHAIN_ROOT); defer x509.destroy(&root)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
testing.expect_value(t, x509.verify_signature(&leaf, &root), x509.Error.None)
}
// RSA verification is not implemented yet; it must report
// Unsupported_Algorithm rather than a spurious pass or fail. rsa.der is
// self-signed, so it is its own issuer.
@(test)
test_verify_signature_rsa_unsupported :: proc(t: ^testing.T) {
rsa, err := x509.parse(RSA_DER); defer x509.destroy(&rsa)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, x509.verify_signature(&rsa, &rsa), x509.Error.Unsupported_Algorithm)
}
@(test)
test_verify_chain_ec :: proc(t: ^testing.T) {
leaf, e1 := x509.parse(EC_CHAIN_LEAF); defer x509.destroy(&leaf)
inter, e2 := x509.parse(EC_CHAIN_INTER); defer x509.destroy(&inter)
root, e3 := x509.parse(EC_CHAIN_ROOT); defer x509.destroy(&root)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
testing.expect_value(t, e3, x509.Error.None)
opts := x509.Verify_Options{
roots = {&root},
intermediates = {&inter},
current_time = time.unix(CHAIN_NOW, 0),
dns_name = "leaf.example.com",
required_eku = .Server_Auth,
}
chain, err := x509.verify_chain(&leaf, opts)
defer delete(chain)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(chain), 3)
if len(chain) == 3 {
testing.expect(t, chain[0] == &leaf, "chain[0] is the leaf")
testing.expect(t, chain[1] == &inter, "chain[1] is the intermediate")
testing.expect(t, chain[2] == &root, "chain[2] is the anchor")
}
}
@(test)
test_verify_chain_ed :: proc(t: ^testing.T) {
leaf, e1 := x509.parse(ED_CHAIN_LEAF); defer x509.destroy(&leaf)
root, e2 := x509.parse(ED_CHAIN_ROOT); defer x509.destroy(&root)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
opts := x509.Verify_Options{
roots = {&root},
current_time = time.unix(CHAIN_NOW, 0),
dns_name = "ed-leaf.example.com",
}
chain, err := x509.verify_chain(&leaf, opts)
defer delete(chain)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(chain), 2)
}
@(test)
test_verify_chain_negatives :: proc(t: ^testing.T) {
leaf, _ := x509.parse(EC_CHAIN_LEAF); defer x509.destroy(&leaf)
inter, _ := x509.parse(EC_CHAIN_INTER); defer x509.destroy(&inter)
root, _ := x509.parse(EC_CHAIN_ROOT); defer x509.destroy(&root)
other, _ := x509.parse(EC_OTHER_ROOT); defer x509.destroy(&other)
roots := []^x509.Certificate{&root}
inters := []^x509.Certificate{&inter}
// Missing the intermediate: cannot bridge leaf -> root.
{
opts := x509.Verify_Options{roots = roots, current_time = time.unix(CHAIN_NOW, 0)}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// Untrusted anchor.
{
opts := x509.Verify_Options{roots = {&other}, intermediates = inters, current_time = time.unix(CHAIN_NOW, 0)}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// Expired (now is past the leaf's notAfter).
{
opts := x509.Verify_Options{roots = roots, intermediates = inters, current_time = time.unix(CHAIN_AFTER, 0)}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Expired)
}
// Not yet valid (now precedes the leaf's notBefore).
{
opts := x509.Verify_Options{roots = roots, intermediates = inters, current_time = time.unix(CHAIN_BEFORE, 0)}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Not_Yet_Valid)
}
// Hostname mismatch on an otherwise-valid chain.
{
opts := x509.Verify_Options{roots = roots, intermediates = inters, current_time = time.unix(CHAIN_NOW, 0), dns_name = "wrong.example.com"}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Hostname_Mismatch)
}
// Required EKU the leaf does not carry (it has serverAuth only).
{
opts := x509.Verify_Options{roots = roots, intermediates = inters, current_time = time.unix(CHAIN_NOW, 0), required_eku = .Client_Auth}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Incompatible_Usage)
}
}
// The trust anchor is validated like any issuer (matching Go/OpenSSL):
// an expired root is rejected, so a chain whose only anchor is expired
// fails to build. (Resilience to expired roots comes from offering an
// alternate valid anchor, not from skipping the check.)
@(test)
test_verify_chain_expired_anchor_rejected :: proc(t: ^testing.T) {
leaf, e1 := x509.parse(EC_EXP_LEAF); defer x509.destroy(&leaf)
root, e2 := x509.parse(EC_EXPIRED_ROOT); defer x509.destroy(&root)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
// Sanity: the anchor really is expired at CHAIN_NOW.
testing.expect_value(t, x509.valid_at(&root, time.unix(CHAIN_NOW, 0)), false)
opts := x509.Verify_Options{roots = {&root}, current_time = time.unix(CHAIN_NOW, 0)}
chain, err := x509.verify_chain(&leaf, opts)
defer delete(chain)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// EKU nesting (surfaced by x509-limbo): an intermediate constrained to
// emailProtection must not be usable to issue a serverAuth leaf when the
// caller requires serverAuth. Without an EKU requirement the chain still
// builds (EKU is unconstrained).
@(test)
test_verify_chain_eku_nesting :: proc(t: ^testing.T) {
root, _ := x509.parse(NEG_ROOT); defer x509.destroy(&root)
inter, _ := x509.parse(NEG_EKU_INTER); defer x509.destroy(&inter)
leaf, _ := x509.parse(NEG_EKU_LEAF); defer x509.destroy(&leaf)
now := time.unix(CHAIN_NOW, 0)
// No EKU requirement: the email-only intermediate does not block it.
{
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = now}
c, err := x509.verify_chain(&leaf, opts)
defer delete(c)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(c), 3)
}
// Requiring serverAuth: the intermediate's emailProtection-only EKU
// forbids it, so the path is rejected.
{
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = now, required_eku = .Server_Auth}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Incompatible_Usage)
}
}
// EKU nesting must be enforced DURING path-building The two intermediates
// share a subject DN and key (so the leaf signature-verifies through
// either), differing only in EKU, and we offer them in BOTH orders, the
// result must be the same successful chain regardless of ordering.
@(test)
test_verify_chain_eku_alt_path :: proc(t: ^testing.T) {
root, _ := x509.parse(NEG_ROOT); defer x509.destroy(&root)
bad, _ := x509.parse(EKU_ALT_BAD); defer x509.destroy(&bad)
good, _ := x509.parse(EKU_ALT_GOOD); defer x509.destroy(&good)
leaf, _ := x509.parse(EKU_ALT_LEAF); defer x509.destroy(&leaf)
now := time.unix(CHAIN_NOW, 0)
// emailProtection-only intermediate offered first:
{
opts := x509.Verify_Options{
roots = {&root},
intermediates = {&bad, &good},
current_time = now,
required_eku = .Server_Auth,
}
c, err := x509.verify_chain(&leaf, opts)
defer delete(c)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(c), 3)
if len(c) == 3 {
testing.expect(t, c[1] == &good, "must select the serverAuth-permitting intermediate")
}
}
// serverAuth intermediate offered first: same successful result.
{
opts := x509.Verify_Options{
roots = {&root},
intermediates = {&good, &bad},
current_time = now,
required_eku = .Server_Auth,
}
c, err := x509.verify_chain(&leaf, opts)
defer delete(c)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, len(c), 3)
if len(c) == 3 {
testing.expect(t, c[1] == &good, "must select the serverAuth-permitting intermediate")
}
}
}
// Name constraints are not decoded, so verify_chain fails CLOSED: a
// chain through a name-constrained CA is rejected even when the leaf's
// name is within the permitted subtree (here a NON-critical NC, so it is
// the explicit NC refusal, not the unhandled-critical path).
@(test)
test_verify_chain_name_constraints_fail_closed :: proc(t: ^testing.T) {
root, _ := x509.parse(NEG_ROOT); defer x509.destroy(&root)
inter, _ := x509.parse(NEG_NC_INTER); defer x509.destroy(&inter)
leaf, _ := x509.parse(NEG_NC_LEAF); defer x509.destroy(&leaf)
// The NC is present but not critical, so it must not have tripped the
// unhandled-critical flag — the refusal is the explicit NC check.
testing.expect(t, !inter.unhandled_critical, "NC is non-critical")
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = time.unix(CHAIN_NOW, 0)}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// A mutually cross-signed CA cycle (neither anchor trusted) must not
// hang the path search: the cycle/depth/signature-budget bounds make it
// terminate with Unknown_Authority. If this test ever hangs rather than
// failing, the search lost its bound. (CVE-2024-0567 shape.)
@(test)
test_verify_chain_cross_signed_cycle :: proc(t: ^testing.T) {
a, _ := x509.parse(CYC_A); defer x509.destroy(&a)
b, _ := x509.parse(CYC_B); defer x509.destroy(&b)
a_by_b, _ := x509.parse(CYC_A_BY_B); defer x509.destroy(&a_by_b)
b_by_a, _ := x509.parse(CYC_B_BY_A); defer x509.destroy(&b_by_a)
leaf, _ := x509.parse(CYC_LEAF); defer x509.destroy(&leaf)
// Every cross-cert and self-signed CA is offered as an intermediate;
// no trust anchor is supplied, so the loop can never terminate at a
// root.
opts := x509.Verify_Options{
intermediates = {&a, &b, &a_by_b, &b_by_a},
current_time = time.unix(CHAIN_NOW, 0),
}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// An RSA-signed leaf surfaces the RSA gap directly.
@(test)
test_verify_chain_rsa_unsupported :: proc(t: ^testing.T) {
rsa, err := x509.parse(RSA_DER); defer x509.destroy(&rsa)
testing.expect_value(t, err, x509.Error.None)
opts := x509.Verify_Options{roots = {&rsa}, current_time = time.unix(CHAIN_NOW, 0)}
c, verr := x509.verify_chain(&rsa, opts); delete(c)
testing.expect_value(t, verr, x509.Error.Unsupported_Algorithm)
}
// Each negative chain must be rejected for the right structural reason
// (all collapse to Unknown_Authority: no usable path to the anchor).
// Together these exercise the RFC 5280 6.1.4 issuer checks — keyCertSign,
// cA, intermediate validity, pathLenConstraint — which the happy-path
// tests never trip.
@(test)
test_verify_chain_bad_issuers :: proc(t: ^testing.T) {
root, _ := x509.parse(NEG_ROOT); defer x509.destroy(&root)
now := time.unix(CHAIN_NOW, 0)
// Intermediate is a CA but its KeyUsage omits keyCertSign.
{
inter, _ := x509.parse(NEG_NOKCS_INTER); defer x509.destroy(&inter)
leaf, _ := x509.parse(NEG_NOKCS_LEAF); defer x509.destroy(&leaf)
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = now}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// Intermediate carries basicConstraints cA=FALSE.
{
inter, _ := x509.parse(NEG_NOTCA_INTER); defer x509.destroy(&inter)
leaf, _ := x509.parse(NEG_NOTCA_LEAF); defer x509.destroy(&leaf)
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = now}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// Intermediate is expired (validity 2010-2015): unlike the trust
// anchor, an intermediate's validity IS enforced.
{
inter, _ := x509.parse(NEG_EXPINTER); defer x509.destroy(&inter)
leaf, _ := x509.parse(NEG_EXPINTER_LEAF); defer x509.destroy(&leaf)
testing.expect_value(t, x509.valid_at(&inter, now), false)
opts := x509.Verify_Options{roots = {&root}, intermediates = {&inter}, current_time = now}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// pathlen:0 on pl_a forbids pl_b (a CA) beneath it.
{
a, _ := x509.parse(NEG_PL_A); defer x509.destroy(&a)
b, _ := x509.parse(NEG_PL_B); defer x509.destroy(&b)
leaf, _ := x509.parse(NEG_PL_LEAF); defer x509.destroy(&leaf)
opts := x509.Verify_Options{roots = {&root}, intermediates = {&a, &b}, current_time = now}
c, err := x509.verify_chain(&leaf, opts); delete(c)
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
}
// Single-byte tamper sweep: no one-byte mutation of a signed certificate
// may BOTH parse cleanly AND verify against its true issuer. The whole
// TBSCertificate is signature-covered and the framing/algorithm/signature
// bytes are structurally checked, so every flip must be caught by parse
// or by the signature. A survivor would mean a trusted region the
// signature does not actually protect.
@(test)
test_verify_signature_tamper_sweep :: proc(t: ^testing.T) {
check :: proc(t: ^testing.T, der, issuer_der: []byte) {
issuer, ierr := x509.parse(issuer_der)
defer x509.destroy(&issuer)
testing.expect_value(t, ierr, x509.Error.None)
buf := make([]byte, len(der))
defer delete(buf)
for pos in 0 ..< len(der) {
copy(buf, der)
buf[pos] ~= 0x01
cert, perr := x509.parse(buf)
if perr == .None {
accepted := x509.verify_signature(&cert, &issuer) == .None
x509.destroy(&cert)
testing.expectf(t, !accepted, "tampered byte %d parsed and verified", pos)
}
}
}
check(t, EC_CHAIN_LEAF, EC_CHAIN_INTER)
check(t, ED_CHAIN_LEAF, ED_CHAIN_ROOT)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/core/crypto/x509/testdata/ec.der vendored Normal file

Binary file not shown.

BIN
tests/core/crypto/x509/testdata/ed.der vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/core/crypto/x509/testdata/rsa.der vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,222 @@
package test_core_asn1
// Deterministic structure-aware fuzzing for the DER reader. The test
// runner seeds context.random_generator and logs the seed, so any
// failure reproduces with -define:ODIN_TEST_RANDOM_SEED=n.
//
// The load-bearing invariant: DER is canonical, so for any element the
// strict reader ACCEPTS, re-encoding the header from (tag, len) must
// reproduce the input bytes exactly. Any acceptance of a non-minimal
// encoding fails the oracle without needing a crash.
import "core:bytes"
import "core:encoding/asn1"
import "core:math/rand"
import "core:testing"
FUZZ_RANDOM_ITERS :: 4096
FUZZ_MUTATE_ITERS :: 2048
FUZZ_MAX_INPUT :: 96
FUZZ_WALK_DEPTH :: 8
// _encode_header re-encodes a DER identifier + length the canonical
// way; used as the acceptance oracle.
@(private="file")
_encode_header :: proc(tag: asn1.Tag, length: int, out: ^[dynamic]byte) {
b := byte(u8(tag.class) << 6)
if tag.constructed {
b |= 0x20
}
if tag.number < 0x1F {
append(out, b | byte(tag.number))
} else {
append(out, b | 0x1F)
// Base-128, big-endian, minimal.
tmp: [5]byte
n := 0
v := tag.number
for {
tmp[n] = byte(v & 0x7F)
n += 1
v >>= 7
if v == 0 {
break
}
}
for i := n - 1; i >= 0; i -= 1 {
c := tmp[i]
if i > 0 {
c |= 0x80
}
append(out, c)
}
}
if length < 0x80 {
append(out, byte(length))
} else {
tmp: [4]byte
n := 0
v := length
for v > 0 {
tmp[n] = byte(v & 0xFF)
n += 1
v >>= 8
}
append(out, 0x80 | byte(n))
for i := n - 1; i >= 0; i -= 1 {
append(out, tmp[i])
}
}
}
// _walk recursively consumes every element in the reader, applying the
// canonical re-encode oracle to each accepted element.
@(private="file")
_walk :: proc(t: ^testing.T, r: ^asn1.Cursor, depth: int, scratch: ^[dynamic]byte) {
for !asn1.is_empty(r) {
start := r.pos
tag, content, err := asn1.read_any(r)
if err != .None {
return
}
element := r.data[start:r.pos]
// Oracle: canonical re-encode must reproduce the element.
clear(scratch)
_encode_header(tag, len(content), scratch)
append(scratch, ..content)
if !bytes.equal(scratch[:], element) {
testing.expectf(t, false, "accepted non-canonical element: % 02x", element)
return
}
if tag.constructed && depth < FUZZ_WALK_DEPTH {
sub := asn1.Cursor{data = content}
_walk(t, &sub, depth + 1, scratch)
}
}
}
@(test)
test_fuzz_read_any_random :: proc(t: ^testing.T) {
buf: [FUZZ_MAX_INPUT]byte
scratch: [dynamic]byte
defer delete(scratch)
for _ in 0 ..< FUZZ_RANDOM_ITERS {
n := rand.int_max(FUZZ_MAX_INPUT + 1)
input := buf[:n]
for i in 0 ..< n {
input[i] = byte(rand.uint32())
}
// Bias half the inputs towards plausible structure: a known
// universal tag and a length that fits.
if n >= 2 && rand.int_max(2) == 0 {
tags := [?]byte{0x02, 0x03, 0x04, 0x05, 0x06, 0x17, 0x18, 0x30, 0x31, 0xA0}
input[0] = rand.choice(tags[:])
input[1] = byte(rand.int_max(n))
}
r: asn1.Cursor
asn1.cursor_init(&r, input)
_walk(t, &r, 0, &scratch)
}
}
@(test)
test_fuzz_typed_readers_random :: proc(t: ^testing.T) {
buf: [FUZZ_MAX_INPUT]byte
for _ in 0 ..< FUZZ_RANDOM_ITERS {
n := rand.int_max(FUZZ_MAX_INPUT + 1)
input := buf[:n]
for i in 0 ..< n {
input[i] = byte(rand.uint32())
}
// Every typed reader must fail cleanly or uphold its contract;
// none may panic. Fresh reader per call: a failed read may
// leave the cursor mid-element by design.
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if v, err := asn1.read_i64(&r); err == .None {
_ = v
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if mag, err := asn1.read_unsigned_integer_bytes(&r); err == .None {
// Magnitude is minimal: no leading zero unless the
// value IS zero.
if len(mag) > 1 {
testing.expect(t, mag[0] != 0x00, "non-minimal magnitude")
}
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if bits, unused, err := asn1.read_bit_string(&r); err == .None {
testing.expect(t, unused <= 7)
if unused > 0 {
testing.expect(t, len(bits) > 0)
mask := byte(1 << uint(unused)) - 1
testing.expect(t, bits[len(bits) - 1] & mask == 0, "padding bits set")
}
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if raw, err := asn1.read_oid(&r); err == .None {
// Layer contract: structural acceptance by read_oid
// means the decoders yield either arcs or Arc_Overflow
// (X.660 arcs are unbounded) — never a structural error.
// This invariant caught a real bug on this fuzzer's
// first run.
arcs, aerr := asn1.oid_components(raw)
str, serr := asn1.oid_to_string(raw)
testing.expect(t, aerr == .None || aerr == .Arc_Overflow, "components: structural error after acceptance")
testing.expect_value(t, serr, aerr)
if aerr == .None {
testing.expect(t, len(arcs) >= 2)
testing.expect(t, len(str) >= 3)
}
delete(arcs)
delete(str)
}
}
{
r: asn1.Cursor
asn1.cursor_init(&r, input)
if _, err := asn1.read_time(&r); err == .None {
// Acceptance implies the RFC 5280 profile already
// validated ranges; nothing further to check here.
continue
}
}
}
}
@(test)
test_fuzz_mutated_spki :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
buf := make([]byte, len(spki))
defer delete(buf)
for _ in 0 ..< FUZZ_MUTATE_ITERS {
copy(buf, spki[:])
// 1-8 random byte mutations; structure mostly survives, so the
// parser gets dragged deep before hitting the damage.
for _ in 0 ..< 1 + rand.int_max(8) {
buf[rand.int_max(len(buf))] = byte(rand.uint32())
}
// Must never panic; success or clean error are both fine.
_, _ = parse_spki(buf)
}
}

View File

@@ -0,0 +1,158 @@
package test_core_asn1
// Deterministic fuzzing for the DER WRITER. The runner seeds
// context.random_generator and logs the seed, so any failure reproduces
// with -define:ODIN_TEST_RANDOM_SEED=n. Run under -sanitize:address to turn
// the value-tree's borrow discipline into a checked property: a stray
// borrow of an out-of-scope composite-literal temporary is a stack
// use-after-scope ASan would catch here.
//
// Invariants:
// - marshal of any constructed tree never crashes / writes out of bounds;
// - the output is well-formed DER (a read_any walk consumes it exactly);
// - leaf values survive a marshal -> read round-trip.
import "core:bytes"
import "core:encoding/asn1"
import "core:math/rand"
import "core:testing"
import "core:time"
WRITER_FUZZ_ITERS :: 2048
WRITER_FUZZ_DEPTH :: 6
// _gen_bytes allocates 0..max random bytes from the temp arena.
@(private = "file")
_gen_bytes :: proc(max: int) -> []byte {
n := rand.int_max(max)
b := make([]byte, n, context.temp_allocator)
for i in 0 ..< n {
b[i] = byte(rand.int_max(256))
}
return b
}
@(private = "file")
_gen_leaf :: proc() -> asn1.Value {
switch rand.int_max(7) {
case 0:
return asn1.boolean(rand.int_max(2) == 1)
case 1:
return asn1.null()
case 2:
return asn1.integer_unsigned(_gen_bytes(20))
case 3:
return asn1.octet_string(_gen_bytes(20))
case 4:
return asn1.bit_string_octets(_gen_bytes(20))
case 5:
return asn1.object_identifier(_gen_bytes(12)) // raw OID octets: read_any tolerates any content
case:
return asn1.generalized_time(time.unix(i64(rand.int_max(2_000_000_000)), 0))
}
}
// _gen_value builds a random tree; constructed nodes draw their children
// arrays from the temp arena (pre-sized, so the slices set() / sequence()
// borrow never move), freed wholesale after each iteration.
@(private = "file")
_gen_value :: proc(depth: int) -> asn1.Value {
if depth <= 0 || rand.int_max(3) == 0 {
return _gen_leaf()
}
n := rand.int_max(4) // 0..3 children
kids := make([]asn1.Value, n, context.temp_allocator)
for i in 0 ..< n {
kids[i] = _gen_value(depth - 1)
}
switch rand.int_max(5) {
case 0:
return asn1.sequence(kids)
case 1:
return asn1.set(kids)
case 2:
return asn1.context_explicit(u32(rand.int_max(8)), kids)
case 3:
return asn1.bit_string_wrap(kids)
case:
sv, serr := asn1.set_of(kids, context.temp_allocator) // allocates scratch + sorts in place
if serr != .None {
return asn1.set(kids)
}
return sv
}
}
// _walk recurses through a marshalled tree with read_any, asserting every
// element frames cleanly and the input is consumed exactly.
@(private = "file")
_walk :: proc(t: ^testing.T, data: []byte, depth: int) {
cur: asn1.Cursor
asn1.cursor_init(&cur, data)
for !asn1.is_empty(&cur) {
tag, content, err := asn1.read_any(&cur)
testing.expect_value(t, err, asn1.Error.None)
if err != .None {
return
}
if tag.constructed && depth > 0 {
_walk(t, content, depth - 1)
}
}
}
@(test)
test_fuzz_writer_wellformed :: proc(t: ^testing.T) {
for _ in 0 ..< WRITER_FUZZ_ITERS {
tree := _gen_value(WRITER_FUZZ_DEPTH)
out, err := asn1.marshal(tree)
testing.expect_value(t, err, asn1.Error.None)
if err == .None {
testing.expect_value(t, len(out), asn1.encoded_len(tree)) // sizing == emission
_walk(t, out, 64)
delete(out)
}
free_all(context.temp_allocator)
}
}
@(test)
test_fuzz_writer_roundtrip :: proc(t: ^testing.T) {
for _ in 0 ..< WRITER_FUZZ_ITERS {
// OCTET STRING: content survives verbatim.
payload := _gen_bytes(32)
if o, e := asn1.marshal(asn1.octet_string(payload)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_octet_string(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
testing.expect(t, bytes.equal(got, payload), "octet string round-trip")
delete(o)
}
// BIT STRING (whole octets): payload survives verbatim.
bits := _gen_bytes(32)
if o, e := asn1.marshal(asn1.bit_string_octets(bits)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_bit_string_octets(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect(t, bytes.equal(got, bits), "bit string round-trip")
delete(o)
}
// BOOLEAN.
bv := rand.int_max(2) == 1
if o, e := asn1.marshal(asn1.boolean(bv)); e == .None {
cur: asn1.Cursor
asn1.cursor_init(&cur, o)
got, re := asn1.read_boolean(&cur)
testing.expect_value(t, re, asn1.Error.None)
testing.expect_value(t, got, bv)
delete(o)
}
free_all(context.temp_allocator)
}
}

View File

@@ -0,0 +1,119 @@
package test_core_asn1
// Out-of-memory robustness for the allocating OID helpers. A failing
// allocator wraps a tracking allocator; sweeping the fail point across
// every allocation (including the Builder's internal growth in
// oid_to_string) must yield Allocation_Failed with nothing leaked.
import "base:runtime"
import "core:mem"
import "core:encoding/asn1"
import "core:testing"
@(private="file")
Failing_Allocator :: struct {
backing: runtime.Allocator,
count: int,
fail_at: int,
}
@(private="file")
failing_allocator_proc :: proc(
data: rawptr, mode: runtime.Allocator_Mode,
size, alignment: int, old_memory: rawptr, old_size: int,
loc := #caller_location,
) -> ([]byte, runtime.Allocator_Error) {
fa := (^Failing_Allocator)(data)
#partial switch mode {
case .Alloc, .Alloc_Non_Zeroed, .Resize, .Resize_Non_Zeroed:
if fa.count == fa.fail_at {
fa.count += 1
return nil, .Out_Of_Memory
}
fa.count += 1
}
return fa.backing.procedure(fa.backing.data, mode, size, alignment, old_memory, old_size, loc)
}
@(private="file")
failing_allocator :: proc(fa: ^Failing_Allocator) -> runtime.Allocator {
return {procedure = failing_allocator_proc, data = fa}
}
// rsaEncryption (1.2.840.113549.1.1.1) — seven arcs rendering to a
// 20-char string, enough to drive the Builder in oid_to_string past
// its initial capacity so the resize path is on the sweep.
@(private="file")
LONG_OID := []byte{0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
@(test)
test_oom_oid_components :: proc(t: ^testing.T) {
raw: asn1.Cursor
asn1.cursor_init(&raw, LONG_OID)
oid, oerr := asn1.read_oid(&raw)
testing.expect_value(t, oerr, asn1.Error.None)
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
arcs, err := asn1.oid_components(oid, failing_allocator(&fa))
testing.expect_value(t, err, asn1.Error.None)
delete(arcs, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 1)
testing.expect_value(t, len(track.allocation_map), 0)
}
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
arcs, err := asn1.oid_components(oid, failing_allocator(&fa))
if err == .None {
delete(arcs, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed, "k=%d: got %v", k, err)
}
testing.expectf(t, len(track.allocation_map) == 0, "k=%d: %d leaked", k, len(track.allocation_map))
}
}
@(test)
test_oom_oid_to_string :: proc(t: ^testing.T) {
raw: asn1.Cursor
asn1.cursor_init(&raw, LONG_OID)
oid, oerr := asn1.read_oid(&raw)
testing.expect_value(t, oerr, asn1.Error.None)
total: int
{
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = -1}
str, err := asn1.oid_to_string(oid, failing_allocator(&fa))
testing.expect_value(t, err, asn1.Error.None)
delete(str, failing_allocator(&fa))
total = fa.count
testing.expect(t, total >= 1)
testing.expect_value(t, len(track.allocation_map), 0)
}
for k in 0 ..< total {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
fa := Failing_Allocator{backing = mem.tracking_allocator(&track), fail_at = k}
str, err := asn1.oid_to_string(oid, failing_allocator(&fa))
if err == .None {
delete(str, failing_allocator(&fa))
} else {
testing.expectf(t, err == .Allocation_Failed, "k=%d: got %v", k, err)
}
testing.expectf(t, len(track.allocation_map) == 0, "k=%d: %d leaked", k, len(track.allocation_map))
}
}

View File

@@ -0,0 +1,532 @@
package test_core_asn1
import "core:encoding/asn1"
import "core:testing"
import "core:time"
// ============================================================
// Tag and length forms
// ============================================================
@(test)
test_tag_forms :: proc(t: ^testing.T) {
// Low tag, primitive universal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x05})
tag, content, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag, asn1.universal(.Integer))
testing.expect_value(t, len(content), 1)
testing.expect_value(t, asn1.done(&r), asn1.Error.None)
}
// Constructed context-specific [0].
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0xA0, 0x00})
tag, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag, asn1.context_specific(0))
}
// High-tag-number form: [31] primitive → 9F 1F.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x1F, 0x00})
tag, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, tag.number, u32(31))
}
// High-tag form used for a number < 31 is non-minimal → invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x1E, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Tag)
}
// High-tag form with 0x80 lead continuation octet is non-minimal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x9F, 0x80, 0x1F, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Tag)
}
}
@(test)
test_length_forms :: proc(t: ^testing.T) {
// Long-form length for 128 bytes: 81 80.
{
buf: [131]byte
buf[0] = 0x04
buf[1] = 0x81
buf[2] = 0x80
r: asn1.Cursor
asn1.cursor_init(&r, buf[:])
_, content, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(content), 128)
}
// Long form for a short length (81 05) is non-minimal.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x04, 0x81, 0x05, 1, 2, 3, 4, 5})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Leading zero octet in long form (82 00 80) is non-minimal.
{
buf: [131]byte
buf[0] = 0x04
buf[1] = 0x82
buf[2] = 0x00
buf[3] = 0x80
r: asn1.Cursor
asn1.cursor_init(&r, buf[:])
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Indefinite length (80) is BER, never DER.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x30, 0x80, 0x00, 0x00})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Length)
}
// Length past end of input.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x04, 0x05, 1, 2})
_, _, err := asn1.read_any(&r)
testing.expect_value(t, err, asn1.Error.Truncated)
}
}
// ============================================================
// Scalar types
// ============================================================
@(test)
test_boolean :: proc(t: ^testing.T) {
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0xFF})
v, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, v, true)
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0x00})
v, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, v, false)
}
// DER: any value other than 0x00/0xFF is invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x01, 0x01})
_, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Boolean)
}
// Wrong width.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x01, 0x02, 0xFF, 0xFF})
_, err := asn1.read_boolean(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Boolean)
}
}
@(test)
test_integer :: proc(t: ^testing.T) {
Case :: struct {
der: []byte,
value: i64,
err: asn1.Error,
}
cases := []Case{
{der = {0x02, 0x01, 0x00}, value = 0},
{der = {0x02, 0x01, 0x7F}, value = 127},
{der = {0x02, 0x02, 0x00, 0x80}, value = 128},
{der = {0x02, 0x01, 0x80}, value = -128},
{der = {0x02, 0x02, 0xFF, 0x7F}, value = -129},
{der = {0x02, 0x08, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, value = max(i64)},
{der = {0x02, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, value = min(i64)},
// Non-minimal: redundant leading 0x00 / 0xFF.
{der = {0x02, 0x02, 0x00, 0x05}, err = .Invalid_Integer},
{der = {0x02, 0x02, 0xFF, 0x85}, err = .Invalid_Integer},
// Empty content.
{der = {0x02, 0x00}, err = .Invalid_Integer},
// Too wide for i64.
{der = {0x02, 0x09, 0x01, 0, 0, 0, 0, 0, 0, 0, 0}, err = .Integer_Overflow},
}
for c in cases {
r: asn1.Cursor
asn1.cursor_init(&r, c.der)
v, err := asn1.read_i64(&r)
testing.expect_value(t, err, c.err)
if c.err == .None {
testing.expect_value(t, v, c.value)
}
}
}
@(test)
test_unsigned_integer :: proc(t: ^testing.T) {
// 0x00 sign octet stripped: 255 encodes as 02 02 00 FF.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x02, 0x00, 0xFF})
mag, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(mag), 1)
testing.expect_value(t, mag[0], u8(0xFF))
}
// Zero stays one octet.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x00})
mag, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(mag), 1)
testing.expect_value(t, mag[0], u8(0x00))
}
// Negative rejected.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x02, 0x01, 0x80})
_, err := asn1.read_unsigned_integer_bytes(&r)
testing.expect_value(t, err, asn1.Error.Negative_Integer)
}
}
@(test)
test_bit_string :: proc(t: ^testing.T) {
// Whole octets: 03 03 00 A0 0F.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x03, 0x00, 0xA0, 0x0F})
octets, err := asn1.read_bit_string_octets(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(octets), 2)
}
// 4 unused bits, correctly zero-padded: A0 = 1010_0000.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA0})
bits, unused, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, unused, 4)
testing.expect_value(t, len(bits), 1)
}
// Non-zero padding bits violate DER.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA1})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Unused count > 7.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x08, 0xA0})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Empty payload must declare zero unused bits.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x01, 0x03})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// Empty content entirely.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x00})
_, _, err := asn1.read_bit_string(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
// PKIX shape requires unused == 0.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x03, 0x02, 0x04, 0xA0})
_, err := asn1.read_bit_string_octets(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Bit_String)
}
}
@(test)
test_null :: proc(t: ^testing.T) {
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x05, 0x00})
testing.expect_value(t, asn1.read_null(&r), asn1.Error.None)
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x05, 0x01, 0x00})
testing.expect_value(t, asn1.read_null(&r), asn1.Error.Invalid_Null)
}
}
// ============================================================
// OBJECT IDENTIFIER
// ============================================================
@(test)
test_oid :: proc(t: ^testing.T) {
// rsaEncryption: 1.2.840.113549.1.1.1
rsa_encryption := []byte{0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
{
r: asn1.Cursor
asn1.cursor_init(&r, rsa_encryption)
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(raw), 9)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "1.2.840.113549.1.1.1")
arcs, aerr := asn1.oid_components(raw)
defer delete(arcs)
testing.expect_value(t, aerr, asn1.Error.None)
testing.expect_value(t, len(arcs), 7)
testing.expect_value(t, arcs[3], u64(113549))
}
// ecPublicKey: 1.2.840.10045.2.1
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01})
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "1.2.840.10045.2.1")
}
// Arc-2 base offset: 2.5.4.3 (id-at-commonName) → 55 04 03.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x03, 0x55, 0x04, 0x03})
raw, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.None)
str, serr := asn1.oid_to_string(raw)
defer delete(str)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, str, "2.5.4.3")
}
// Empty content is invalid.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x00})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
// Non-minimal subidentifier (leading 0x80 continuation octet).
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x03, 0x2A, 0x80, 0x01})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
// Truncated subidentifier (continuation bit set on last octet).
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x06, 0x02, 0x2A, 0x86})
_, err := asn1.read_oid(&r)
testing.expect_value(t, err, asn1.Error.Invalid_Object_Identifier)
}
}
// ============================================================
// Time
// ============================================================
@(test)
test_time :: proc(t: ^testing.T) {
// Times are returned as time.Time; compare via Unix seconds.
// Reference epochs computed independently (e.g.
// `date -u -d '1999-01-01T00:00:00Z' +%s`). UTCTime century window:
// 990101000000Z → 1999, 490101000000Z → 2049.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x17, 0x0D, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_utc_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(915148800)) // 1999-01-01T00:00:00Z
}
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x17, 0x0D, '4', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_utc_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(2493072000)) // 2049-01-01T00:00:00Z
}
// GeneralizedTime: 20260612153000Z.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '2', '0', '2', '6', '0', '6', '1', '2', '1', '5', '3', '0', '0', '0', 'Z'})
v, err := asn1.read_generalized_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(1781278200)) // 2026-06-12T15:30:00Z
}
// read_time dispatches on tag.
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '2', '0', '5', '0', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'})
v, err := asn1.read_time(&r)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(v), i64(2524608000)) // 2050-01-01T00:00:00Z
}
// Far-future dates (year 9999, the RFC 5280 no-expiration sentinel)
// must still parse, but time.Time tops out near year 2262, so the
// value SATURATES rather than carrying the literal 9999 epoch. This
// is an intentional limitation (see asn1's _time_from_unix): the
// 9999 sentinel just needs to read as "effectively never expires".
{
r: asn1.Cursor
asn1.cursor_init(&r, []byte{0x18, 0x0F, '9', '9', '9', '9', '1', '2', '3', '1', '2', '3', '5', '9', '5', '9', 'Z'})
v, err := asn1.read_generalized_time(&r)
testing.expect_value(t, err, asn1.Error.None)
// Saturated near year 2262, NOT the true 9999 epoch (253402300799).
testing.expect(t, time.to_unix_seconds(v) >= i64(9_223_372_036), "9999 saturates to time.Time max")
testing.expect(t, time.to_unix_seconds(v) < i64(253402300799), "saturated, not the literal 9999 epoch")
}
// RFC 5280 profile rejections: offset instead of Z, missing
// seconds, fractional seconds, month/day out of range.
bad := [][]byte{
{0x17, 0x11, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', '+', '0', '1', '0', '0'},
{0x17, 0x0B, '9', '9', '0', '1', '0', '1', '0', '0', '0', '0', 'Z'},
{0x18, 0x12, '2', '0', '2', '6', '0', '6', '1', '2', '1', '5', '3', '0', '0', '0', '.', '5', '0', 'Z'},
{0x17, 0x0D, '9', '9', '1', '3', '0', '1', '0', '0', '0', '0', '0', '0', 'Z'},
{0x17, 0x0D, '9', '9', '0', '1', '0', '0', '0', '0', '0', '0', '0', '0', 'Z'},
{0x17, 0x0D, '9', '9', '0', '1', '0', '1', '2', '4', '0', '0', '0', '0', 'Z'},
}
for der in bad {
r: asn1.Cursor
asn1.cursor_init(&r, der)
_, err := asn1.read_time(&r)
testing.expect(t, err != .None, "malformed time must be rejected")
}
}
// ============================================================
// Compound structures
// ============================================================
// A real SubjectPublicKeyInfo for an EC P-256 key:
// SEQUENCE {
// SEQUENCE { OID ecPublicKey, OID prime256v1 }
// BIT STRING (65 octets of uncompressed point, 0 unused bits)
// }
@(private)
make_spki :: proc(allocator := context.allocator) -> [dynamic]byte {
out: [dynamic]byte
out.allocator = allocator
append(&out, 0x30, 0x59) // SEQUENCE, len 89
append(&out, 0x30, 0x13) // SEQUENCE, len 19
append(&out, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01) // ecPublicKey
append(&out, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07) // prime256v1
append(&out, 0x03, 0x42, 0x00) // BIT STRING, len 66, 0 unused
append(&out, 0x04) // uncompressed point marker
for i in 0 ..< 64 {
append(&out, u8(i))
}
return out
}
@(private)
parse_spki :: proc(data: []byte) -> (point: []byte, err: asn1.Error) {
r: asn1.Cursor
asn1.cursor_init(&r, data)
spki, serr := asn1.read_sequence(&r)
if serr != .None {
return nil, serr
}
if derr := asn1.done(&r); derr != .None {
return nil, derr
}
alg, aerr := asn1.read_sequence(&spki)
if aerr != .None {
return nil, aerr
}
alg_oid, oerr := asn1.read_oid(&alg)
if oerr != .None {
return nil, oerr
}
_ = alg_oid
curve_oid, cerr := asn1.read_oid(&alg)
if cerr != .None {
return nil, cerr
}
_ = curve_oid
if derr := asn1.done(&alg); derr != .None {
return nil, derr
}
key, kerr := asn1.read_bit_string_octets(&spki)
if kerr != .None {
return nil, kerr
}
if derr := asn1.done(&spki); derr != .None {
return nil, derr
}
return key, .None
}
@(test)
test_spki_walk :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
point, err := parse_spki(spki[:])
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, len(point), 65)
testing.expect_value(t, point[0], u8(0x04))
}
// Every truncation of a valid structure must error cleanly — never
// panic, never succeed.
@(test)
test_truncation_sweep :: proc(t: ^testing.T) {
spki := make_spki()
defer delete(spki)
for n in 0 ..< len(spki) {
_, err := parse_spki(spki[:n])
testing.expect(t, err != .None, "truncated input must be rejected")
}
}
@(test)
test_explicit_optional :: proc(t: ^testing.T) {
// [0] EXPLICIT INTEGER 2 (the X.509 version field shape), then an
// INTEGER at the outer level.
der := []byte{0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x07}
r: asn1.Cursor
asn1.cursor_init(&r, der)
inner, present, err := asn1.read_explicit(&r, 0)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, present, true)
version, verr := asn1.read_i64(&inner)
testing.expect_value(t, verr, asn1.Error.None)
testing.expect_value(t, version, i64(2))
testing.expect_value(t, asn1.done(&inner), asn1.Error.None)
// Absent optional: next element is [1]? No — it's the INTEGER, so
// read_explicit(1) must not consume.
_, present2, err2 := asn1.read_explicit(&r, 1)
testing.expect_value(t, err2, asn1.Error.None)
testing.expect_value(t, present2, false)
serial, serr := asn1.read_i64(&r)
testing.expect_value(t, serr, asn1.Error.None)
testing.expect_value(t, serial, i64(7))
testing.expect_value(t, asn1.done(&r), asn1.Error.None)
}

View File

@@ -0,0 +1,298 @@
package test_core_asn1
import "core:bytes"
import "core:encoding/asn1"
import "core:testing"
import "core:time"
@(private = "file")
_marshal :: proc(t: ^testing.T, v: asn1.Value) -> []byte {
out, err := asn1.marshal(v)
testing.expect_value(t, err, asn1.Error.None)
return out
}
@(private = "file")
_expect_der :: proc(t: ^testing.T, v: asn1.Value, want: []byte) {
got := _marshal(t, v)
defer delete(got)
testing.expect_value(t, len(got), asn1.encoded_len(v)) // encoded_len must match what was written
testing.expectf(t, bytes.equal(got, want), "got %x, want %x", got, want)
}
// INTEGER from an unsigned magnitude: minimal stripping + sign-octet
// insertion, the inverse of read_unsigned_integer_bytes.
@(test)
test_writer_integer_unsigned :: proc(t: ^testing.T) {
_expect_der(t, asn1.integer_unsigned({}), {0x02, 0x01, 0x00}) // empty -> 0
_expect_der(t, asn1.integer_unsigned({0x00}), {0x02, 0x01, 0x00}) // 0
_expect_der(t, asn1.integer_unsigned({0x00, 0x00}), {0x02, 0x01, 0x00}) // all-zero -> 0
_expect_der(t, asn1.integer_unsigned({0x2A}), {0x02, 0x01, 0x2A}) // 42
_expect_der(t, asn1.integer_unsigned({0x00, 0x2A}), {0x02, 0x01, 0x2A}) // strip leading zero
_expect_der(t, asn1.integer_unsigned({0x80}), {0x02, 0x02, 0x00, 0x80}) // 128: insert sign octet
_expect_der(t, asn1.integer_unsigned({0xFF, 0xFF}), {0x02, 0x03, 0x00, 0xFF, 0xFF}) // 65535
_expect_der(t, asn1.integer_unsigned({0x01, 0x00, 0x01}), {0x02, 0x03, 0x01, 0x00, 0x01}) // 65537 (RSA e)
}
// Length octets: short form below 128, otherwise minimal long form. Drive
// the boundary with OCTET STRINGs of crafted sizes and check the header.
@(test)
test_writer_length_forms :: proc(t: ^testing.T) {
cases := []struct {
n: int,
head: []byte,
} {
{0, {0x04, 0x00}},
{1, {0x04, 0x01}},
{127, {0x04, 0x7F}}, // last short-form length
{128, {0x04, 0x81, 0x80}}, // first long form
{255, {0x04, 0x81, 0xFF}},
{256, {0x04, 0x82, 0x01, 0x00}},
{300, {0x04, 0x82, 0x01, 0x2C}},
}
for c in cases {
content := make([]byte, c.n)
defer delete(content)
got := _marshal(t, asn1.octet_string(content))
defer delete(got)
testing.expectf(t, len(got) >= len(c.head), "n=%d: short output", c.n)
testing.expectf(t, bytes.equal(got[:len(c.head)], c.head), "n=%d: header %x, want %x", c.n, got[:len(c.head)], c.head)
testing.expect_value(t, len(got), len(c.head) + c.n)
}
}
// encode into a buffer one byte too small must write nothing and report it.
@(test)
test_writer_buffer_too_small :: proc(t: ^testing.T) {
v := asn1.integer_unsigned({0x80}) // encodes to 4 bytes
need := asn1.encoded_len(v)
testing.expect_value(t, need, 4)
short := make([]byte, need - 1)
defer delete(short)
n, err := asn1.encode(v, short)
testing.expect_value(t, err, asn1.Error.Buffer_Too_Small)
testing.expect_value(t, n, 0)
exact := make([]byte, need)
defer delete(exact)
n2, err2 := asn1.encode(v, exact)
testing.expect_value(t, err2, asn1.Error.None)
testing.expect_value(t, n2, need)
}
// Round-trip every write back through the cursor reader: SEQUENCE { INTEGER
// r, INTEGER s }, the ECDSA signature shape, with s's top bit set so a sign
// octet is inserted on write and stripped on read.
@(test)
test_writer_roundtrip_ecdsa_sig :: proc(t: ^testing.T) {
r := []byte{0x01, 0x23, 0x45, 0x67}
s := []byte{0x80, 0x00, 0x00, 0x01} // top bit set
sig := _marshal(t, asn1.sequence({asn1.integer_unsigned(r), asn1.integer_unsigned(s)}))
defer delete(sig)
// s gained a 0x00 sign octet: 2 + (2+4) + (2+5) = 15 bytes.
testing.expect_value(t, len(sig), 15)
cur: asn1.Cursor
asn1.cursor_init(&cur, sig)
seq, serr := asn1.read_sequence(&cur)
testing.expect_value(t, serr, asn1.Error.None)
gr, rerr := asn1.read_unsigned_integer_bytes(&seq)
gs, srerr := asn1.read_unsigned_integer_bytes(&seq)
testing.expect_value(t, rerr, asn1.Error.None)
testing.expect_value(t, srerr, asn1.Error.None)
testing.expect_value(t, asn1.done(&seq), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
testing.expect(t, bytes.equal(gr, r), "r round-trips")
testing.expect(t, bytes.equal(gs, s), "s round-trips")
}
// Structural Tier 1 encoders: NULL, OID passthrough, BIT STRING (whole
// octets), SET, and the context-specific [n] wrappers, by exact bytes.
@(test)
test_writer_structural :: proc(t: ^testing.T) {
_expect_der(t, asn1.null(), {0x05, 0x00})
// BIT STRING, whole octets: 03 <len+1> 00 <payload>.
_expect_der(t, asn1.bit_string_octets({0xCA, 0xFE}), {0x03, 0x03, 0x00, 0xCA, 0xFE})
_expect_der(t, asn1.bit_string_octets({}), {0x03, 0x01, 0x00})
// OID passthrough: rsaEncryption (1.2.840.113549.1.1.1) content octets.
rsa_oid := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01}
_expect_der(t, asn1.object_identifier(rsa_oid), {0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01})
// [0] IMPLICIT primitive, and [0] EXPLICIT wrapping INTEGER 2.
_expect_der(t, asn1.context_primitive(0, {0xAB, 0xCD}), {0x80, 0x02, 0xAB, 0xCD})
_expect_der(t, asn1.context_explicit(0, {asn1.integer_unsigned({0x02})}), {0xA0, 0x03, 0x02, 0x01, 0x02})
// SET emits in the given order (single/pre-sorted element is the contract).
_expect_der(t, asn1.set({asn1.boolean(false)}), {0x31, 0x03, 0x01, 0x01, 0x00})
}
// A SubjectPublicKeyInfo-shaped tree round-trips through the reader:
// SEQUENCE { SEQUENCE { OID, NULL }, BIT STRING }.
@(test)
test_writer_spki_shape :: proc(t: ^testing.T) {
oid := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01} // rsaEncryption
key := []byte{0x30, 0x06, 0x02, 0x01, 0x2A, 0x02, 0x01, 0x03} // opaque inner key octets
der := _marshal(t, asn1.sequence({asn1.sequence({asn1.object_identifier(oid), asn1.null()}), asn1.bit_string_octets(key)}))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
spki, e0 := asn1.read_sequence(&cur)
testing.expect_value(t, e0, asn1.Error.None)
alg, e1 := asn1.read_sequence(&spki)
testing.expect_value(t, e1, asn1.Error.None)
got_oid, e2 := asn1.read_oid(&alg)
testing.expect_value(t, e2, asn1.Error.None)
testing.expect(t, bytes.equal(got_oid, oid), "oid round-trips")
testing.expect_value(t, asn1.read_null(&alg), asn1.Error.None)
testing.expect_value(t, asn1.done(&alg), asn1.Error.None)
got_key, e3 := asn1.read_bit_string_octets(&spki)
testing.expect_value(t, e3, asn1.Error.None)
testing.expect(t, bytes.equal(got_key, key), "key bits round-trip")
testing.expect_value(t, asn1.done(&spki), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
}
// time() auto-selects UTCTime (<2050) vs GeneralizedTime (>=2050) per RFC 5280.
@(test)
test_writer_time_auto :: proc(t: ^testing.T) {
// 2049-12-31T23:59:59Z -> UTCTime (tag 0x17).
before := _marshal(t, asn1.time(time.unix(2524607999, 0)))
defer delete(before)
testing.expect_value(t, before[0], u8(0x17))
// 2050-01-01T00:00:00Z -> GeneralizedTime (tag 0x18).
after := _marshal(t, asn1.time(time.unix(2524608000, 0)))
defer delete(after)
testing.expect_value(t, after[0], u8(0x18))
}
// set_of sorts components into DER canonical order (X.690 11.6, by encoding).
@(test)
test_writer_set_of :: proc(t: ^testing.T) {
// Integers given 3,1,2 must emit sorted 1,2,3.
kids := [3]asn1.Value{asn1.integer_unsigned({0x03}), asn1.integer_unsigned({0x01}), asn1.integer_unsigned({0x02})}
v, err := asn1.set_of(kids[:])
testing.expect_value(t, err, asn1.Error.None)
out := _marshal(t, v)
defer delete(out)
cur: asn1.Cursor
asn1.cursor_init(&cur, out)
s, e := asn1.read_set(&cur)
testing.expect_value(t, e, asn1.Error.None)
for want in ([]byte{0x01, 0x02, 0x03}) {
got, ge := asn1.read_unsigned_integer_bytes(&s)
testing.expect_value(t, ge, asn1.Error.None)
testing.expect(t, len(got) == 1 && got[0] == want, "sorted ascending")
}
testing.expect_value(t, asn1.done(&s), asn1.Error.None)
// Ordering is by ENCODING, not value: 2 (02 01 02) sorts before 256
// (02 02 01 00) because the length octet 0x01 < 0x02.
kids2 := [2]asn1.Value{asn1.integer_unsigned({0x01, 0x00}), asn1.integer_unsigned({0x02})}
v2, err2 := asn1.set_of(kids2[:])
testing.expect_value(t, err2, asn1.Error.None)
_expect_der(t, v2, {0x31, 0x07, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, 0x00})
}
// raw() splices a complete pre-encoded element in verbatim, the composition
// primitive for nesting independently-marshalled structures.
@(test)
test_writer_raw :: proc(t: ^testing.T) {
pre := []byte{0x02, 0x01, 0x2A} // a pre-encoded INTEGER 42
_expect_der(t, asn1.raw(pre), {0x02, 0x01, 0x2A})
// embedded beside another value inside a SEQUENCE.
_expect_der(t, asn1.sequence({asn1.raw(pre), asn1.boolean(true)}), {0x30, 0x06, 0x02, 0x01, 0x2A, 0x01, 0x01, 0xFF})
}
@(private = "file")
_expect_time :: proc(t: ^testing.T, v: asn1.Value, tag: byte, ascii: string) {
got := _marshal(t, v)
defer delete(got)
want := make([]byte, 2 + len(ascii))
defer delete(want)
want[0] = tag
want[1] = byte(len(ascii))
copy(want[2:], transmute([]byte)ascii)
testing.expectf(t, bytes.equal(got, want), "got %x, want %x", got, want)
}
// UTCTime / GeneralizedTime formatting (option B: time.Time formatted into
// the output at emit) by exact bytes, plus a round-trip through the reader.
// Tags: UTCTime 0x17, GeneralizedTime 0x18.
@(test)
test_writer_time :: proc(t: ^testing.T) {
epoch := time.unix(0, 0) // 1970-01-01 00:00:00Z
_expect_time(t, asn1.generalized_time(epoch), 0x18, "19700101000000Z")
_expect_time(t, asn1.utc_time(epoch), 0x17, "700101000000Z")
y2027 := time.unix(1798761600, 0) // 2027-01-01 00:00:00Z
_expect_time(t, asn1.generalized_time(y2027), 0x18, "20270101000000Z")
_expect_time(t, asn1.utc_time(y2027), 0x17, "270101000000Z")
// Nonzero month/day/time-of-day: 2023-11-14 22:13:20Z.
tod := time.unix(1700000000, 0)
_expect_time(t, asn1.generalized_time(tod), 0x18, "20231114221320Z")
// Round-trip both forms through the reader.
{
der := _marshal(t, asn1.generalized_time(tod))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
got, err := asn1.read_generalized_time(&cur)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(got), i64(1700000000))
}
{
der := _marshal(t, asn1.utc_time(y2027))
defer delete(der)
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
got, err := asn1.read_utc_time(&cur)
testing.expect_value(t, err, asn1.Error.None)
testing.expect_value(t, time.to_unix_seconds(got), i64(1798761600))
}
}
// Nesting + a second leaf type: SEQUENCE { BOOLEAN, SEQUENCE { INTEGER } }.
// Re-encoding the parsed structure must reproduce the bytes exactly
// (DER is canonical), which is the core correctness property for a writer.
@(test)
test_writer_nested_and_idempotent :: proc(t: ^testing.T) {
inner := asn1.sequence({asn1.integer_unsigned({0x2A})})
outer := asn1.sequence({asn1.boolean(true), inner})
der := _marshal(t, outer)
defer delete(der)
// 30 08 01 01 FF 30 03 02 01 2A
want := []byte{0x30, 0x08, 0x01, 0x01, 0xFF, 0x30, 0x03, 0x02, 0x01, 0x2A}
testing.expectf(t, bytes.equal(der, want), "got %x, want %x", der, want)
// Walk it back through the reader to confirm it parses as DER.
cur: asn1.Cursor
asn1.cursor_init(&cur, der)
o, oerr := asn1.read_sequence(&cur)
testing.expect_value(t, oerr, asn1.Error.None)
b, berr := asn1.read_boolean(&o)
testing.expect_value(t, berr, asn1.Error.None)
testing.expect_value(t, b, true)
i, ierr := asn1.read_sequence(&o)
testing.expect_value(t, ierr, asn1.Error.None)
mag, merr := asn1.read_unsigned_integer_bytes(&i)
testing.expect_value(t, merr, asn1.Error.None)
testing.expect(t, bytes.equal(mag, {0x2A}), "inner integer round-trips")
testing.expect_value(t, asn1.done(&o), asn1.Error.None)
testing.expect_value(t, asn1.done(&cur), asn1.Error.None)
}

View File

@@ -139,6 +139,13 @@ IP_Address_Parsing_Test_Vectors :: []IP_Address_Parsing_Test_Vector{
{ .IP4, "[10.0.128.31] :80", "", ""},
{ .IP4, "[255.255.255.255]:65536", "", ""},
// "]:" with no opening '[' is not a bracketed host:port; it parses
// as a plain host (no port) and so is not a valid address here.
{ .IP4, "]:80", "", ""},
{ .IP4, "]:", "", ""},
{ .IP6, "]:1", "", ""},
{ .IP4, "foo]:80", "", ""},
// numbers-and-dots notation, but not dotted-decimal
{ .IP4_Alt, "1.2.03.4", "01020304", ""},

View File

@@ -13,6 +13,8 @@ download_assets :: proc "contextless" () {
@(require) import "c/libc"
@(require) import "compress"
@(require) import "container"
@(require) import "crypto/x509"
@(require) import "encoding/asn1"
@(require) import "encoding/base32"
@(require) import "encoding/base64"
@(require) import "encoding/cbor"