rsa, x509 write path

This commit is contained in:
kalsprite
2026-07-12 12:14:32 -07:00
parent 2fd11467c8
commit e4fc7a3a9e
12 changed files with 959 additions and 100 deletions

View File

@@ -31,10 +31,17 @@ 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.
- Signature verification covers RSA PKCS#1 v1.5 and RSA-PSS
(SHA-1/256/384/512), ECDSA P-256/P-384, and Ed25519. These paths return
.Unsupported_Algorithm: ECDSA P-521 (effectively dead in web PKI), and
RSA-PSS naming a digest or MGF this package does not recognize.
- Name constraints are NOT decoded; verify_chain fails CLOSED on them.
Any CA (intermediate or trust anchor) asserting a nameConstraints
extension, critical or not, is refused as an issuer, so a chain through
a name-constrained CA is rejected rather than accepted unchecked. RFC
5280 section 6.1.4(g) requires a validator that processes name
constraints to enforce them regardless of criticality; until that is
implemented, refusing is the only safe option.
- 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, ).
@@ -50,30 +57,6 @@ LIMITATIONS:
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).

160
core/crypto/x509/ext.odin Normal file
View File

@@ -0,0 +1,160 @@
package x509
import "core:encoding/asn1"
// Extension encoders. Each returns a complete DER Extension
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING }
// with extnValue carrying the encoded value (the inverse of the matching
// _parse_known_extension branch). The returned slice is allocated;
// a certificate builder splices these into the extensions [3] list via
// asn1.raw. critical=false is omitted per DEFAULT FALSE.
// marshal_ext_basic_constraints encodes id-ce-basicConstraints:
// BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER OPTIONAL }
// cA is emitted only when `is_ca`; pathLenConstraint only when is_ca and
// `max_path_len` >= 0 (a negative value means "absent").
@(require_results)
marshal_ext_basic_constraints :: proc(is_ca: bool, max_path_len: int, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) {
vals: [2]asn1.Value
path_buf: [8]byte
n := 0
if is_ca {
vals[n] = asn1.boolean(true)
n += 1
if max_path_len >= 0 {
vals[n] = asn1.integer_unsigned(_uint_be(max_path_len, path_buf[:]))
n += 1
}
}
return _marshal_extension(_OID_EXT_BASIC_CONSTRAINTS, critical, asn1.sequence(vals[:n]), allocator)
}
// marshal_ext_key_usage encodes id-ce-keyUsage as a KeyUsage BIT STRING,
// minimally (trailing zero bits dropped, per DER named bit strings).
@(require_results)
marshal_ext_key_usage :: proc(usage: Key_Usage, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) {
hi := -1
for bit in Key_Usage_Bit {
if bit in usage {
hi = int(bit) // bits enumerate ascending, so the last present is the highest
}
}
buf: [3]byte // unused-bits octet + up to 2 payload octets (9 named bits)
content: []byte
if hi < 0 {
content = buf[:1] // no bits set: empty bit string, 0 unused
} else {
nbytes := hi / 8 + 1
buf[0] = byte(nbytes * 8 - (hi + 1)) // unused-bits count
for bit in Key_Usage_Bit {
i := int(bit)
if i <= hi && bit in usage {
buf[1 + i / 8] |= 0x80 >> uint(i % 8)
}
}
content = buf[:1 + nbytes]
}
return _marshal_extension(_OID_EXT_KEY_USAGE, critical, asn1.primitive(asn1.universal(.Bit_String), content), allocator)
}
// marshal_ext_ext_key_usage encodes id-ce-extKeyUsage:
// ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
// in EKU_Bit order.
@(require_results)
marshal_ext_ext_key_usage :: proc(eku: Ext_Key_Usage, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) {
purposes: [7]asn1.Value
n := 0
for bit in EKU_Bit {
if bit in eku {
purposes[n] = asn1.object_identifier(_eku_oid(bit))
n += 1
}
}
return _marshal_extension(_OID_EXT_EXT_KEY_USAGE, critical, asn1.sequence(purposes[:n]), allocator)
}
// marshal_ext_san encodes id-ce-subjectAltName:
// GeneralNames ::= SEQUENCE OF GeneralName
// emitting dNSName [2] IA5String entries (in order) followed by iPAddress [7]
// OCTET STRING entries. IP values are the raw 4- or 16-octet address.
@(require_results)
marshal_ext_san :: proc(dns_names: []string, ip_addresses: [][]byte, critical: bool, allocator := context.allocator) -> (der: []byte, err: Error) {
n := len(dns_names) + len(ip_addresses)
names, merr := make([]asn1.Value, n, allocator) // dynamic count: scaffolding, freed below
if merr != nil {
return nil, .Allocation_Failed
}
defer delete(names, allocator)
i := 0
for d in dns_names {
names[i] = asn1.context_primitive(2, transmute([]byte)d) // [2] IMPLICIT IA5String
i += 1
}
for ip in ip_addresses {
names[i] = asn1.context_primitive(7, ip) // [7] IMPLICIT OCTET STRING
i += 1
}
return _marshal_extension(_OID_EXT_SAN, critical, asn1.sequence(names[:]), allocator)
}
// _marshal_extension wraps an extension value tree as a complete Extension.
// `value`'s borrowed backing lives in the caller's frame, which is active for
// the duration of this synchronous call, so the encode below sees it intact.
@(private, require_results)
_marshal_extension :: proc(oid: []byte, critical: bool, value: asn1.Value, allocator := context.allocator) -> (der: []byte, err: Error) {
out: []byte
merr: asn1.Error
if critical {
out, merr = asn1.marshal(
asn1.sequence({asn1.object_identifier(oid), asn1.boolean(true), asn1.octet_string_wrap({value})}),
allocator,
)
} else {
out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid), asn1.octet_string_wrap({value})}), allocator)
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// _uint_be writes the minimal big-endian magnitude of a non-negative int into
// buf and returns the slice (empty for zero, which integer_unsigned encodes as 0).
@(private)
_uint_be :: proc(v: int, buf: []byte) -> []byte {
if v <= 0 {
return buf[:0]
}
n := 0
x := v
for x > 0 {
n += 1
x >>= 8
}
for i in 0 ..< n {
buf[n - 1 - i] = byte(v >> uint(8 * i))
}
return buf[:n]
}
@(private)
_eku_oid :: proc(bit: EKU_Bit) -> []byte {
switch bit {
case .Server_Auth:
return _OID_EKU_SERVER_AUTH
case .Client_Auth:
return _OID_EKU_CLIENT_AUTH
case .Code_Signing:
return _OID_EKU_CODE_SIGNING
case .Email_Protection:
return _OID_EKU_EMAIL_PROTECTION
case .Time_Stamping:
return _OID_EKU_TIME_STAMPING
case .OCSP_Signing:
return _OID_EKU_OCSP_SIGNING
case .Any:
return _OID_EKU_ANY
}
return nil
}

