Module: PqcAsn1::DER

Defined in:
lib/pqc_asn1.rb,
lib/pqc_asn1.rb,
lib/pqc_asn1/key_sizes.rb

Overview

Unified result returned by DER.parse_spki and DER.parse_pkcs8.

An immutable value object carrying all parsed fields plus a format field (:spki or :pkcs8) that identifies which structure was parsed.

For SPKI: key is the public key bytes (frozen ASCII-8BIT String). For PKCS#8: key is a SecureBuffer; its backing memory is securely zeroed when the object is garbage-collected.

Supports attribute access (+result.oid+) and deconstruct_keys for pattern matching.

Examples:

SPKI

parsed = PqcAsn1::DER.parse_spki(der)
puts parsed.oid.name  # => "ML_DSA_44"

PKCS#8 pattern matching

case PqcAsn1::DER.parse_pkcs8(der)
in { format: :pkcs8, oid:, key: }
  key.use { |bytes| sign(bytes) }
end

Defined Under Namespace

Classes: CompositeKeyInfo, Cursor, EncryptedKeyInfo, KeyInfo

Constant Summary collapse

REGISTERED_KEY_SIZES =

Key sizes for algorithms registered at runtime via OID.register. Unlike KEY_SIZES (frozen at load time from oids.yml), this hash is mutable so OID.register can extend it without rebuilding KEY_SIZES. Intentionally a mutable constant (standard Ruby pattern); mutated only by OID.register under the GVL.

{}
KEY_SIZES =

Known key sizes (in bytes) for NIST PQC algorithms. Source: FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA).

{
  OID::ML_DSA_44 => { public: 1312, secret: 2560 }.freeze,
  OID::ML_DSA_65 => { public: 1952, secret: 4032 }.freeze,
  OID::ML_DSA_87 => { public: 2592, secret: 4896 }.freeze,
  OID::ML_KEM_512 => { public: 800, secret: 1632 }.freeze,
  OID::ML_KEM_768 => { public: 1184, secret: 2400 }.freeze,
  OID::ML_KEM_1024 => { public: 1568, secret: 3168 }.freeze,
  OID::SLH_DSA_SHA2_128S => { public: 32, secret: 64 }.freeze,
  OID::SLH_DSA_SHA2_128F => { public: 32, secret: 64 }.freeze,
  OID::SLH_DSA_SHA2_192S => { public: 48, secret: 96 }.freeze,
  OID::SLH_DSA_SHA2_192F => { public: 48, secret: 96 }.freeze,
  OID::SLH_DSA_SHA2_256S => { public: 64, secret: 128 }.freeze,
  OID::SLH_DSA_SHA2_256F => { public: 64, secret: 128 }.freeze,
  OID::SLH_DSA_SHAKE_128S => { public: 32, secret: 64 }.freeze,
  OID::SLH_DSA_SHAKE_128F => { public: 32, secret: 64 }.freeze,
  OID::SLH_DSA_SHAKE_192S => { public: 48, secret: 96 }.freeze,
  OID::SLH_DSA_SHAKE_192F => { public: 48, secret: 96 }.freeze,
  OID::SLH_DSA_SHAKE_256S => { public: 64, secret: 128 }.freeze,
  OID::SLH_DSA_SHAKE_256F => { public: 64, secret: 128 }.freeze,
}.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.max_input_size ⇒ Integer?

Returns maximum input size in bytes, or nil to disable.

Returns:

  • (Integer, nil) —

    maximum input size in bytes, or nil to disable



813
814
815
# File 'lib/pqc_asn1.rb', line 813

def max_input_size
  @max_input_size
end

Class Method Details

._c_build_pkcs8 ⇒ PqcAsn1::SecureBuffer

Build a PKCS#8 / OneAsymmetricKey DER structure.

