mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-14 20:00:31 +00:00
asn1 DER reader/writer and X.509 reader
This commit is contained in:
111
core/crypto/x509/doc.odin
Normal file
111
core/crypto/x509/doc.odin
Normal 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
634
core/crypto/x509/parse.odin
Normal 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
|
||||
}
|
||||
485
core/crypto/x509/verify.odin
Normal file
485
core/crypto/x509/verify.odin
Normal 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
221
core/crypto/x509/x509.odin
Normal 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)
|
||||
72
core/encoding/asn1/asn1.odin
Normal file
72
core/encoding/asn1/asn1.odin
Normal 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}
|
||||
}
|
||||
624
core/encoding/asn1/cursor.odin
Normal file
624
core/encoding/asn1/cursor.odin
Normal 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
|
||||
}
|
||||
29
core/encoding/asn1/doc.odin
Normal file
29
core/encoding/asn1/doc.odin
Normal 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
|
||||
498
core/encoding/asn1/writer.odin
Normal file
498
core/encoding/asn1/writer.odin
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
133
tests/core/crypto/x509/fuzz_x509.odin
Normal file
133
tests/core/crypto/x509/fuzz_x509.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
138
tests/core/crypto/x509/oom_x509.odin
Normal file
138
tests/core/crypto/x509/oom_x509.odin
Normal 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))
|
||||
}
|
||||
}
|
||||
661
tests/core/crypto/x509/test_core_x509.odin
Normal file
661
tests/core/crypto/x509/test_core_x509.odin
Normal 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)
|
||||
}
|
||||
BIN
tests/core/crypto/x509/testdata/chain_ec_expired_root.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_expired_root.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ec_expleaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_expleaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ec_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ec_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ec_other_root.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_other_root.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ec_root.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ec_root.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ed_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ed_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/chain_ed_root.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/chain_ed_root.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/cyc_a.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/cyc_a.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/cyc_a_by_b.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/cyc_a_by_b.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/cyc_b.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/cyc_b.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/cyc_b_by_a.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/cyc_b_by_a.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/cyc_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/cyc_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/dup_extension.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/dup_extension.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/ec.der
vendored
Normal file
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
BIN
tests/core/crypto/x509/testdata/ed.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/eku_alt_bad.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/eku_alt_bad.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/eku_alt_good.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/eku_alt_good.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/eku_alt_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/eku_alt_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_eku_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_eku_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_eku_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_eku_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_expinter_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_expinter_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_expinter_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_expinter_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_nc_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_nc_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_nc_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_nc_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_nokcs_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_nokcs_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_nokcs_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_nokcs_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_notca_inter.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_notca_inter.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_notca_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_notca_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_pl_a.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_pl_a.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_pl_b.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_pl_b.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_pl_leaf.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_pl_leaf.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/neg_root.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/neg_root.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/rsa.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/rsa.der
vendored
Normal file
Binary file not shown.
BIN
tests/core/crypto/x509/testdata/serial_zero.der
vendored
Normal file
BIN
tests/core/crypto/x509/testdata/serial_zero.der
vendored
Normal file
Binary file not shown.
222
tests/core/encoding/asn1/fuzz_asn1.odin
Normal file
222
tests/core/encoding/asn1/fuzz_asn1.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
158
tests/core/encoding/asn1/fuzz_writer.odin
Normal file
158
tests/core/encoding/asn1/fuzz_writer.odin
Normal 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)
|
||||
}
|
||||
}
|
||||
119
tests/core/encoding/asn1/oom_asn1.odin
Normal file
119
tests/core/encoding/asn1/oom_asn1.odin
Normal 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))
|
||||
}
|
||||
}
|
||||
532
tests/core/encoding/asn1/test_core_asn1.odin
Normal file
532
tests/core/encoding/asn1/test_core_asn1.odin
Normal 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)
|
||||
}
|
||||
298
tests/core/encoding/asn1/test_writer.odin
Normal file
298
tests/core/encoding/asn1/test_writer.odin
Normal 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)
|
||||
}
|
||||
@@ -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", ""},
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user