View File

@@ -0,0 +1,431 @@
package x509
import "core:encoding/asn1"
import "core:time"
// Distinguished-name attribute types the DN builder knows by name.
DN_Attribute_Type :: enum {
Common_Name, // CN, 2.5.4.3
Country, // C, 2.5.4.6
Locality, // L, 2.5.4.7
State_Or_Province, // ST, 2.5.4.8
Organization, // O, 2.5.4.10
Organizational_Unit, // OU, 2.5.4.11
Serial_Number, // 2.5.4.5
}
// DN_Attribute is one relative distinguished name: a type and its value.
// Values are emitted as UTF8String, except Country and Serial_Number which
// are PrintableString (X.520), the policy RFC 5280 section 4.1.2.4 advises.
DN_Attribute :: struct {
type: DN_Attribute_Type,
value: string,
}
@(rodata, private)
_OID_AT_CN := []byte{0x55, 0x04, 0x03}
@(rodata, private)
_OID_AT_C := []byte{0x55, 0x04, 0x06}
@(rodata, private)
_OID_AT_L := []byte{0x55, 0x04, 0x07}
@(rodata, private)
_OID_AT_ST := []byte{0x55, 0x04, 0x08}
@(rodata, private)
_OID_AT_O := []byte{0x55, 0x04, 0x0A}
@(rodata, private)
_OID_AT_OU := []byte{0x55, 0x04, 0x0B}
@(rodata, private)
_OID_AT_SERIAL := []byte{0x55, 0x04, 0x05}
// marshal_dn encodes `attrs` as a DER Name (RDNSequence): one single-valued
// RelativeDistinguishedName per attribute, in the given order. The returned
// slice is the caller's to free; the attribute value bytes are copied into
// it, so `attrs` need not outlive the call.
@(require_results)
marshal_dn :: proc(attrs: []DN_Attribute, allocator := context.allocator) -> (der: []byte, err: Error) {
n := len(attrs)
// Value-tree scaffolding, freed once the bytes are produced. Pre-sized so
// the sub-slices handed to set()/sequence() never move before the encode.
rdns, e1 := make([]asn1.Value, n, allocator) // one SET per attribute
if e1 != nil {
return nil, .Allocation_Failed
}
defer delete(rdns, allocator)
atvs, e2 := make([]asn1.Value, n, allocator) // the AttributeTypeAndValue SEQUENCE
if e2 != nil {
return nil, .Allocation_Failed
}
defer delete(atvs, allocator)
pairs, e3 := make([]asn1.Value, 2 * n, allocator) // {type OID, value} per attribute
if e3 != nil {
return nil, .Allocation_Failed
}
defer delete(pairs, allocator)
for a, i in attrs {
pairs[2 * i] = asn1.object_identifier(_dn_oid(a.type))
pairs[2 * i + 1] = asn1.primitive(_dn_string_tag(a.type), transmute([]byte)a.value)
atvs[i] = asn1.sequence(pairs[2 * i:2 * i + 2])
rdns[i] = asn1.set(atvs[i:i + 1])
}
out, merr := asn1.marshal(asn1.sequence(rdns), allocator)
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
@(private)
_dn_oid :: proc(type: DN_Attribute_Type) -> []byte {
switch type {
case .Common_Name:
return _OID_AT_CN
case .Country:
return _OID_AT_C
case .Locality:
return _OID_AT_L
case .State_Or_Province:
return _OID_AT_ST
case .Organization:
return _OID_AT_O
case .Organizational_Unit:
return _OID_AT_OU
case .Serial_Number:
return _OID_AT_SERIAL
}
return nil
}
@(private)
_dn_string_tag :: proc(type: DN_Attribute_Type) -> asn1.Tag {
#partial switch type {
case .Country, .Serial_Number:
return asn1.universal(.Printable_String)
}
return asn1.universal(.UTF8_String)
}
@(rodata, private)
_DER_INT_ZERO := []byte{0x00}
// marshal_csr_info encodes the CertificationRequestInfo, the to-be-signed
// portion of a PKCS#10 CSR (RFC 2986): version v1, the subject DN, the subject
// public key, and the attributes set. Sign the returned bytes and pass them
// with the signature to marshal_csr. The slice is the caller's to free;
// `subject`, `key`, and `extensions` need not outlive the call.
//
// `extensions` is a list of pre-encoded Extension DER (from the marshal_ext_*
// helpers); when non-empty it is requested via a single PKCS#9
// extensionRequest attribute (the standard way a CSR asks the CA to place
// extensions, SANs, key usage in the issued certificate). Pass nil for the
// empty attributes set.
@(require_results)
marshal_csr_info :: proc(subject: []DN_Attribute, key: Public_Key, extensions: [][]byte = nil, allocator := context.allocator) -> (cri_der: []byte, err: Error) {
dn := marshal_dn(subject, allocator) or_return
defer delete(dn, allocator)
spki := marshal_spki(key, allocator) or_return
defer delete(spki, allocator)
attrs := _marshal_csr_attributes(extensions, allocator) or_return
defer delete(attrs, allocator)
// CertificationRequestInfo ::= SEQUENCE { version, subject, subjectPKInfo, [0] attributes }
out, merr := asn1.marshal(
asn1.sequence(
{
asn1.integer_raw(_DER_INT_ZERO), // version v1 (0)
asn1.raw(dn), // subject Name
asn1.raw(spki), // subjectPublicKeyInfo
asn1.raw(attrs), // attributes [0]
},
),
allocator,
)
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// _marshal_csr_attributes encodes the CertificationRequestInfo attributes
// field, [0] IMPLICIT SET OF Attribute. With no extensions it is the empty
// set (A0 00); otherwise it carries a single PKCS#9 extensionRequest attribute
// Attribute ::= SEQUENCE { type extensionRequest, values SET { Extensions } }
// whose value is the Extensions SEQUENCE OF Extension built from `extensions`.
@(private, require_results)
_marshal_csr_attributes :: proc(extensions: [][]byte, allocator := context.allocator) -> (der: []byte, err: Error) {
out: []byte
merr: asn1.Error
if len(extensions) == 0 {
out, merr = asn1.marshal(asn1.context_explicit(0, {}), allocator)
} else {
// Pre-sized scaffolding for the Extensions SEQUENCE OF, kept alive
// (never resized) through the marshal below.
ext_raws, e := make([]asn1.Value, len(extensions), allocator)
if e != nil {
return nil, .Allocation_Failed
}
defer delete(ext_raws, allocator)
for ext, i in extensions {
ext_raws[i] = asn1.raw(ext)
}
out, merr = asn1.marshal(
asn1.context_explicit(
0,
{asn1.sequence({asn1.object_identifier(_OID_EXT_REQUEST), asn1.set({asn1.sequence(ext_raws[:])})})},
),
allocator,
)
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// marshal_csr wraps a (separately signed) CertificationRequestInfo into a
// complete PKCS#10 CertificationRequest. `signature` is the raw signature
// value over `cri_der`, a DER ECDSA-Sig-Value for ECDSA, the 64-byte value
// for Ed25519, and `signature_algorithm` selects the matching
// AlgorithmIdentifier. The slice is the caller's to free.
@(require_results)
marshal_csr :: proc(cri_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (csr_der: []byte, err: Error) {
// CertificationRequest ::= SEQUENCE { CRI, signatureAlgorithm, signature BIT STRING }
return _marshal_signed(cri_der, signature_algorithm, signature, allocator)
}
// _marshal_signed wraps an already-encoded body (a CertificationRequestInfo
// or a TBSCertificate) with its signature algorithm and signature BIT STRING:
// SEQUENCE { body, AlgorithmIdentifier, BIT STRING signature }
// the common shape of PKCS#10 CSRs and X.509 certificates.
@(private, require_results)
_marshal_signed :: proc(body_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (der: []byte, err: Error) {
oid, null_params, ok := _sig_alg_identifier(signature_algorithm)
if !ok {
return nil, .Unsupported_Algorithm
}
out: []byte
merr: asn1.Error
if null_params {
out, merr = asn1.marshal(
asn1.sequence(
{
asn1.raw(body_der),
asn1.sequence({asn1.object_identifier(oid), asn1.null()}),
asn1.bit_string_octets(signature),
},
),
allocator,
)
} else {
out, merr = asn1.marshal(
asn1.sequence(
{asn1.raw(body_der), asn1.sequence({asn1.object_identifier(oid)}), asn1.bit_string_octets(signature)},
),
allocator,
)
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// _marshal_alg_id encodes a standalone AlgorithmIdentifier (NULL params for
// RSA PKCS#1, absent for ECDSA/EdDSA) for embedding in a TBSCertificate.
@(private, require_results)
_marshal_alg_id :: proc(signature_algorithm: Signature_Algorithm, allocator := context.allocator) -> (der: []byte, err: Error) {
oid, null_params, ok := _sig_alg_identifier(signature_algorithm)
if !ok {
return nil, .Unsupported_Algorithm
}
out: []byte
merr: asn1.Error
if null_params {
out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid), asn1.null()}), allocator)
} else {
out, merr = asn1.marshal(asn1.sequence({asn1.object_identifier(oid)}), allocator)
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// Maps a signature algorithm to its AlgorithmIdentifier OID and whether the
// parameters field is an explicit NULL (RSA PKCS#1) or absent (ECDSA, EdDSA).
@(private)
_sig_alg_identifier :: proc(alg: Signature_Algorithm) -> (oid: []byte, null_params: bool, ok: bool) {
#partial switch alg {
case .RSA_SHA256:
return _OID_SIG_RSA_SHA256, true, true
case .RSA_SHA384:
return _OID_SIG_RSA_SHA384, true, true
case .RSA_SHA512:
return _OID_SIG_RSA_SHA512, true, true
case .ECDSA_SHA256:
return _OID_SIG_ECDSA_SHA256, false, true
case .ECDSA_SHA384:
return _OID_SIG_ECDSA_SHA384, false, true
case .ECDSA_SHA512:
return _OID_SIG_ECDSA_SHA512, false, true
case .Ed25519:
return _OID_ED25519, false, true
}
return nil, false, false
}
@(rodata, private)
_DER_INT_V3 := []byte{0x02} // Version v3 (value 2)
// _marshal_extensions_field encodes the TBSCertificate extensions field:
// [3] EXPLICIT Extensions, Extensions ::= SEQUENCE OF Extension
// from a list of pre-encoded Extension DER, or nil when there are none (the
// field is OPTIONAL, so an empty list omits it entirely).
@(private, require_results)
_marshal_extensions_field :: proc(extensions: [][]byte, allocator := context.allocator) -> (der: []byte, err: Error) {
if len(extensions) == 0 {
return nil, .None
}
raws, merr := make([]asn1.Value, len(extensions), allocator)
if merr != nil {
return nil, .Allocation_Failed
}
defer delete(raws, allocator)
for e, i in extensions {
raws[i] = asn1.raw(e)
}
out, ferr := asn1.marshal(asn1.context_explicit(3, {asn1.sequence(raws[:])}), allocator)
if ferr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// TBS_Certificate gathers the fields of a TBSCertificate to encode. `issuer`
// and `subject` are RDNSequences; `serial` is the serialNumber's unsigned
// magnitude; `extensions` is a list of pre-encoded Extension DER (from the
// marshal_ext_* helpers), embedded in order.
TBS_Certificate :: struct {
serial: []byte,
signature_algorithm: Signature_Algorithm,
issuer: []DN_Attribute,
not_before: time.Time,
not_after: time.Time,
subject: []DN_Attribute,
public_key: Public_Key,
extensions: [][]byte,
}
// marshal_tbs_certificate encodes a v3 TBSCertificate (RFC 5280 section 4.1),
// the to-be-signed portion of a certificate. Sign the returned bytes and pass
// them with the signature to marshal_certificate. The slice is the caller's to
// free; the inputs need not outlive the call.
@(require_results)
marshal_tbs_certificate :: proc(tbs: TBS_Certificate, allocator := context.allocator) -> (der: []byte, err: Error) {
issuer_dn := marshal_dn(tbs.issuer, allocator) or_return
defer delete(issuer_dn, allocator)
subject_dn := marshal_dn(tbs.subject, allocator) or_return
defer delete(subject_dn, allocator)
spki := marshal_spki(tbs.public_key, allocator) or_return
defer delete(spki, allocator)
sig_alg := _marshal_alg_id(tbs.signature_algorithm, allocator) or_return
defer delete(sig_alg, allocator)
ext_field := _marshal_extensions_field(tbs.extensions, allocator) or_return
defer delete(ext_field, allocator)
// raw() splices the independently-marshalled pieces in place; raw(nil) for
// an absent extensions field contributes nothing.
out, merr := asn1.marshal(
asn1.sequence(
{
asn1.context_explicit(0, {asn1.integer_raw(_DER_INT_V3)}), // version [0] EXPLICIT v3
asn1.integer_unsigned(tbs.serial), // serialNumber
asn1.raw(sig_alg), // signature AlgorithmIdentifier
asn1.raw(issuer_dn), // issuer
asn1.sequence({asn1.time(tbs.not_before), asn1.time(tbs.not_after)}), // validity
asn1.raw(subject_dn), // subject
asn1.raw(spki), // subjectPublicKeyInfo
asn1.raw(ext_field), // extensions [3] (absent when empty)
},
),
allocator,
)
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}
// marshal_certificate wraps a (separately signed) TBSCertificate into a
// complete X.509 Certificate. `signature_algorithm` must match the one inside
// the TBS (RFC 5280 section 4.1.1.2). The slice is the caller's to free.
@(require_results)
marshal_certificate :: proc(tbs_der: []byte, signature_algorithm: Signature_Algorithm, signature: []byte, allocator := context.allocator) -> (der: []byte, err: Error) {
return _marshal_signed(tbs_der, signature_algorithm, signature, allocator)
}
// Public_Key holds the subject public-key material to encode into a
// SubjectPublicKeyInfo, mirroring the fields parse() extracts onto a
// Certificate: rsa_n/rsa_e (unsigned magnitudes) for RSA; ec_point for
// ECDSA (the uncompressed point 0x04||X||Y) and Ed25519 (the 32-byte key).
Public_Key :: struct {
algorithm: Public_Key_Algorithm,
rsa_n: []byte,
rsa_e: []byte,
ec_point: []byte,
}
// marshal_spki encodes `key` as a DER SubjectPublicKeyInfo, the inverse of
// the SPKI decoding in parse(); the returned slice is the caller's to free.
// Unknown / unsupported key algorithms yield .Unsupported_Algorithm.
@(require_results)
marshal_spki :: proc(key: Public_Key, allocator := context.allocator) -> (der: []byte, err: Error) {
out: []byte
merr: asn1.Error
switch key.algorithm {
case .RSA:
// SEQUENCE { SEQUENCE { OID rsaEncryption, NULL }, BIT STRING { RSAPublicKey } }
out, merr = asn1.marshal(
asn1.sequence(
{
asn1.sequence({asn1.object_identifier(_OID_KEY_RSA), asn1.null()}),
asn1.bit_string_wrap({asn1.sequence({asn1.integer_unsigned(key.rsa_n), asn1.integer_unsigned(key.rsa_e)})}),
},
),
allocator,
)
case .ECDSA_P256, .ECDSA_P384, .ECDSA_P521:
curve_oid: []byte
#partial switch key.algorithm {
case .ECDSA_P256:
curve_oid = _OID_CURVE_P256
case .ECDSA_P384:
curve_oid = _OID_CURVE_P384
case .ECDSA_P521:
curve_oid = _OID_CURVE_P521
}
// SEQUENCE { SEQUENCE { OID ecPublicKey, OID namedCurve }, BIT STRING point }
out, merr = asn1.marshal(
asn1.sequence(
{
asn1.sequence({asn1.object_identifier(_OID_KEY_EC), asn1.object_identifier(curve_oid)}),
asn1.bit_string_octets(key.ec_point),
},
),
allocator,
)
case .Ed25519:
// SEQUENCE { SEQUENCE { OID Ed25519 }, BIT STRING key } (no params, RFC 8410)
out, merr = asn1.marshal(
asn1.sequence({asn1.sequence({asn1.object_identifier(_OID_ED25519)}), asn1.bit_string_octets(key.ec_point)}),
allocator,
)
case .Unknown:
return nil, .Unsupported_Algorithm
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}

View File

@@ -1,6 +1,7 @@
package x509
import "core:bytes"
import "core:crypto/hash"
import "core:encoding/asn1"
/*
@@ -121,12 +122,17 @@ parse :: proc(der: []byte, allocator := context.allocator) -> (cert: Certificate
cert.raw_tbs = outer.data[tbs_start:outer.pos]
// signatureAlgorithm + signatureValue.
sig_oid, _, serr := _read_algorithm_identifier(&outer)
sig_oid, sig_params, serr := _read_algorithm_identifier(&outer)
if serr != .None {
return {}, .Malformed
}
cert.signature_oid = sig_oid
cert.signature_algorithm = _signature_algorithm(sig_oid)
if cert.signature_algorithm == .RSA_PSS {
if perr := _parse_pss_params(&cert, sig_params); perr != .None {
return {}, perr
}
}
sig_bits, sberr := asn1.read_bit_string_octets(&outer)
if sberr != .None || asn1.done(&outer) != .None {
@@ -161,14 +167,18 @@ parse :: proc(der: []byte, allocator := context.allocator) -> (cert: Certificate
}
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)
// signature must match the outer signatureAlgorithm per RFC 5280 section 4.1.1.2,
// the OID always, and for RSA-PSS the parameters too (they carry the digest).
tbs_sig_oid, tbs_sig_params, tserr := _read_algorithm_identifier(&tbs)
if tserr != .None {
return {}, .Malformed
}
if !bytes.equal(tbs_sig_oid, sig_oid) {
return {}, .Malformed
}
if cert.signature_algorithm == .RSA_PSS && !bytes.equal(tbs_sig_params, sig_params) {
return {}, .Malformed
}
// issuer
issuer_start := tbs.pos
@@ -301,6 +311,108 @@ _signature_algorithm :: proc(oid: []byte) -> Signature_Algorithm {
return .Unknown
}
// _hash_from_oid maps a bare hash-algorithm OID (as it appears in an
// RSASSA-PSS AlgorithmIdentifier) to a hash.Algorithm, reporting ok=false for
// digests this package does not verify (leaving the field .Invalid so the
// verifier fails closed rather than the parser rejecting the certificate).
@(private)
_hash_from_oid :: proc(oid: []byte) -> (hash.Algorithm, bool) {
switch {
case bytes.equal(oid, _OID_HASH_SHA256):
return .SHA256, true
case bytes.equal(oid, _OID_HASH_SHA384):
return .SHA384, true
case bytes.equal(oid, _OID_HASH_SHA512):
return .SHA512, true
case bytes.equal(oid, _OID_HASH_SHA1):
return .Insecure_SHA1, true
}
return .Invalid, false
}
// _parse_pss_params decodes RSASSA-PSS-params (RFC 4055 section 3.1) from the
// signatureAlgorithm parameters into cert.pss_*:
// SEQUENCE { hashAlgorithm [0], maskGenAlgorithm [1], saltLength [2] INTEGER,
// trailerField [3] INTEGER }, all EXPLICIT and all with defaults.
// Omitted fields take their RFC 4055 defaults (SHA-1 / MGF1-SHA-1 / salt 20).
// A structurally broken params element is .Malformed; an unrecognized digest is
// NOT an error here, the hash is left .Invalid for verify_signature to reject.
@(private)
_parse_pss_params :: proc(cert: ^Certificate, params: []byte) -> Error {
// RFC 4055 defaults (applied when a field is absent).
cert.pss_hash = .Insecure_SHA1
cert.pss_mgf_hash = .Insecure_SHA1
cert.pss_salt_len = 20
if len(params) == 0 {
return .None // absent parameters: all defaults
}
cur: asn1.Cursor
asn1.cursor_init(&cur, params)
seq, e := asn1.read_sequence(&cur)
if e != .None || asn1.done(&cur) != .None {
return .Malformed
}
// hashAlgorithm [0] EXPLICIT AlgorithmIdentifier
if inner, present, ie := asn1.read_explicit(&seq, 0); ie != .None {
return .Malformed
} else if present {
oid, _, oe := _read_algorithm_identifier(&inner)
if oe != .None || asn1.done(&inner) != .None {
return .Malformed
}
cert.pss_hash, _ = _hash_from_oid(oid) // .Invalid when unrecognized
}
// maskGenAlgorithm [1] EXPLICIT AlgorithmIdentifier { id-mgf1, hashAlgorithm }
if inner, present, ie := asn1.read_explicit(&seq, 1); ie != .None {
return .Malformed
} else if present {
mgf_oid, mgf_params, me := _read_algorithm_identifier(&inner)
if me != .None || asn1.done(&inner) != .None {
return .Malformed
}
if !bytes.equal(mgf_oid, _OID_MGF1) {
cert.pss_mgf_hash = .Invalid // an MGF other than MGF1: unverifiable here
} else {
mp: asn1.Cursor
asn1.cursor_init(&mp, mgf_params)
hoid, _, he := _read_algorithm_identifier(&mp)
if he != .None || asn1.done(&mp) != .None {
return .Malformed
}
cert.pss_mgf_hash, _ = _hash_from_oid(hoid)
}
}
// saltLength [2] EXPLICIT INTEGER
if inner, present, ie := asn1.read_explicit(&seq, 2); ie != .None {
return .Malformed
} else if present {
sl, se := asn1.read_i64(&inner)
if se != .None || asn1.done(&inner) != .None || sl < 0 {
return .Malformed
}
cert.pss_salt_len = int(sl)
}
// trailerField [3] EXPLICIT INTEGER, only trailerFieldBC (1) is defined.
if inner, present, ie := asn1.read_explicit(&seq, 3); ie != .None {
return .Malformed
} else if present {
tf, te := asn1.read_i64(&inner)
if te != .None || asn1.done(&inner) != .None || tf != 1 {
return .Malformed
}
}
if asn1.done(&seq) != .None {
return .Malformed
}
return .None
}
@(private)
_parse_spki :: proc(cert: ^Certificate, spki: ^asn1.Cursor) -> Error {
key_oid, key_params, aerr := _read_algorithm_identifier(spki)

View File

@@ -0,0 +1,85 @@
package x509
import "core:encoding/asn1"
// Private_Key holds the RAW key material to serialize as a PKCS#8
// PrivateKeyInfo (RFC 5208 / 5958). It is the bytes the crypto packages expose
// (ecdsa.private_key_bytes / public_key_bytes, ed25519.private_key_bytes),
// which marshal_pkcs8 only assembles into DER. For ECDSA, `private` is the
// secret scalar and `public` (optional) the uncompressed point; for Ed25519,
// `private` is the 32-byte seed and `public` is ignored.
Private_Key :: struct {
algorithm: Public_Key_Algorithm,
private: []byte,
public: []byte,
}
@(rodata, private)
_DER_INT_ONE := []byte{0x01}
// Serializes `key` as a DER PKCS#8 PrivateKeyInfo. The crypto has already
// happened; this only nests the raw bytes. RSA and unknown algorithms
// yield .Unsupported_Algorithm (RSA is out of scope here). The returned
// slice is the caller's to free.
@(require_results)
marshal_pkcs8 :: proc(key: Private_Key, allocator := context.allocator) -> (der: []byte, err: Error) {
out: []byte
merr: asn1.Error
switch key.algorithm {
case .ECDSA_P256, .ECDSA_P384, .ECDSA_P521:
curve_oid: []byte
#partial switch key.algorithm {
case .ECDSA_P256:
curve_oid = _OID_CURVE_P256
case .ECDSA_P384:
curve_oid = _OID_CURVE_P384
case .ECDSA_P521:
curve_oid = _OID_CURVE_P521
}
// ECPrivateKey ::= SEQUENCE { version(1), privateKey OCTET STRING, [1] publicKey BIT STRING OPTIONAL }
// (parameters [0] is omitted: the curve is in privateKeyAlgorithm.)
ec_fields: [3]asn1.Value
pub_wrap: [1]asn1.Value // stable backing for the [1] EXPLICIT child
n := 0
ec_fields[n] = asn1.integer_raw(_DER_INT_ONE)
n += 1
ec_fields[n] = asn1.octet_string(key.private)
n += 1
if len(key.public) > 0 {
pub_wrap[0] = asn1.bit_string_octets(key.public)
ec_fields[n] = asn1.context_explicit(1, pub_wrap[:])
n += 1
}
// PrivateKeyInfo ::= SEQUENCE { version(0), AlgId{ecPublicKey, curve},
// privateKey OCTET STRING { ECPrivateKey } }
out, merr = asn1.marshal(
asn1.sequence(
{
asn1.integer_raw(_DER_INT_ZERO),
asn1.sequence({asn1.object_identifier(_OID_KEY_EC), asn1.object_identifier(curve_oid)}),
asn1.octet_string_wrap({asn1.sequence(ec_fields[:n])}),
},
),
allocator,
)
case .Ed25519:
// PrivateKeyInfo ::= SEQUENCE { version(0), AlgId{Ed25519},
// privateKey OCTET STRING { CurvePrivateKey ::= OCTET STRING seed } }
out, merr = asn1.marshal(
asn1.sequence(
{
asn1.integer_raw(_DER_INT_ZERO),
asn1.sequence({asn1.object_identifier(_OID_ED25519)}),
asn1.octet_string_wrap({asn1.octet_string(key.private)}),
},
),
allocator,
)
case .RSA, .Unknown:
return nil, .Unsupported_Algorithm
}
if merr != .None {
return nil, .Allocation_Failed
}
return out, .None
}

View File

@@ -4,6 +4,7 @@ import "core:bytes"
import "core:crypto/ecdsa"
import "core:crypto/ed25519"
import "core:crypto/hash"
import "core:crypto/rsa"
import "core:net"
import "core:strings"
import "core:time"
@@ -107,18 +108,51 @@ _match_hostname :: proc(pattern, host: string) -> bool {
//
// 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
// 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.
// and .Unsupported_Algorithm when the signature algorithm (ECDSA P-521, or
// RSA-PSS with an unrecognized digest) 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 .RSA_SHA1, .RSA_SHA256, .RSA_SHA384, .RSA_SHA512:
if issuer.public_key_algorithm != .RSA {
// An RSA signature paired with a non-RSA issuer key can never verify.
return .Unsupported_Algorithm
}
pub: rsa.Public_Key
defer rsa.public_key_clear(&pub)
if !rsa.public_key_set_bytes(&pub, issuer.rsa_n, issuer.rsa_e) {
return .Signature_Invalid
}
h := _hash_for_rsa(cert.signature_algorithm)
if !rsa.verify_pkcs1(&pub, h, cert.raw_tbs, cert.signature) {
return .Signature_Invalid
}
return .None
case .RSA_PSS:
if issuer.public_key_algorithm != .RSA {
return .Unsupported_Algorithm
}
// The parser decodes the RSASSA-PSS-params into cert.pss_*, leaving a
// digest it cannot verify as .Invalid; refuse rather than guess.
if cert.pss_hash == .Invalid || cert.pss_mgf_hash == .Invalid {
return .Unsupported_Algorithm
}
pub: rsa.Public_Key
defer rsa.public_key_clear(&pub)
if !rsa.public_key_set_bytes(&pub, issuer.rsa_n, issuer.rsa_e) {
return .Signature_Invalid
}
if !rsa.verify_pss(&pub, cert.pss_hash, cert.pss_salt_len, cert.raw_tbs, cert.signature, mgf1_algo = cert.pss_mgf_hash) {
return .Signature_Invalid
}
return .None
case .ECDSA_SHA256, .ECDSA_SHA384, .ECDSA_SHA512:
curve: ecdsa.Curve
@@ -170,6 +204,21 @@ _hash_for_ecdsa :: proc "contextless" (s: Signature_Algorithm) -> hash.Algorithm
return .SHA256
}
// _hash_for_rsa maps an RSA PKCS#1 v1.5 signature algorithm to its message
// digest. SHA-1 (obsolete, legacy verification only) maps to .Insecure_SHA1.
@(private)
_hash_for_rsa :: proc "contextless" (s: Signature_Algorithm) -> hash.Algorithm {
#partial switch s {
case .RSA_SHA1:
return .Insecure_SHA1
case .RSA_SHA384:
return .SHA384
case .RSA_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
@@ -221,11 +270,11 @@ _MAX_SIG_CHECKS :: 100
// 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 treated as trusted input, as in Go and OpenSSL: it
// must be valid at opts.current_time and is name-chained + signature-checked
// as the issuer below it, but its CA authorization (basicConstraints /
// keyCertSign / pathLenConstraint) and its own self-signature are NOT
// re-checked. An expired anchor is still rejected; resilience to that comes
// The trust anchor is treated as trusted input: it must be valid at
// opts.current_time and is name-chained + signature-checked as the issuer
// below it, but its CA authorization (basicConstraints / keyCertSign /
// pathLenConstraint) and its own self-signature are NOT re-checked.
// An expired anchor is still rejected; resilience to that comes
// from the search trying every other available anchor and intermediate.
//
@(require_results)
@@ -244,21 +293,18 @@ verify_chain :: proc(
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
}
// A leaf signed with an algorithm the verifier cannot check (RSA-PSS with
// an unrecognized digest, ECDSA P-521) surfaces via `saw_unsupported` from
// the per-edge verify_signature call, so an unbuildable path still returns
// .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
// 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
@@ -333,8 +379,7 @@ _build_to_anchor :: proc(
// Prefer terminating at a trust anchor. The anchor must have issued `cert`
// (name chaining + signature) and be valid at `now`, but as TRUSTED INPUT
// its CA authorization (basicConstraints / keyCertSign / pathLenConstraint)
// and its own self-signature are NOT re-checked, matching how Go and
// OpenSSL treat roots. See _anchor_usable.
// and its own self-signature are NOT re-checked. See _anchor_usable.
for root in opts.roots {
if !_is_issuer_of(root, cert) || !_anchor_usable(root, opts.current_time) {
continue
@@ -430,9 +475,9 @@ _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
// Name constraints are NOT decoded. RFC 5280 section 6.1.4(g) requires a
// validator that processes them to enforce them regardless of criticality;
// until that is implemented, refuse any issuer that asserts them.
if _has_extension(issuer, _OID_EXT_NAME_CONSTRAINTS) {
return false
}
@@ -451,12 +496,11 @@ _issuer_usable :: proc(issuer: ^Certificate, now: time.Time, below: int) -> bool
return true
}
// The anchor is trusted input, so unlike _issuer_usable its CA authorization
// (basicConstraints / keyCertSign / pathLenConstraint) and self-signature are
// NOT re-checked, matching Go and OpenSSL's treatment of roots. Still required:
// valid at `now` (Go and OpenSSL both enforce this; resilience to an expired
// anchor comes from the search trying other anchors/intermediates), no
// uninterpreted critical extension, and no name constraints (which we cannot
// The anchor is trusted input, so unlike _issuer_usable its CA authorization
// (basicConstraints / keyCertSign / pathLenConstraint) and self-signature are
// NOT re-checked. Still required: valid at `now` (resilience to an expired
// anchor comes from the search trying other anchors/intermediates), no
// uninterpreted critical extension, and no name constraints (which we cannot
// enforce, so refuse rather than ignore).
@(private)
_anchor_usable :: proc(anchor: ^Certificate, now: time.Time) -> bool {

View File

@@ -1,5 +1,6 @@
package x509
import "core:crypto/hash"
import "core:time"
Error :: enum {
@@ -24,8 +25,8 @@ Error :: enum {
}
// 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.
// encounters in practice. RSASSA-PSS parameters are decoded into the
// Certificate's pss_* fields.
Signature_Algorithm :: enum {
Unknown,
RSA_SHA1, // obsolete; parsed for identification only
@@ -103,7 +104,7 @@ Certificate :: struct {
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
// bit is set carries a leading 0x00 sign octet; 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,
@@ -122,6 +123,16 @@ Certificate :: struct {
// ECDSA: the uncompressed point (0x04 || X || Y); Ed25519: the 32-byte key.
ec_point: []byte,
// RSASSA-PSS parameters, decoded from the signatureAlgorithm's
// RSASSA-PSS-params (meaningful only when signature_algorithm ==
// .RSA_PSS; RFC 4055 defaults applied for omitted fields). pss_hash and
// pss_mgf_hash are hash.Algorithm.Invalid when the certificate names a
// digest this package cannot verify, which verify_signature reports as
// .Unsupported_Algorithm rather than a failure.
pss_hash: hash.Algorithm,
pss_mgf_hash: hash.Algorithm,
pss_salt_len: int,
// BasicConstraints (basic_constraints_valid reports presence).
basic_constraints_valid: bool,
is_ca: bool,
@@ -178,6 +189,21 @@ _OID_SIG_ECDSA_SHA512 := []byte{0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04}
@(rodata, private)
_OID_ED25519 := []byte{0x2B, 0x65, 0x70} // id-Ed25519 (1.3.101.112), RFC 8410
@(rodata, private)
_OID_EXT_REQUEST := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x0E} // id-extensionRequest (1.2.840.113549.1.9.14), PKCS#9
// Bare hash OIDs (content octets), for the RSASSA-PSS AlgorithmIdentifiers.
@(rodata, private)
_OID_HASH_SHA1 := []byte{0x2B, 0x0E, 0x03, 0x02, 0x1A} // id-sha1 (1.3.14.3.2.26)
@(rodata, private)
_OID_HASH_SHA256 := []byte{0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01} // id-sha256 (2.16.840.1.101.3.4.2.1)
@(rodata, private)
_OID_HASH_SHA384 := []byte{0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02} // id-sha384 (2.16.840.1.101.3.4.2.2)
@(rodata, private)
_OID_HASH_SHA512 := []byte{0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03} // id-sha512 (2.16.840.1.101.3.4.2.3)
@(rodata, private)
_OID_MGF1 := []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x08} // id-mgf1 (1.2.840.113549.1.1.8), RFC 4055
@(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)

View File

@@ -165,9 +165,8 @@ read_i64 :: proc "contextless" (r: ^Cursor) -> (value: i64, err: Error) {
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.
// Reads a non-negative INTEGER and returns its magnitude octets with any leading 0x00 sign
// octet stripped.
read_unsigned_integer_bytes :: proc "contextless" (r: ^Cursor) -> (magnitude: []byte, err: Error) {
content, ierr := read_integer_bytes(r)
if ierr != .None {
@@ -210,8 +209,7 @@ read_bit_string :: proc "contextless" (r: ^Cursor) -> (bits: []byte, unused: int
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.
// Reads a BIT STRING that must be a whole number of octets (unused == 0).
read_bit_string_octets :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
bits, unused, berr := read_bit_string(r)
if berr != .None {
@@ -402,9 +400,8 @@ oid_to_string :: proc(raw: []byte, allocator := context.allocator) -> (str: stri
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).
// Builder writes swallow allocator failures, so tally the written vs expected lengths
// and treat any shortfall as an allocation failure.
written, expected := 0, 0
acc: u64 = 0
first := true
@@ -569,11 +566,9 @@ _two_digits :: proc "contextless" (b: []byte) -> (value: int, ok: bool) {
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".
// Converts Unix seconds to a time.Time, saturating at time.Time's i64-nanosecond
// bounds (near year 2262) rather than overflowing. See the note above
// read_utc_time for why far-future dates are saturated instead of rejected.
@(private)
_time_from_unix :: proc "contextless" (secs: i64) -> dt.Time {
NS_PER_SEC :: i64(1_000_000_000)

View File

@@ -9,8 +9,7 @@ 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.
children (DER SET OF ordering).
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
@@ -23,7 +22,7 @@ its children with a slice/array that outlives the encode call:
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.
covers all of PKIX; high-tag-number form is not supported.
See:
- [[ https://www.itu.int/rec/T-REC-X.690 ]]
@@ -81,10 +80,8 @@ boolean :: proc "contextless" (v: bool) -> Value {
// 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.
// inserted when the top bit would otherwise read as negative. An empty
// or all-zero magnitude encodes as 0. The inverse of read_unsigned_integer_bytes.
integer_unsigned :: proc "contextless" (magnitude: []byte) -> Value {
return Value{tag = universal(.Integer), form = .Integer_Magnitude, content = magnitude}
}
@@ -321,9 +318,7 @@ _content_len :: proc(v: Value) -> int {
//
// 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.
// single O(n) sizing pass, this does the single O(n) write pass.
@(private)
_emit :: proc(v: Value, dst: []byte) -> int {
if v.form == .Raw {
@@ -372,7 +367,7 @@ _emit :: proc(v: Value, dst: []byte) -> int {
end -= _emit(v.children[i], dst[:end])
}
}
clen := len(dst) - end // content length, read off the cursor never recomputed
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)

View File

@@ -72,7 +72,7 @@ test_fuzz_bitflips :: proc(t: ^testing.T) {
// Random multi-byte mutations across all three fixtures.
@(test)
test_fuzz_mutations :: proc(t: ^testing.T) {
fixtures := [?][]byte{RSA_DER, EC_DER, ED_DER}
fixtures := [?][]byte{RSA_DER, PSS_DER, EC_DER, ED_DER}
max_len := 0
for f in fixtures {
@@ -86,7 +86,7 @@ test_fuzz_mutations :: proc(t: ^testing.T) {
input := buf[:len(fixture)]
copy(input, fixture)
// Mutate 1-16 positions, occasionally truncating instead
// 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)]

View File

@@ -19,6 +19,7 @@ package test_core_x509
// Field expectations below were cross-checked against
// `openssl x509 -noout -text`.
import "core:crypto/hash"
import "core:crypto/x509"
import "core:testing"
import "core:time"
@@ -27,6 +28,12 @@ RSA_DER := #load("testdata/rsa.der")
EC_DER := #load("testdata/ec.der")
ED_DER := #load("testdata/ed.der")
// Self-signed RSA-PSS CA (SHA-256, MGF1-SHA-256, salt 32), generated with:
// openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 3650 \
// -subj "/CN=pss.rsa.test" -sigopt rsa_padding_mode:pss \
// -sigopt rsa_pss_saltlen:32 -addext "basicConstraints=critical,CA:TRUE"
PSS_DER := #load("testdata/pss.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
@@ -335,14 +342,33 @@ test_verify_signature_ed :: proc(t: ^testing.T) {
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.
// RSA PKCS#1 v1.5 verification is wired through to core:crypto/rsa. rsa.der is
// a self-signed RSA-SHA256 CA, so it is its own issuer and its signature must
// verify; pairing an RSA-signed cert with a non-RSA issuer key is unsupported.
@(test)
test_verify_signature_rsa_unsupported :: proc(t: ^testing.T) {
rsa, err := x509.parse(RSA_DER); defer x509.destroy(&rsa)
test_verify_signature_rsa :: proc(t: ^testing.T) {
rsa, e1 := x509.parse(RSA_DER); defer x509.destroy(&rsa)
ec, e2 := x509.parse(EC_DER); defer x509.destroy(&ec)
testing.expect_value(t, e1, x509.Error.None)
testing.expect_value(t, e2, x509.Error.None)
testing.expect_value(t, x509.verify_signature(&rsa, &rsa), x509.Error.None)
testing.expect_value(t, x509.verify_signature(&rsa, &ec), x509.Error.Unsupported_Algorithm)
}
// RSA-PSS end-to-end: the parser decodes RSASSA-PSS-params (hash, MGF, salt)
// off the signatureAlgorithm, and verify_signature feeds them to
// rsa.verify_pss. pss.der is a self-signed RSA-PSS CA, so it is its own issuer.
@(test)
test_verify_signature_rsa_pss :: proc(t: ^testing.T) {
pss, err := x509.parse(PSS_DER); defer x509.destroy(&pss)
testing.expect_value(t, err, x509.Error.None)
testing.expect_value(t, x509.verify_signature(&rsa, &rsa), x509.Error.Unsupported_Algorithm)
testing.expect_value(t, pss.signature_algorithm, x509.Signature_Algorithm.RSA_PSS)
// Decoded parameters (SHA-256, MGF1-SHA-256, salt length 32).
testing.expect_value(t, pss.pss_hash, hash.Algorithm.SHA256)
testing.expect_value(t, pss.pss_mgf_hash, hash.Algorithm.SHA256)
testing.expect_value(t, pss.pss_salt_len, 32)
// The PSS signature verifies against the embedded key.
testing.expect_value(t, x509.verify_signature(&pss, &pss), x509.Error.None)
}
@(test)
@@ -573,14 +599,16 @@ test_verify_chain_cross_signed_cycle :: proc(t: ^testing.T) {
testing.expect_value(t, err, x509.Error.Unknown_Authority)
}
// An RSA-signed leaf surfaces the RSA gap directly.
// End-to-end: the self-signed RSA-SHA256 CA validates as a chain with itself
// as the trust anchor — the RSA PKCS#1 v1.5 signature check now runs.
@(test)
test_verify_chain_rsa_unsupported :: proc(t: ^testing.T) {
test_verify_chain_rsa_selfsigned :: 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)
c, verr := x509.verify_chain(&rsa, opts); defer delete(c)
testing.expect_value(t, verr, x509.Error.None)
testing.expect(t, len(c) >= 1, "RSA self-signed chain built")
}
// Each negative chain must be rejected for the right structural reason

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

Binary file not shown.