Parameters:

  • oid (PqcAsn1::OID, String) —

    algorithm OID

  • secret_key (String) —

    raw secret key bytes

  • parameters (String, nil) —

    optional AlgorithmIdentifier parameters DER

  • public_key (String, nil) —

    optional publicKey [1] IMPLICIT BIT STRING

Returns:



# File 'lib/pqc_asn1.rb', line 828

._c_build_spki ⇒ String

Build a SubjectPublicKeyInfo (SPKI) DER structure.

Parameters:

  • oid (PqcAsn1::OID, String) —

    algorithm OID

  • public_key (String) —

    raw public key bytes

  • parameters (String, nil) —

    optional AlgorithmIdentifier parameters DER

Returns:

  • (String) —

    frozen DER bytes (ASCII-8BIT)



# File 'lib/pqc_asn1.rb', line 827

._c_parse_pkcs8 ⇒ PqcAsn1::DER::KeyInfo

Parse a PKCS#8 / OneAsymmetricKey DER structure.

Parameters:

Returns:

Raises:



# File 'lib/pqc_asn1.rb', line 830

._c_parse_spki ⇒ PqcAsn1::DER::KeyInfo

Parse a SubjectPublicKeyInfo DER structure.

Parameters:

  • der (String) —

    DER-encoded SPKI

Returns:

Raises:



# File 'lib/pqc_asn1.rb', line 829

.build_composite_spki ⇒ Object

Build a composite SubjectPublicKeyInfo DER structure.

Composite key structures (PQC + traditional hybrid algorithms) are defined in draft NIST/IETF standards and are not yet implemented. This placeholder stabilises the API surface.

Raises:

  • (NotImplementedError) —

    always



1069
1070
1071
1072
1073
# File 'lib/pqc_asn1.rb', line 1069

def self.build_composite_spki(**)
  raise NotImplementedError,
    "Composite key support is not yet implemented. " \
    "See https://github.com/msuliq/pqc_asn1/issues for status."
end

.build_encrypted_pkcs8(encryption_algorithm_der, encrypted_data) ⇒ String

Build an EncryptedPrivateKeyInfo DER structure (RFC 5958).

This gem is a codec — the caller must perform the actual encryption and provide the resulting ciphertext together with the AlgorithmIdentifier DER that describes the encryption scheme.

Parameters:

  • encryption_algorithm_der (String) —

    Full AlgorithmIdentifier DER TLV (tag 0x30 + length + OID + params).

  • encrypted_data (String) —

    Raw ciphertext bytes. These will be wrapped in an OCTET STRING.

Returns:

  • (String) —

    frozen DER bytes (ASCII-8BIT)

Raises:

  • (ArgumentError)


1011
1012
1013
1014
1015
1016
1017
1018
# File 'lib/pqc_asn1.rb', line 1011

def self.build_encrypted_pkcs8(encryption_algorithm_der, encrypted_data)
  raise ArgumentError, "encryption_algorithm_der must not be nil" if encryption_algorithm_der.nil?
  raise ArgumentError, "encrypted_data must not be nil" if encrypted_data.nil?

  enc_data_tlv = PqcAsn1::DER.write_tlv(0x04, encrypted_data)
  content = encryption_algorithm_der.b + enc_data_tlv
  PqcAsn1::DER.write_tlv(0x30, content).freeze
end

.build_pkcs8(oid, secret_key, parameters: nil, public_key: nil, validate: true) ⇒ PqcAsn1::SecureBuffer

Build a PKCS#8 DER structure.

When validate: true, checks that the secret key size matches the expected size for the given OID (if known) before encoding.

Parameters:

  • oid (PqcAsn1::OID, String)
  • secret_key (String, PqcAsn1::SecureBuffer)
  • parameters (String, nil) (defaults to: nil)
  • public_key (String, nil) (defaults to: nil)
  • validate (Boolean) (defaults to: true) —

    check key size against KEY_SIZES

Returns:



# File 'lib/pqc_asn1.rb', line 250

