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