Module: PQCrypto::PKCS8::PrivateKeyChoice

Defined in:
lib/pq_crypto/pkcs8/private_key_choice.rb

Constant Summary collapse

SEED_TAG =
0x80
EXPANDED_TAG =
0x04
BOTH_TAG =
0x30
KEYPAIR_FROM_SEED_METHODS =
{
  ml_kem_512: :native_ml_kem_512_keypair_from_seed,
  ml_kem_768: :native_ml_kem_keypair_from_seed,
  ml_kem_1024: :native_ml_kem_1024_keypair_from_seed,
  ml_dsa_44: :native_ml_dsa_44_keypair_from_seed,
  ml_dsa_65: :native_ml_dsa_keypair_from_seed,
  ml_dsa_87: :native_ml_dsa_87_keypair_from_seed,
}.freeze

Class Method Summary collapse

Class Method Details

.consistency_error_message(algorithm) ⇒ Object



179
180
181
182
183
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 179

def consistency_error_message(algorithm)
  return "seed/expandedKey inconsistency in ML-DSA PKCS#8 'both' encoding (RFC 9881 §6)" if ml_dsa_algorithm?(algorithm)

  "seed/expandedKey inconsistency in PKCS#8 'both' encoding (RFC 9935 §8)"
end

.decode(algorithm, choice_der) ⇒ Object

Raises:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 36

def decode(algorithm, choice_der)
  tag = choice_der.getbyte(0)
  raise SerializationError, "PKCS#8 privateKey CHOICE is empty" if tag.nil?

  case tag
  when SEED_TAG
    ensure_format_supported!(algorithm, :seed)
    decode_seed(algorithm, choice_der)
  when EXPANDED_TAG
    ensure_format_supported!(algorithm, :expanded)
    decode_expanded(algorithm, choice_der)
  when BOTH_TAG
    ensure_format_supported!(algorithm, :both)
    decode_both(algorithm, choice_der)
  else
    raise SerializationError,
          "Unsupported PKCS#8 #{algorithm.inspect} private key CHOICE tag: 0x#{tag.to_s(16).rjust(2, '0')}"
  end
end

.decode_both(algorithm, choice_der) ⇒ Object

Raises:



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 123

def decode_both(algorithm, choice_der)
  _tag, body, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: BOTH_TAG, label: "both")
  raise SerializationError, "PKCS#8 both contains trailing data" unless next_offset == choice_der.bytesize

  _seed_tag, seed_bytes, offset = DER.decode_tlv(body, 0, expected_tag: EXPANDED_TAG, label: "both seed")
  _expanded_tag, expanded_bytes, offset = DER.decode_tlv(body, offset, expected_tag: EXPANDED_TAG, label: "both expandedKey")
  raise SerializationError, "PKCS#8 both must contain exactly 2 elements" unless offset == body.bytesize

  validate_seed_length!(algorithm, seed_bytes)
  validate_expanded_length!(algorithm, expanded_bytes)
  verify_both_consistency!(algorithm, seed_bytes, expanded_bytes)

  [algorithm, :both, [seed_bytes, expanded_bytes]]
end

.decode_expanded(algorithm, choice_der) ⇒ Object

Raises:



115
116
117
118
119
120
121
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 115

def decode_expanded(algorithm, choice_der)
  _tag, bytes, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: EXPANDED_TAG, label: "expandedKey")
  raise SerializationError, "PKCS#8 expandedKey contains trailing data" unless next_offset == choice_der.bytesize

  validate_expanded_length!(algorithm, bytes)
  [algorithm, :expanded, bytes]
end

.decode_seed(algorithm, choice_der) ⇒ Object

Raises:



107
108
109
110
111
112
113
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 107

def decode_seed(algorithm, choice_der)
  _tag, seed, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: SEED_TAG, label: "seed")
  raise SerializationError, "PKCS#8 seed contains trailing data" unless next_offset == choice_der.bytesize

  validate_seed_length!(algorithm, seed)
  [algorithm, :seed, seed]
end

.encode(algorithm, secret_material, format) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 21

def encode(algorithm, secret_material, format)
  ensure_format_supported!(algorithm, format)

  case format
  when :seed
    encode_seed(algorithm, secret_material)
  when :expanded
    encode_expanded(algorithm, secret_material)
  when :both
    encode_both(algorithm, secret_material)
  else
    raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
  end
end

.encode_both(algorithm, secret_material) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 152