.build_spki(oid, public_key, parameters: nil, validate: true) ⇒ String

Build an SPKI DER structure.

When validate: true, checks that the public key size matches the expected size for the given OID (if known) before encoding.

Parameters:

  • oid (PqcAsn1::OID, String)
  • public_key (String)
  • parameters (String, nil) (defaults to: nil)
  • validate (Boolean) (defaults to: true) —

    check key size against KEY_SIZES

Returns:

  • (String) —

    frozen DER bytes



# File 'lib/pqc_asn1.rb', line 243

.check_input_size!(der) ⇒ Object

Raises:

  • (ArgumentError)


892
893
894
895
896
897
898
899
900
901
902
# File 'lib/pqc_asn1.rb', line 892

def check_input_size!(der)
  limit = PqcAsn1::DER.max_input_size
  return unless limit

  size = der.respond_to?(:bytesize) ? der.bytesize : 0
  return if size <= limit

  raise ArgumentError,
    "input size #{size} exceeds maximum allowed #{limit} bytes; " \
    "increase PqcAsn1::DER.max_input_size or set to nil to disable"
end

.parse_auto(der) ⇒ PqcAsn1::DER::KeyInfo, PqcAsn1::DER::EncryptedKeyInfo

Detect the DER format and dispatch to the appropriate parser.

