module

Krikri::X509CertInfo

Shared X.509 certificate parsing for the community.crypto info modules (x509_certificate_info, get_certificate). Both plugins need the same field set - subject/issuer dicts, validity, extensions, fingerprints, public key data - so it lives here once, driven by the openssl CLI (the same backend choice every other crypto plugin in this tree makes; there is no Python cryptography library to lean on on the target host).

Field vocabularies were matched against the Ansible module (community.crypto 3.1.1, ansible-core 2.19.4), whose own output comes from Python's cryptography library mapped through OpenSSL's objects.txt long names - which is exactly the vocabulary openssl x509 -text itself prints, so most fields read straight off that output. The module sorts every list-valued extension it returns (key usage, extended key usage, basic constraints), which openssl does not do, so parsed entries are sorted here.

extensions_by_oid (the DER-encoded value of every extension) comes from the same asn1parse walk for certificates and CSRs - see parse_extensions_by_oid.

Integers wider than Int64 (RSA moduli, EC coordinates, most serial numbers) go out as JSON numbers too - see json_int.

Constants

CRYPTOGRAPHY_CURVE_NAMES = {"prime192v1" => "secp192r1", "prime256v1" => "secp256r1"}

openssl's ASN1 OID short names where cryptography names the curve differently (the X9.62 prime curves); every other curve, secp384r1/secp521r1/brainpool* included, agrees.

CSR_INFO_KEY_ORDER = ["subject", "subject_ordered", "key_usage", "key_usage_critical", "extended_key_usage", "extended_key_usage_critical", "basic_constraints", "basic_constraints_critical", "ocsp_must_staple", "ocsp_must_staple_critical", "subject_alt_name", "subject_alt_name_critical", "name_constraints_permitted", "name_constraints_excluded", "name_constraints_critical", "public_key", "public_key_type", "public_key_data", "public_key_fingerprints", "subject_key_identifier", "authority_key_identifier", "authority_cert_issuer", "authority_cert_serial_number", "extensions_by_oid", "signature_valid"] of ::String

The real csr_info get_info()'s own key order (community.crypto's module_backends/csr_info.py CSRInfoRetrieval.get_info) - the controller backfills failed/changed after these.

EXTENSION_OID_NAMES = {"X509v3 Subject Alternative Name" => "2.5.29.17", "X509v3 Issuer Alternative Name" => "2.5.29.18", "X509v3 Basic Constraints" => "2.5.29.19", "X509v3 Key Usage" => "2.5.29.15", "X509v3 Extended Key Usage" => "2.5.29.37", "X509v3 Subject Key Identifier" => "2.5.29.14", "X509v3 Authority Key Identifier" => "2.5.29.35", "X509v3 CRL Distribution Points" => "2.5.29.31", "X509v3 Freshest CRL" => "2.5.29.46", "X509v3 Authority Information Access" => "1.3.6.1.5.5.7.1.1", "Authority Information Access" => "1.3.6.1.5.5.7.1.1", "Subject Information Access" => "1.3.6.1.5.5.7.1.11", "CT Precertificate SCTs" => "1.3.6.1.4.1.11129.2.4.2", "CT Precertificate Poison" => "1.3.6.1.4.1.11129.2.4.3", "X509v3 Subject Information Access" => "1.3.6.1.5.5.7.1.11", "X509v3 Name Constraints" => "2.5.29.30", "X509v3 Policy Constraints" => "2.5.29.36", "X509v3 Certificate Policies" => "2.5.29.32", "X509v3 Policy Mappings" => "2.5.29.33", "X509v3 Inhibit Any-Policy" => "2.5.29.54", "X509v3 CRL Number" => "2.5.29.20", "X509v3 Reason Code" => "2.5.29.21", "X509v3 Invalidity Date" => "2.5.29.24", "OCSP No Check" => "1.3.6.1.5.5.7.48.1.5", "TLS Feature" => "1.3.6.1.5.5.7.1.24", "X509v3 Signed Certificate Timestamp" => "1.3.6.1.4.1.11129.2.4.2", "X509v3 Certificate Type" => "2.16.840.1.113730.1.1", "X509v3 Netscape Cert Type" => "2.16.840.1.113730.1.1", "X509v3 S/MIME Capabilities" => "1.2.840.113549.1.9.15"}

The Ansible module's extensions_by_oid: cryptography_get_extensions_from_csr - {dotted_oid: {"critical": bool, "value": base64(DER of the extension value)}}. The extension value DER is exactly the content of each extension entry's extnValue OCTET STRING, which openssl asn1parse prints verbatim as a [HEX DUMP]. asn1parse only names known OIDs symbolically, so a name table covers the standard X509v3 extensions and an already dotted OBJECT passes through; names outside the table are skipped (an unknown key would be wrong, a missing one merely incomplete). Non-extension PKCS#9 request attributes (challengePassword etc.) have the same SEQUENCE{OBJECT,OCTET STRING} shape - their OIDs are excluded explicitly.

