Module: CsrPeek::Extensions
- Defined in:
- lib/csr_peek/extensions.rb
Overview
Decoders for the X.509 extensions we surface, working from the extension's DER rather than OpenSSL's human-readable #value string. The display string splits on ", " (which corrupts a directoryName between RDNs) and its labels are locale- and version-dependent; decoding the ASN.1 is exact and stable. Every method tolerates a nil extension and never raises.
Constant Summary collapse
- GN_RFC822 =
GeneralName CHOICE context tags (RFC 5280 4.2.1.6).
1- GN_DNS =
rfc822Name (email)
2- GN_URI =
dNSName
6- GN_IP =
uniformResourceIdentifier
7- GN_DIRNAME =
iPAddress
4- KEY_USAGE_NAMES =
keyUsage BIT STRING positions (RFC 5280 4.2.1.3), bit 0 first.
%w[ digitalSignature nonRepudiation keyEncipherment dataEncipherment keyAgreement keyCertSign cRLSign encipherOnly decipherOnly ].freeze
Class Method Summary collapse
-
.basic_constraints(extension) ⇒ Object
basicConstraints as { ca: true/false, path_length: Integer or nil }.
- .bit_set?(bytes, index) ⇒ Boolean
-
.deep_freeze(sans) ⇒ Object
Deep-freeze so the value object that holds this is genuinely immutable: the category arrays AND the (OpenSSL-derived, non-literal) strings inside.
- .empty_sans ⇒ Object
-
.extended_key_usages(extension) ⇒ Object
extendedKeyUsage as short OID names, e.g.
-
.extn_value_der(extension) ⇒ Object
The raw DER carried inside an extension's OCTET STRING.
-
.format_ip(bytes) ⇒ Object
dotted-quad for IPv4, canonical (compressed) form for IPv6, nil otherwise.
-
.general_name_to_s(gn) ⇒ Object
directoryName is decoded to its slash form so no information is lost; any other kind is labelled by its tag number.
- .general_names(sequence) ⇒ Object
-
.key_usages(extension) ⇒ Object
keyUsage as canonical names, e.g.
- .parse_san(extension) ⇒ Object
-
.subject_alt_names(extension) ⇒ Object
Parse a subjectAltName extension into { dns:, ip:, email:, uri:, other: }.
Class Method Details
.basic_constraints(extension) ⇒ Object
basicConstraints as { ca: true/false, path_length: Integer or nil }.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/csr_peek/extensions.rb', line 88 def basic_constraints(extension) absent = {ca: false, path_length: nil} return absent if extension.nil? der = extn_value_der(extension) return absent if der.nil? sequence = OpenSSL::ASN1.decode(der) elements = Array(sequence.value) ca = elements.find { |e| e.is_a?(OpenSSL::ASN1::Boolean) }&.value || false path = elements.find { |e| e.is_a?(OpenSSL::ASN1::Integer) } {ca: ca, path_length: path&.value&.to_i} rescue OpenSSL::ASN1::ASN1Error, NoMethodError absent end |
.bit_set?(bytes, index) ⇒ Boolean
150 151 152 153 154 155 |
# File 'lib/csr_peek/extensions.rb', line 150 def bit_set?(bytes, index) byte = bytes[index / 8] return false if byte.nil? byte.anybits?(0x80 >> (index % 8)) end |
.deep_freeze(sans) ⇒ Object
Deep-freeze so the value object that holds this is genuinely immutable: the category arrays AND the (OpenSSL-derived, non-literal) strings inside.
52 53 54 55 |
# File 'lib/csr_peek/extensions.rb', line 52 def deep_freeze(sans) sans.each_value { |values| values.each(&:freeze).freeze } sans.freeze end |
.empty_sans ⇒ Object
28 29 30 |
# File 'lib/csr_peek/extensions.rb', line 28 def empty_sans {dns: [], ip: [], email: [], uri: [], other: []} end |
.extended_key_usages(extension) ⇒ Object
extendedKeyUsage as short OID names, e.g. ["serverAuth", "clientAuth"]. Falls back to the dotted OID for usages OpenSSL cannot name.
73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/csr_peek/extensions.rb', line 73 def extended_key_usages(extension) return [] if extension.nil? der = extn_value_der(extension) return [] if der.nil? sequence = OpenSSL::ASN1.decode(der) Array(sequence.value).map do |oid| (oid.respond_to?(:sn) ? (oid.sn || oid.oid) : oid.to_s).freeze end rescue OpenSSL::ASN1::ASN1Error, NoMethodError [] end |
.extn_value_der(extension) ⇒ Object
The raw DER carried inside an extension's OCTET STRING.
105 106 107 108 109 |
# File 'lib/csr_peek/extensions.rb', line 105 def extn_value_der(extension) sequence = OpenSSL::ASN1.decode(extension.to_der) octet = Array(sequence.value).find { |el| el.is_a?(OpenSSL::ASN1::OctetString) } octet&.value end |
.format_ip(bytes) ⇒ Object
dotted-quad for IPv4, canonical (compressed) form for IPv6, nil otherwise.
128 129 130 131 132 133 134 135 136 |
# File 'lib/csr_peek/extensions.rb', line 128 def format_ip(bytes) raw = bytes.to_s case raw.bytesize when 4 then raw.unpack("C4").join(".") when 16 then IPAddr.new(IPAddr.ntop(raw)).to_s end rescue IPAddr::Error nil end |
.general_name_to_s(gn) ⇒ Object
directoryName is decoded to its slash form so no information is lost; any other kind is labelled by its tag number.
140 141 142 143 144 145 146 147 148 |
# File 'lib/csr_peek/extensions.rb', line 140 def general_name_to_s(gn) if gn.tag == GN_DIRNAME inner = Array(gn.value).first return "dirName:#{OpenSSL::X509::Name.new(inner.to_der)}" if inner end "tag#{gn.tag}" rescue "tag#{gn.tag}" end |
.general_names(sequence) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/csr_peek/extensions.rb', line 111 def general_names(sequence) result = empty_sans Array(sequence.value).each do |gn| next unless gn.respond_to?(:tag) && gn.tag_class == :CONTEXT_SPECIFIC case gn.tag when GN_DNS then result[:dns] << gn.value.to_s when GN_RFC822 then result[:email] << gn.value.to_s when GN_URI then result[:uri] << gn.value.to_s when GN_IP then (ip = format_ip(gn.value)) && result[:ip] << ip else result[:other] << general_name_to_s(gn) end end result end |
.key_usages(extension) ⇒ Object
keyUsage as canonical names, e.g. ["digitalSignature", "keyEncipherment"].
58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/csr_peek/extensions.rb', line 58 def key_usages(extension) return [] if extension.nil? der = extn_value_der(extension) return [] if der.nil? bits = OpenSSL::ASN1.decode(der) bytes = bits.value.bytes KEY_USAGE_NAMES.each_index.select { |i| bit_set?(bytes, i) }.map { |i| KEY_USAGE_NAMES[i] } rescue OpenSSL::ASN1::ASN1Error, NoMethodError [] end |
.parse_san(extension) ⇒ Object
39 40 41 42 43 44 45 46 47 48 |
# File 'lib/csr_peek/extensions.rb', line 39 def parse_san(extension) return empty_sans if extension.nil? der = extn_value_der(extension) return empty_sans if der.nil? general_names(OpenSSL::ASN1.decode(der)) rescue OpenSSL::ASN1::ASN1Error, OpenSSL::X509::ExtensionError empty_sans end |
.subject_alt_names(extension) ⇒ Object
Parse a subjectAltName extension into { dns:, ip:, email:, uri:, other: }. The result is deep-frozen: the value objects hold it directly, and a caller must not be able to mutate a memoized view.
35 36 37 |
# File 'lib/csr_peek/extensions.rb', line 35 def subject_alt_names(extension) deep_freeze(parse_san(extension)) end |