Accepts a String (SPKI, PKCS#8, or EncryptedPrivateKeyInfo) or a SecureBuffer (always dispatched as PKCS#8 — public keys have no need for secure memory).

Parameters:

Returns:

Raises:



916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
# File 'lib/pqc_asn1.rb', line 916

def self.parse_auto(der)
  # SecureBuffer input is always PKCS#8 — public keys don't need
  # secure memory, so dispatch directly without format detection.
  return parse_pkcs8(der) if der.is_a?(PqcAsn1::SecureBuffer)

  case detect_format(der)
  when :spki then parse_spki(der)
  when :pkcs8 then parse_pkcs8(der)
  when :encrypted_pkcs8 then parse_encrypted_pkcs8(der)
  when nil
    raise PqcAsn1::DERError.new(
      "cannot detect DER format: input does not start with a valid DER SEQUENCE",
      code: :der_parse
    )
  else
    raise PqcAsn1::DERError.new(
      "cannot detect DER format: expected SPKI, PKCS#8, or EncryptedPrivateKeyInfo",
      code: :der_parse
    )
  end
end

.parse_composite_spki(_der) ⇒ Object

Parse a composite SubjectPublicKeyInfo DER structure.

Raises:

  • (NotImplementedError) —

    always



1078
1079
1080
1081
1082
# File 'lib/pqc_asn1.rb', line 1078

def self.parse_composite_spki(_der)
  raise NotImplementedError,
    "Composite key support is not yet implemented. " \
    "See https://github.com/msuliq/pqc_asn1/issues for status."
end

.parse_encrypted_pkcs8(der) ⇒ PqcAsn1::DER::EncryptedKeyInfo

Parse an EncryptedPrivateKeyInfo DER structure (RFC 5958).

This gem is a codec — it does not decrypt the key material. The returned EncryptedKeyInfo carries the opaque encryption_algorithm DER and raw encrypted_data bytes so the caller can pass them to their cipher implementation.

Parameters:

  • der (String) —

    DER-encoded EncryptedPrivateKeyInfo

Returns:

Raises:



975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
# File 'lib/pqc_asn1.rb', line 975

def self.parse_encrypted_pkcs8(der)
  check_input_size!(der)
  cursor = Cursor.new(der)
  outer = cursor.read_sequence
  unless cursor.eof?
    raise PqcAsn1::DERError.new(
      "EncryptedPrivateKeyInfo parse error: trailing data after outer SEQUENCE",
      code: :trailing_data
    )
  end

  # AlgorithmIdentifier is a SEQUENCE — capture the full TLV so callers
  # can pass it directly to their cipher without re-encoding.
  algo_tlv = outer.read_raw(0x30)
  encrypted_data = outer.read_octet_string
  unless outer.eof?
    raise PqcAsn1::DERError.new(
      "EncryptedPrivateKeyInfo parse error: extra fields after encryptedData",
      code: :extra_fields
    )
  end

  EncryptedKeyInfo.new(algo_tlv, encrypted_data)
end

.parse_pem(pem) ⇒ PqcAsn1::DER::KeyInfo, PqcAsn1::DER::EncryptedKeyInfo

Decode a PEM string and parse the contained DER structure.

Uses the PEM label to choose the parser:

"PUBLIC KEY"           → {parse_spki}
"PRIVATE KEY"          → {parse_pkcs8}
"ENCRYPTED PRIVATE KEY" → {parse_encrypted_pkcs8}

Any other label raises PEMError.

Parameters:

  • pem (String) —

    PEM-encoded key

Returns:

Raises:



950
951
952
953
954
955
956
957
958
959
960
961
962
963
# File 'lib/pqc_asn1.rb', line 950

def self.parse_pem(pem)
  result = PqcAsn1::PEM.decode_auto(pem)
  case result.label
  when "PUBLIC KEY" then parse_spki(result.data)
  when "PRIVATE KEY" then parse_pkcs8(result.data)
  when "ENCRYPTED PRIVATE KEY" then parse_encrypted_pkcs8(result.data)
  else
    raise PqcAsn1::PEMError.new(
      "unsupported PEM label #{result.label.inspect}: " \
      "expected \"PUBLIC KEY\", \"PRIVATE KEY\", or \"ENCRYPTED PRIVATE KEY\"",
      code: :pem_label
    )
  end
end

.parse_pkcs8(der) ⇒ Object

Parse a PKCS#8 DER structure with optional input size limit.



# File 'lib/pqc_asn1.rb', line 264

.parse_spki(der) ⇒ Object

Parse an SPKI DER structure with optional input size limit.



# File 'lib/pqc_asn1.rb', line 258

.validate_key_size(oid, key_bytesize, type) ⇒ true

Validate that a key's byte size matches the expected size for the given OID and type (:public or :secret).

Delegates to the C-level OID.validate_key_size table for known algorithms (O(1) lookup, no Ruby hash involved). Falls back to KEY_SIZES (built-in) then REGISTERED_KEY_SIZES (user-registered).

Raises ArgumentError if the OID is unknown (not in any table) or if the key size does not match the expected size.

Parameters:

  • oid (PqcAsn1::OID) —

    the algorithm OID

  • key_bytesize (Integer) —

    actual key size in bytes

  • type (Symbol) —

    :public or :secret

Returns:

  • (true)

Raises:

  • (ArgumentError) —

    if the size does not match or OID is unknown



1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
# File 'lib/pqc_asn1.rb', line 1035

def self.validate_key_size(oid, key_bytesize, type)
  # Fast path: C-level table for built-in NIST PQC algorithms.
  c_result = PqcAsn1::OID.validate_key_size(oid, key_bytesize, type == :public)
  return true if c_result

  # Fallback: built-in KEY_SIZES, then user-registered REGISTERED_KEY_SIZES.
  sizes = KEY_SIZES[oid] || REGISTERED_KEY_SIZES[oid]
  unless sizes
    name = (oid.respond_to?(:name) && oid.name) ? oid.name : oid.to_s
    raise ArgumentError,
      "unknown OID #{name} — cannot validate key size; " \
      "register it with OID.register or pass validate: false"
  end

  expected = sizes[type]
  return true unless expected

  if key_bytesize != expected
    name = (oid.respond_to?(:name) && oid.name) ? oid.name : oid.to_s
    raise ArgumentError,
      "#{type} key size #{key_bytesize} does not match expected " \
      "#{expected} for #{name}"
  end

  true
end