Module: Ruby::Rego::Builtins::Crypto::CertificateStruct
- Defined in:
- lib/ruby/rego/builtins/crypto/certificate_uri.rb,
lib/ruby/rego/builtins/crypto/certificate_struct.rb,
lib/ruby/rego/builtins/crypto/certificate_extensions.rb,
lib/ruby/rego/builtins/crypto/certificate_public_key.rb,
lib/ruby/rego/builtins/crypto/certificate_request_struct.rb
Overview
CSR fields of the x509 struct builder (see certificate_struct.rb for the module role). A CertificateRequest is a subset of a Certificate: Subject/PublicKey/Signature/Raw* and the SANs derived from the requested extensions, plus the raw Attributes set. Reuses the certificate atoms (Name.build, public_key, signature_algorithm, the SAN validators via general_names, parsed_uris/url_struct, scrub, b64, oid_ints, context_tag?).
Constant Summary collapse
- SIGNATURE_ALGORITHMS =
Go x509.SignatureAlgorithm enum, keyed by OpenSSL’s signature-algorithm name. RSA-PSS shares one OID across digests (the digest lives in the parameters), so 13/14/15 are resolved from those parameters in pss_signature_algorithm, not this table.
{ "md2WithRSAEncryption" => 1, "md5WithRSAEncryption" => 2, "sha1WithRSAEncryption" => 3, "sha256WithRSAEncryption" => 4, "sha384WithRSAEncryption" => 5, "sha512WithRSAEncryption" => 6, "dsaWithSHA1" => 7, "dsa_with_SHA256" => 8, "ecdsa-with-SHA1" => 9, "ecdsa-with-SHA256" => 10, "ecdsa-with-SHA384" => 11, "ecdsa-with-SHA512" => 12, "ED25519" => 16 }.freeze
- RSA_PSS_NAME =
OpenSSL’s name for the RSASSA-PSS signature algorithm, and the SHA-256,384,512 digest OIDs in its parameters -> Go’s SHA*WithRSAPSS enum values.
"rsassaPss"- PSS_DIGEST_ALGORITHMS =
{ "2.16.840.1.101.3.4.2.1" => 13, "2.16.840.1.101.3.4.2.2" => 14, "2.16.840.1.101.3.4.2.3" => 15 }.freeze
- PUBLIC_KEY_ALGORITHM_OIDS =
Go x509.PublicKeyAlgorithm enum, keyed by the SubjectPublicKeyInfo algorithm OID. Keying on the DER OID (not OpenSSL’s key object) lets an unsupported algorithm (e.g. X25519/Ed448, whose key OpenSSL cannot expose) map to UnknownPublicKeyAlgorithm(0) without raising.
{ "1.2.840.113549.1.1.1" => 1, "1.2.840.10040.4.1" => 2, "1.2.840.10045.2.1" => 3, "1.3.101.112" => 4 }.freeze
- ZERO_FIELDS =
The certificate field hash at Go’s zero values; computed fields override these.
{ "Raw" => nil, "RawTBSCertificate" => nil, "RawSubjectPublicKeyInfo" => nil, "RawSubject" => nil, "RawIssuer" => nil, "Signature" => nil, "SignatureAlgorithm" => 0, "PublicKeyAlgorithm" => 0, "PublicKey" => nil, "Version" => 0, "SerialNumber" => 0, "Issuer" => nil, "Subject" => nil, "NotBefore" => nil, "NotAfter" => nil, "KeyUsage" => 0, "Extensions" => nil, "ExtraExtensions" => nil, "UnhandledCriticalExtensions" => nil, "ExtKeyUsage" => nil, "UnknownExtKeyUsage" => nil, "BasicConstraintsValid" => false, "IsCA" => false, "MaxPathLen" => 0, "MaxPathLenZero" => false, "SubjectKeyId" => nil, "AuthorityKeyId" => nil, "OCSPServer" => nil, "IssuingCertificateURL" => nil, "DNSNames" => nil, "EmailAddresses" => nil, "IPAddresses" => nil, "URIs" => nil, "URIStrings" => nil, "PermittedDNSDomainsCritical" => false, "PermittedDNSDomains" => nil, "ExcludedDNSDomains" => nil, "PermittedIPRanges" => nil, "ExcludedIPRanges" => nil, "PermittedEmailAddresses" => nil, "ExcludedEmailAddresses" => nil, "PermittedURIDomains" => nil, "ExcludedURIDomains" => nil, "CRLDistributionPoints" => nil, "PolicyIdentifiers" => nil, "Policies" => nil, "PolicyMappings" => nil, "InhibitAnyPolicy" => 0, "InhibitAnyPolicyZero" => false, "InhibitPolicyMapping" => 0, "InhibitPolicyMappingZero" => false, "RequireExplicitPolicy" => 0, "RequireExplicitPolicyZero" => false }.freeze
- EXT_KEY_USAGES =
Go x509.ExtKeyUsage enum, keyed by the EKU purpose OID.
{ "2.5.29.37.0" => 0, "1.3.6.1.5.5.7.3.1" => 1, "1.3.6.1.5.5.7.3.2" => 2, "1.3.6.1.5.5.7.3.3" => 3, "1.3.6.1.5.5.7.3.4" => 4, "1.3.6.1.5.5.7.3.5" => 5, "1.3.6.1.5.5.7.3.6" => 6, "1.3.6.1.5.5.7.3.7" => 7, "1.3.6.1.5.5.7.3.8" => 8, "1.3.6.1.5.5.7.3.9" => 9, "1.3.6.1.4.1.311.10.3.3" => 10, "2.16.840.1.113730.4.1" => 11, "1.3.6.1.4.1.311.2.1.22" => 12, "1.3.6.1.4.1.311.61.1.1" => 13 }.freeze
- NAME_CONSTRAINT_FIELDS =
GeneralName context tag -> [permitted field, excluded field] for nameConstraints subtrees.
{ 2 => %w[PermittedDNSDomains ExcludedDNSDomains], 1 => %w[PermittedEmailAddresses ExcludedEmailAddresses], 6 => %w[PermittedURIDomains ExcludedURIDomains], 7 => %w[PermittedIPRanges ExcludedIPRanges] }.freeze
- ACCESS_METHODS =
AccessDescription method OID -> the field its URI accessLocation populates.
{ "1.3.6.1.5.5.7.48.1" => "OCSPServer", "1.3.6.1.5.5.7.48.2" => "IssuingCertificateURL" }.freeze
- RECOGNIZED_EXTENSION_OIDS =
Extension OIDs crypto/x509’s parseCertificate recognizes; a critical extension whose OID is not here is reported in UnhandledCriticalExtensions (matching Go).
%w[ 2.5.29.14 2.5.29.35 2.5.29.15 2.5.29.37 2.5.29.19 2.5.29.17 2.5.29.31 1.3.6.1.5.5.7.1.1 2.5.29.30 2.5.29.32 2.5.29.36 2.5.29.54 2.5.29.33 ].freeze
- MAX_ASN1_DEPTH =
Nesting cap for a known extension’s DER. Far beyond any real certificate (extensions nest a handful deep) yet well under the depth at which OpenSSL::ASN1.decode’s C recursion overflows the stack uncatchably; OPA’s Go asn1 tolerates deeper input, so this diverges only on adversarial certificates that no legitimate issuer produces.
1000- INT64_MIN =
Go’s int is 64-bit on OPA’s platforms; asn1.Unmarshal into an int field errors (-> OPA undefined) when the value does not fit, so the int-typed extension fields are bounded here.
-(2**63)
- INT64_MAX =
(2**63) - 1
- SUPPORTED_EC_CURVES =
The named curves Go’s crypto/x509 supports (P-224/256/384/521); any other curve makes ParseCertificate error -> OPA undefined.
%w[secp224r1 prime256v1 secp384r1 secp521r1].freeze
- EXTENSION_REQUEST_OID =
PKCS#9 extensionRequest attribute OID (its value carries the requested extensions).
"1.2.840.113549.1.9.14"- REQUEST_ZERO_FIELDS =
The CertificateRequest field hash at Go’s zero values; computed fields override these.
{ "Raw" => nil, "RawTBSCertificateRequest" => nil, "RawSubjectPublicKeyInfo" => nil, "RawSubject" => nil, "Version" => 0, "Signature" => nil, "SignatureAlgorithm" => 0, "PublicKeyAlgorithm" => 0, "PublicKey" => nil, "Subject" => nil, "Attributes" => nil, "Extensions" => nil, "ExtraExtensions" => nil, "DNSNames" => nil, "EmailAddresses" => nil, "IPAddresses" => nil, "URIs" => nil }.freeze
Class Method Summary collapse
-
.apply_extensions(fields, tbs) ⇒ Hash[String, untyped]
A known extension whose DER is not the shape crypto/x509 expects makes Go’s asn1.Unmarshal fail, so parseCertificate errors and OPA returns undefined; the structural exceptions that surfaces in Ruby (nil/array/string where another node was assumed) are mapped to that same undefined via MalformedCertificate.
-
.build(cert) ⇒ Hash[String, untyped]
rubocop:disable Metrics/AbcSize, Metrics/MethodLength.
-
.build_request(request) ⇒ Hash[String, untyped]
rubocop:disable Metrics/AbcSize, Metrics/MethodLength.
-
.extension_nodes(tbs) ⇒ Object
The raw Extension nodes (the [3] EXPLICIT wrapper’s SEQUENCE OF Extension), or nil when the certificate carries no extensions.
-
.oid_ints(oid) ⇒ Object
OID dotted string -> array of integer arcs (Go marshals asn1.ObjectIdentifier as []int).
-
.public_key(cert) ⇒ Object
The PublicKey field, matching Go’s json.Marshal of the concrete public-key type: an rsa.PublicKey is E; an ecdsa.PublicKey is Curve:{, X, Y}; an ed25519.PublicKey is the std-base64 of its 32 bytes.
-
.safe_asn1?(der) ⇒ Boolean
Iterative scan of definite-length DER TLV headers (no decode, no recursion, never raises): false once nesting would exceed MAX_ASN1_DEPTH, and false on a BER indefinite length (which would otherwise bypass the depth count, and which DER — so Go — rejects anyway).
-
.transcode(bytes, encoding) ⇒ Object
Transcode raw bytes from an ASN.1 string encoding to UTF-8, replacing invalid bytes with U+FFFD as Go’s json.Marshal does.
-
.uri_fields(uri_texts) ⇒ Hash[String, untyped]
Builds the URIs / URIStrings fields from the URI SANs.
Class Method Details
.apply_extensions(fields, tbs) ⇒ Hash[String, untyped]
A known extension whose DER is not the shape crypto/x509 expects makes Go’s asn1.Unmarshal fail, so parseCertificate errors and OPA returns undefined; the structural exceptions that surfaces in Ruby (nil/array/string where another node was assumed) are mapped to that same undefined via MalformedCertificate. Unknown extension OIDs never reach a handler, matching Go keeping them as raw bytes. (SystemStackError from a pathologically nested extension is a non-StandardError; it is caught by the certificate builder’s outer rescue.)
63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/ruby/rego/builtins/crypto/certificate_extensions.rb', line 63 def self.apply_extensions(fields, tbs) extensions = certificate_extensions(tbs) oids = extensions.map(&:first) # Go's parseCertificate rejects a certificate with a repeated extension OID -> OPA undefined. raise MalformedCertificate, "duplicate extension" if oids.uniq.length != oids.length unhandled = extensions.filter_map do |oid, critical, der| dispatch_extension(fields, oid, critical, der) oid_ints(oid) if critical && !RECOGNIZED_EXTENSION_OIDS.include?(oid) end fields["UnhandledCriticalExtensions"] = unhandled unless unhandled.empty? fields end |
.build(cert) ⇒ Hash[String, untyped]
rubocop:disable Metrics/AbcSize, Metrics/MethodLength
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/ruby/rego/builtins/crypto/certificate_struct.rb', line 74 def self.build(cert) decoded = OpenSSL::ASN1.decode(cert.to_der) tbs = decoded.value[0] elements = tbs.value # TBSCertificate omits the [0] EXPLICIT version field for v1 certs, shifting serialNumber # to index 0 (it sits at index 1 once a version is present) and every later field with it. serial_index = context_tag?(elements[0], 0) ? 1 : 0 issuer = elements[serial_index + 2] subject = elements[serial_index + 4] spki = elements[serial_index + 5] # A 0 (unknown) algorithm has PublicKey null and never calls cert.public_key (it raises for # key types OpenSSL cannot represent), matching Go's UnknownPublicKeyAlgorithm handling. algorithm = PUBLIC_KEY_ALGORITHM_OIDS.fetch(spki.value[0].value[0].oid, 0) fields = ZERO_FIELDS.merge( scalar_fields(cert, decoded), raw_fields(cert, tbs, issuer, subject, spki), "PublicKey" => algorithm.zero? ? nil : CertificateStruct.public_key(cert), "PublicKeyAlgorithm" => algorithm, "Issuer" => Name.build(issuer), "Subject" => Name.build(subject), "Extensions" => extension_list(tbs) ) scrub(apply_extensions(fields, tbs)) end |
.build_request(request) ⇒ Hash[String, untyped]
rubocop:disable Metrics/AbcSize, Metrics/MethodLength
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/ruby/rego/builtins/crypto/certificate_request_struct.rb', line 30 def self.build_request(request) der = request.to_der # DoS guard. X509::Request.new stores attribute values as opaque ASN.1 ANY (its template # parse caps recursion at the d2i depth limit), but OpenSSL::ASN1.decode below recurses into # them eagerly, so a deeply-nested attribute value can overflow the C stack uncatchably. # Pre-scan depth like the extension decode path; a CSR nested beyond MAX_ASN1_DEPTH (which no # conforming CSR reaches) maps to undefined — a deliberate divergence from OPA, which stores # the value un-recursed and tolerates arbitrary nesting, traded for not crashing. raise MalformedCertificate, "request nested beyond depth limit" unless safe_asn1?(der) decoded = OpenSSL::ASN1.decode(der) info = decoded.value[0] # CertificationRequestInfo: [version, subject, spki, [0] attributes] subject = info.value[1] spki = info.value[2] attributes = info.value.find { |element| context_tag?(element, 0) } extension_nodes = request_extension_nodes(attributes) # walked once, shared by Extensions + SANs algorithm = PUBLIC_KEY_ALGORITHM_OIDS.fetch(spki.value[0].value[0].oid, 0) fields = REQUEST_ZERO_FIELDS.merge( "Version" => request.version.to_i, "Raw" => b64(der), "RawTBSCertificateRequest" => b64(info.to_der), "RawSubject" => b64(subject.to_der), "RawSubjectPublicKeyInfo" => b64(spki.to_der), "Subject" => Name.build(subject, lenient: true), "PublicKey" => algorithm.zero? ? nil : public_key(request), "PublicKeyAlgorithm" => algorithm, "SignatureAlgorithm" => signature_algorithm(request, decoded), "Signature" => b64(decoded.value[2].value), "Attributes" => request_attributes(attributes), "Extensions" => extension_nodes&.map { |ext| extension_entry(ext) } ) request_sans(fields, extension_nodes) scrub(fields) end |
.extension_nodes(tbs) ⇒ Object
The raw Extension nodes (the [3] EXPLICIT wrapper’s SEQUENCE OF Extension), or nil when the certificate carries no extensions. Shared by extension_list and the extension parsers.
167 168 169 170 171 172 |
# File 'lib/ruby/rego/builtins/crypto/certificate_struct.rb', line 167 def self.extension_nodes(tbs) wrapper = tbs.value.find { |element| context_tag?(element, 3) } return nil unless wrapper wrapper.value[0].value end |
.oid_ints(oid) ⇒ Object
OID dotted string -> array of integer arcs (Go marshals asn1.ObjectIdentifier as []int).
211 212 213 |
# File 'lib/ruby/rego/builtins/crypto/certificate_struct.rb', line 211 def self.oid_ints(oid) oid.split(".").map { |arc| Integer(arc) } end |
.public_key(cert) ⇒ Object
The PublicKey field, matching Go’s json.Marshal of the concrete public-key type: an rsa.PublicKey is E; an ecdsa.PublicKey is Curve:{, X, Y}; an ed25519.PublicKey is the std-base64 of its 32 bytes. rubocop:disable Metrics/AbcSize
19 20 21 22 23 24 25 26 27 |
# File 'lib/ruby/rego/builtins/crypto/certificate_public_key.rb', line 19 def self.public_key(cert) key = cert.public_key case key.oid when "id-ecPublicKey" then ec_public_key(key) when "ED25519" then Base64.strict_encode64(key.raw_public_key) when "DSA" then { "G" => key.g.to_i, "P" => key.p.to_i, "Q" => key.q.to_i, "Y" => key.pub_key.to_i } else { "N" => key.n.to_i, "E" => key.e.to_i } end end |
.safe_asn1?(der) ⇒ Boolean
Iterative scan of definite-length DER TLV headers (no decode, no recursion, never raises): false once nesting would exceed MAX_ASN1_DEPTH, and false on a BER indefinite length (which would otherwise bypass the depth count, and which DER — so Go — rejects anyway). Huge or truncated definite lengths are left for OpenSSL to reject shallowly (ASN1Error -> undefined). Public: the generic ASN.1-depth DoS guard, also called cross-module by the key parser’s format_matches? before it hands attacker DER to OpenSSL::ASN1.decode. rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity :reek:TooManyStatements – a single byte-scanning loop is the clearest form.
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/ruby/rego/builtins/crypto/certificate_extensions.rb', line 373 def self.safe_asn1?(der) bytes = der.b size = bytes.bytesize open_ends = [] # : Array[Integer] pos = 0 while pos < size open_ends.pop while !open_ends.empty? && pos >= open_ends.fetch(-1) return false if open_ends.size >= MAX_ASN1_DEPTH tag = bytes.getbyte(pos).to_i # to_i: pos < size guarantees a byte pos += 1 if tag.allbits?(0x1f) # high-tag-number form: skip the multi-byte tag pos += 1 while pos < size && bytes.getbyte(pos).to_i >= 0x80 pos += 1 end return true if pos >= size length_byte = bytes.getbyte(pos).to_i pos += 1 content_len = length_byte if length_byte >= 0x80 count = length_byte & 0x7f return false if count.zero? # BER indefinite length: bypasses the depth count; reject return true if count > 4 || (pos + count) > size content_len = 0 count.times do content_len = (content_len << 8) | bytes.getbyte(pos).to_i pos += 1 end end if tag.nobits?(0x20) # primitive: skip its content pos += content_len else open_ends.push(pos + content_len) end end true end |
.transcode(bytes, encoding) ⇒ Object
Transcode raw bytes from an ASN.1 string encoding to UTF-8, replacing invalid bytes with U+FFFD as Go’s json.Marshal does. Shared by the pkix.Name builder and the CSR attribute value marshaler (both reproduce Go’s asn1 string decoding). Called cross-module, so public. :reek:UtilityFunction – a pure byte-encoding transform.
229 230 231 |
# File 'lib/ruby/rego/builtins/crypto/certificate_struct.rb', line 229 def self.transcode(bytes, encoding) bytes.dup.force_encoding(encoding).encode(Encoding::UTF_8, invalid: :replace, undef: :replace) end |
.uri_fields(uri_texts) ⇒ Hash[String, untyped]
Builds the URIs / URIStrings fields from the URI SANs. crypto/x509 runs each through net/url.Parse (a reject -> the whole certificate fails to parse -> OPA undefined); URIs is json.Marshal of each *url.URL, URIStrings the injected url.String() of each.
16 17 18 19 20 |
# File 'lib/ruby/rego/builtins/crypto/certificate_uri.rb', line 16 def self.uri_fields(uri_texts) parsed = parsed_uris(uri_texts) { "URIs" => parsed.map { |components| url_struct(components) }, "URIStrings" => parsed.map { |components| Uri::Parser.string(components) } } end |