def encode_both(algorithm, secret_material)
  unless secret_material.is_a?(Array) && secret_material.size == 2
    raise SerializationError, "PKCS#8 both format requires [seed, expandedKey]"
  end

  seed, expanded = secret_material
  seed_bytes = Internal.binary_string(seed)
  expanded_bytes = Internal.binary_string(expanded)
  validate_seed_length!(algorithm, seed_bytes)
  validate_expanded_length!(algorithm, expanded_bytes)

  body = DER.encode_tlv(EXPANDED_TAG, seed_bytes) + DER.encode_tlv(EXPANDED_TAG, expanded_bytes)
  DER.encode_tlv(BOTH_TAG, body)
end

.encode_expanded(algorithm, secret_material) ⇒ Object



145
146
147
148
149
150
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 145

def encode_expanded(algorithm, secret_material)
  bytes = Internal.binary_string(secret_material)
  validate_expanded_length!(algorithm, bytes)

  DER.encode_tlv(EXPANDED_TAG, bytes)
end

.encode_seed(algorithm, secret_material) ⇒ Object



138
139
140
141
142
143
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 138

def encode_seed(algorithm, secret_material)
  seed = Internal.binary_string(secret_material)
  validate_seed_length!(algorithm, seed)

  DER.encode_tlv(SEED_TAG, seed)
end

.ensure_format_supported!(algorithm, format) ⇒ Object

Raises:



62
63
64
65
66
67
68
69
70
71
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 62

def ensure_format_supported!(algorithm, format)
  if ml_dsa_algorithm?(algorithm) && %i[seed both].include?(format) && !PKCS8.allow_ml_dsa_seed_format
    raise SerializationError,
          "ML-DSA seed-format PKCS#8 is opt-in; set PQCrypto::PKCS8.allow_ml_dsa_seed_format = true to enable (see SECURITY.md for caveats)"
  end

  return if profile(algorithm).fetch(:supported_formats).include?(format)

  raise SerializationError, "Unsupported PKCS#8 private key format for #{algorithm.inspect}: #{format.inspect}"
end

.expanded_bytes(algorithm) ⇒ Object



77
78
79
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 77

def expanded_bytes(algorithm)
  profile(algorithm).fetch(:expanded_bytes)
end

.ml_dsa_algorithm?(algorithm) ⇒ Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 81

def ml_dsa_algorithm?(algorithm)
  %i[ml_dsa_44 ml_dsa_65 ml_dsa_87].include?(algorithm)
end

.profile(algorithm) ⇒ Object



85
86
87
88
89
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 85

def profile(algorithm)
  PKCS8::PRIVATE_KEY_CHOICES.fetch(algorithm) do
    raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
  end
end

.seed_bytes(algorithm) ⇒ Object



73
74
75
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 73

def seed_bytes(algorithm)
  profile(algorithm).fetch(:seed_bytes)
end

.validate_expanded_length!(algorithm, expanded) ⇒ Object

Raises:



99
100
101
102
103
104
105
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 99

def validate_expanded_length!(algorithm, expanded)
  expected = expanded_bytes(algorithm)
  return if expanded.bytesize == expected

  raise SerializationError,
        "Invalid #{algorithm.inspect} expanded private key length: expected #{expected}, got #{expanded.bytesize}"
end

.validate_secret_key_algorithm!(algorithm, entry) ⇒ Object

Raises:



56
57
58
59
60
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 56

def validate_secret_key_algorithm!(algorithm, entry)
  return if PKCS8::PRIVATE_KEY_CHOICES.key?(algorithm) && %i[ml_kem ml_dsa].include?(entry.fetch(:family))

  raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
end

.validate_seed_length!(algorithm, seed) ⇒ Object

Raises:



91
92
93
94
95
96
97
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 91

def validate_seed_length!(algorithm, seed)
  expected = seed_bytes(algorithm)
  return if seed.bytesize == expected

  raise SerializationError,
        "Invalid #{algorithm.inspect} seed private key length: expected #{expected}, got #{seed.bytesize}"
end

.verify_both_consistency!(algorithm, seed, expanded) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
# File 'lib/pq_crypto/pkcs8/private_key_choice.rb', line 167

def verify_both_consistency!(algorithm, seed, expanded)
  native_method = KEYPAIR_FROM_SEED_METHODS.fetch(algorithm, nil)
  return if native_method.nil?

  _public_key, expected_expanded = PQCrypto.__send__(native_method, seed)
  return if Internal.constant_time_equal?(expected_expanded, expanded)

  raise SerializationError, consistency_error_message(algorithm)
ensure
  Internal.safe_wipe(expected_expanded) if defined?(expected_expanded)
end