FINGERPRINT_ALGORITHMS = {"md5" => "md5", "sha1" => "sha1", "sha224" => "sha224", "sha256" => "sha256", "sha384" => "sha384", "sha512" => "sha512", "sha3_224" => "sha3-224", "sha3_256" => "sha3-256", "sha3_384" => "sha3-384", "sha3_512" => "sha3-512", "blake2b" => "blake2b512", "blake2s" => "blake2s256"}

hashlib.algorithms_guaranteed - ALL of it, including the two SHAKE XOFs: the Ansible module emits them too (its bare hexdigest() call raises TypeError on the XOFs and falls back to hexdigest(32), i.e. 32 BYTES for both shake_128 and shake_256). Crystal's OpenSSL::Digest cannot finalize an XOF (EVP_DigestFinal_ex has no XOF length knob - live-verified: it errors on both), so the SHAKE pair goes through the openssl dgst -shakeN -xoflen 32 CLI instead (OpenSSL 3.0+, jammy's 3.0.2 included). On an older openssl the CLI call fails and the two entries are omitted - the same graceful degradation the hash loop's rescue already applies. Everything else is emitted under the Ansible module's Python algorithm names.

NON_EXTENSION_OIDS = ["1.2.840.113549.1.9.7", "1.2.840.113549.1.9.9", "1.2.840.113549.1.9.14", "1.2.840.113549.1.9.3", "1.2.840.113549.1.9.4", "1.2.840.113549.1.9.26"]

Request attributes that share the extension shape but are not X.509 extensions.

SHAKE_ALGORITHMS = {"shake_128" => "shake128", "shake_256" => "shake256"}

The two SHAKE XOFs under their real-module (hashlib) names. The digest length is fixed at 32 bytes - the Ansible module's hexdigest(32) fallback, not the XOF's nominal block size.

Class methods

big_hex_to_decimal(hex : String) : String

Hex string -> decimal string, for values far wider than Int64 (an RSA modulus is 2048+ bits). Base-10 school multiplication over the hex nibbles - no BigInt shard in this tree.

Source
classify_public_key(text : String) : Tuple(String, Hash(String, JSON::Any))

Reads the same openssl -text shapes the Ansible module's Python backend reads from cryptography objects: RSA gives size/modulus/ exponent, ECC gives curve/x/y/exponent_size, Ed25519/X25519 etc. give an empty public_data dict.

Source
csr_info_ordered(csr_pem : String) : Hash(String, JSON::Any) | Nil

parse_csr's output reordered into the real get_info() order, with extensions_by_oid added - the shape both openssl_csr_info (as the task result) and openssl_csr (as its diff's before/after payload) must reproduce.

Source
der_bytes(args : Array(String)) : Bytes | Nil
Source
fingerprints(data : Bytes) : Hash(String, JSON::Any)
Source
fingerprints_any(data : Bytes) : JSON::Any
Source
json_int(decimal : String) : JSON::Any

A value that fits Int64 stays a JSON number (matching the real module's Python ints); anything wider - RSA moduli, ECC coordinates, big serial numbers - goes out as the BIGINT_SENTINEL_PREFIX-marked decimal string, because this engine's result world is JSON::Any (Int64 at widest) and truncating digits would be worse than a string. BasePlugin#run_and_capture rewrites the marked string to its bare digits on the JSON wire, and the controller's bigint-aware parse (Krikri::BigJson) turns those bytes back into the marked string - so every renderer downstream prints the value exactly like Ansible's Python int.

Source
labeled_hex_value(text : String, header : String) : JSON::Any | Nil

Public wrapper for the private-key modules: the decimal value of a labeled hex block ("prime1:", "priv:", ...) as a JSON int-or-string.

Source
parse(cert_pem : String, now : Time = Time.utc) : Hash(String, JSON::Any) | Nil

The main entry point: cert_pem is the PEM text of one certificate. Returns the module's result fields as a hash, or nil if openssl cannot parse it at all.

Source
parse_csr(csr_pem : String) : Hash(String, JSON::Any) | Nil

The openssl_csr_info half of the family: parses a PKCS#10 certificate request and returns the fields the Ansible module (community.crypto 3.1.1, csr_info.py's get_info) returns that this openssl-CLI backend can produce: subject, subject_ordered, key_usage/extended_key_usage/basic_constraints/ocsp_must_staple/ subject_alt_name (each with its _critical flag, absent exactly when the extension is absent - matching the real backend's None), public_key, public_key_type, public_key_data, public_key_fingerprints, and signature_valid (openssl's own req -verify - the Ansible module asks cryptography's is_signature_valid).

Same deliberate divergence as the certificate half above: extensions_by_oid, subject_key_identifier, authority_key_identifier and the name_constraints_* fields are not returned (they need an ASN.1 decoder this tree does not carry).

Source
parse_extensions_by_oid(csr_pem : String) : Hash(String, JSON::Any)
Source
run_openssl(args : Array(String)) : String | Nil
Source