pq_crypto 0.3.2 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +56 -0
- data/CHANGELOG.md +37 -0
- data/GET_STARTED.md +361 -40
- data/README.md +58 -241
- data/SECURITY.md +101 -82
- data/ext/pqcrypto/extconf.rb +40 -7
- data/ext/pqcrypto/mldsa_api.h +71 -1
- data/ext/pqcrypto/mlkem_api.h +24 -0
- data/ext/pqcrypto/pq_externalmu.c +14 -1
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +484 -81
- data/ext/pqcrypto/pqcrypto_secure.c +179 -72
- data/ext/pqcrypto/pqcrypto_secure.h +87 -7
- data/ext/pqcrypto/pqcrypto_version.h +7 -0
- data/ext/pqcrypto/vendor/.vendored +1 -1
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
- data/lib/pq_crypto/algorithm_registry.rb +200 -0
- data/lib/pq_crypto/hybrid_kem.rb +1 -12
- data/lib/pq_crypto/kem.rb +104 -13
- data/lib/pq_crypto/pkcs8.rb +387 -0
- data/lib/pq_crypto/serialization.rb +1 -14
- data/lib/pq_crypto/signature.rb +123 -17
- data/lib/pq_crypto/spki.rb +131 -0
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +78 -19
- data/script/vendor_libs.rb +4 -0
- metadata +95 -3
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module PQCrypto
|
|
6
|
+
module PKCS8
|
|
7
|
+
PEM_LABEL = "PRIVATE KEY"
|
|
8
|
+
PEM_BEGIN = "-----BEGIN #{PEM_LABEL}-----"
|
|
9
|
+
PEM_END = "-----END #{PEM_LABEL}-----"
|
|
10
|
+
ML_KEM_SEED_BYTES = 64
|
|
11
|
+
ML_DSA_SEED_BYTES = 32
|
|
12
|
+
|
|
13
|
+
@allow_ml_dsa_seed_format = false
|
|
14
|
+
|
|
15
|
+
PRIVATE_KEY_CHOICES = {
|
|
16
|
+
ml_kem_512: {
|
|
17
|
+
seed_bytes: ML_KEM_SEED_BYTES,
|
|
18
|
+
expanded_bytes: PQCrypto::ML_KEM_512_SECRET_KEY_BYTES,
|
|
19
|
+
supported_formats: %i[seed expanded both],
|
|
20
|
+
}.freeze,
|
|
21
|
+
ml_kem_768: {
|
|
22
|
+
seed_bytes: ML_KEM_SEED_BYTES,
|
|
23
|
+
expanded_bytes: PQCrypto::ML_KEM_SECRET_KEY_BYTES,
|
|
24
|
+
supported_formats: %i[seed expanded both],
|
|
25
|
+
}.freeze,
|
|
26
|
+
ml_kem_1024: {
|
|
27
|
+
seed_bytes: ML_KEM_SEED_BYTES,
|
|
28
|
+
expanded_bytes: PQCrypto::ML_KEM_1024_SECRET_KEY_BYTES,
|
|
29
|
+
supported_formats: %i[seed expanded both],
|
|
30
|
+
}.freeze,
|
|
31
|
+
ml_dsa_44: {
|
|
32
|
+
seed_bytes: ML_DSA_SEED_BYTES,
|
|
33
|
+
expanded_bytes: PQCrypto::SIGN_44_SECRET_KEY_BYTES,
|
|
34
|
+
supported_formats: %i[seed expanded both],
|
|
35
|
+
}.freeze,
|
|
36
|
+
ml_dsa_65: {
|
|
37
|
+
seed_bytes: ML_DSA_SEED_BYTES,
|
|
38
|
+
expanded_bytes: PQCrypto::SIGN_SECRET_KEY_BYTES,
|
|
39
|
+
supported_formats: %i[seed expanded both],
|
|
40
|
+
}.freeze,
|
|
41
|
+
ml_dsa_87: {
|
|
42
|
+
seed_bytes: ML_DSA_SEED_BYTES,
|
|
43
|
+
expanded_bytes: PQCrypto::SIGN_87_SECRET_KEY_BYTES,
|
|
44
|
+
supported_formats: %i[seed expanded both],
|
|
45
|
+
}.freeze,
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
attr_accessor :allow_ml_dsa_seed_format
|
|
50
|
+
|
|
51
|
+
def encode_der(algorithm_symbol, secret_material, format:)
|
|
52
|
+
entry = AlgorithmRegistry.fetch(algorithm_symbol)
|
|
53
|
+
validate_secret_key_algorithm!(algorithm_symbol, entry)
|
|
54
|
+
ensure_format_supported!(algorithm_symbol, format)
|
|
55
|
+
|
|
56
|
+
choice_der = case format
|
|
57
|
+
when :seed
|
|
58
|
+
encode_seed_choice(secret_material, algorithm_symbol)
|
|
59
|
+
when :expanded
|
|
60
|
+
encode_expanded_key_choice(secret_material, algorithm_symbol)
|
|
61
|
+
when :both
|
|
62
|
+
encode_both_choice(secret_material, algorithm_symbol)
|
|
63
|
+
else
|
|
64
|
+
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
OpenSSL::ASN1::Sequence.new([
|
|
68
|
+
OpenSSL::ASN1::Integer.new(0),
|
|
69
|
+
OpenSSL::ASN1::Sequence.new([
|
|
70
|
+
OpenSSL::ASN1::ObjectId.new(AlgorithmRegistry.standard_oid(algorithm_symbol)),
|
|
71
|
+
]),
|
|
72
|
+
OpenSSL::ASN1::OctetString.new(choice_der),
|
|
73
|
+
]).to_der.b
|
|
74
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
75
|
+
raise SerializationError, e.message
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def encode_pem(algorithm_symbol, secret_material, format:)
|
|
79
|
+
der = encode_der(algorithm_symbol, secret_material, format: format)
|
|
80
|
+
body = encode_base64(der).scan(/.{1,64}/).join("\n")
|
|
81
|
+
"#{PEM_BEGIN}\n#{body}\n#{PEM_END}\n"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def decode_der(der)
|
|
85
|
+
input = String(der).b
|
|
86
|
+
outer = decode_asn1(input)
|
|
87
|
+
raise SerializationError, "PKCS#8 DER contains trailing data" unless outer.to_der.b == input
|
|
88
|
+
raise SerializationError, "PKCS#8 must be an ASN.1 SEQUENCE" unless outer.is_a?(OpenSSL::ASN1::Sequence)
|
|
89
|
+
raise SerializationError, "PKCS#8 OneAsymmetricKey must contain exactly 3 elements" unless outer.value.size == 3
|
|
90
|
+
|
|
91
|
+
version, algorithm_identifier, private_key = outer.value
|
|
92
|
+
decode_version(version)
|
|
93
|
+
algorithm = decode_algorithm_identifier(algorithm_identifier)
|
|
94
|
+
entry = AlgorithmRegistry.fetch(algorithm)
|
|
95
|
+
validate_secret_key_algorithm!(algorithm, entry)
|
|
96
|
+
|
|
97
|
+
unless private_key.is_a?(OpenSSL::ASN1::OctetString)
|
|
98
|
+
raise SerializationError, "PKCS#8 privateKey must be an OCTET STRING"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
decode_private_key_choice(algorithm, String(private_key.value).b)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def decode_pem(pem)
|
|
105
|
+
der = der_from_pem(pem)
|
|
106
|
+
decode_der(der)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def decode_asn1(der)
|
|
112
|
+
OpenSSL::ASN1.decode(der)
|
|
113
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
114
|
+
raise SerializationError, e.message
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def decode_version(value)
|
|
118
|
+
raise SerializationError, "PKCS#8 version must be an INTEGER" unless value.is_a?(OpenSSL::ASN1::Integer)
|
|
119
|
+
|
|
120
|
+
version = value.value.respond_to?(:to_i) ? value.value.to_i : value.value
|
|
121
|
+
raise SerializationError, "PKCS#8 version must be 0" unless version == 0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def decode_algorithm_identifier(value)
|
|
125
|
+
unless value.is_a?(OpenSSL::ASN1::Sequence)
|
|
126
|
+
raise SerializationError, "PKCS#8 algorithm must be an AlgorithmIdentifier SEQUENCE"
|
|
127
|
+
end
|
|
128
|
+
unless value.value.size == 1
|
|
129
|
+
raise SerializationError, "PKCS#8 AlgorithmIdentifier parameters must be absent"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
oid = value.value.first
|
|
133
|
+
raise SerializationError, "PKCS#8 AlgorithmIdentifier must contain an OBJECT IDENTIFIER" unless oid.is_a?(OpenSSL::ASN1::ObjectId)
|
|
134
|
+
|
|
135
|
+
algorithm = AlgorithmRegistry.by_standard_oid(oid.oid)
|
|
136
|
+
raise SerializationError, "Unsupported PKCS#8 algorithm OID: #{oid.oid}" if algorithm.nil?
|
|
137
|
+
|
|
138
|
+
algorithm
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def decode_private_key_choice(algorithm, choice_der)
|
|
142
|
+
tag = choice_der.getbyte(0)
|
|
143
|
+
raise SerializationError, "PKCS#8 privateKey CHOICE is empty" if tag.nil?
|
|
144
|
+
|
|
145
|
+
case tag
|
|
146
|
+
when 0x80
|
|
147
|
+
ensure_format_supported!(algorithm, :seed)
|
|
148
|
+
decode_seed_choice(algorithm, choice_der)
|
|
149
|
+
when 0x04
|
|
150
|
+
ensure_format_supported!(algorithm, :expanded)
|
|
151
|
+
decode_expanded_key(algorithm, choice_der)
|
|
152
|
+
when 0x30
|
|
153
|
+
ensure_format_supported!(algorithm, :both)
|
|
154
|
+
decode_both_choice(algorithm, choice_der)
|
|
155
|
+
else
|
|
156
|
+
raise SerializationError,
|
|
157
|
+
"Unsupported PKCS#8 #{algorithm.inspect} private key CHOICE tag: 0x#{tag.to_s(16).rjust(2, '0')}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def decode_seed_choice(algorithm, choice_der)
|
|
162
|
+
seed = decode_tlv_value(choice_der, expected_tag: 0x80, label: "seed")
|
|
163
|
+
validate_seed_length!(algorithm, seed)
|
|
164
|
+
|
|
165
|
+
[algorithm, :seed, seed]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def decode_expanded_key(algorithm, choice_der)
|
|
169
|
+
expanded = decode_asn1(choice_der)
|
|
170
|
+
unless expanded.to_der.b == choice_der
|
|
171
|
+
raise SerializationError, "PKCS#8 expandedKey contains trailing data"
|
|
172
|
+
end
|
|
173
|
+
unless expanded.is_a?(OpenSSL::ASN1::OctetString)
|
|
174
|
+
raise SerializationError, "PKCS#8 expandedKey must be an OCTET STRING"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
bytes = String(expanded.value).b
|
|
178
|
+
validate_expanded_key_length!(algorithm, bytes)
|
|
179
|
+
|
|
180
|
+
[algorithm, :expanded, bytes]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def decode_both_choice(algorithm, choice_der)
|
|
184
|
+
both = decode_asn1(choice_der)
|
|
185
|
+
raise SerializationError, "PKCS#8 both contains trailing data" unless both.to_der.b == choice_der
|
|
186
|
+
raise SerializationError, "PKCS#8 both must be a SEQUENCE" unless both.is_a?(OpenSSL::ASN1::Sequence)
|
|
187
|
+
raise SerializationError, "PKCS#8 both must contain exactly 2 elements" unless both.value.size == 2
|
|
188
|
+
|
|
189
|
+
seed, expanded = both.value
|
|
190
|
+
raise SerializationError, "PKCS#8 both seed must be an OCTET STRING" unless seed.is_a?(OpenSSL::ASN1::OctetString)
|
|
191
|
+
unless expanded.is_a?(OpenSSL::ASN1::OctetString)
|
|
192
|
+
raise SerializationError, "PKCS#8 both expandedKey must be an OCTET STRING"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
seed_bytes = String(seed.value).b
|
|
196
|
+
expanded_bytes = String(expanded.value).b
|
|
197
|
+
validate_seed_length!(algorithm, seed_bytes)
|
|
198
|
+
validate_expanded_key_length!(algorithm, expanded_bytes)
|
|
199
|
+
verify_both_consistency!(algorithm, seed_bytes, expanded_bytes)
|
|
200
|
+
|
|
201
|
+
[algorithm, :both, [seed_bytes, expanded_bytes]]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def encode_seed_choice(secret_material, algorithm)
|
|
205
|
+
seed = String(secret_material).b
|
|
206
|
+
validate_seed_length!(algorithm, seed)
|
|
207
|
+
|
|
208
|
+
encode_tlv(0x80, seed)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def encode_expanded_key_choice(secret_material, algorithm)
|
|
212
|
+
bytes = String(secret_material).b
|
|
213
|
+
validate_expanded_key_length!(algorithm, bytes)
|
|
214
|
+
|
|
215
|
+
OpenSSL::ASN1::OctetString.new(bytes).to_der.b
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def encode_both_choice(secret_material, algorithm)
|
|
219
|
+
unless secret_material.is_a?(Array) && secret_material.size == 2
|
|
220
|
+
raise SerializationError, "PKCS#8 both format requires [seed, expandedKey]"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
seed, expanded = secret_material
|
|
224
|
+
seed_bytes = String(seed).b
|
|
225
|
+
expanded_bytes = String(expanded).b
|
|
226
|
+
validate_seed_length!(algorithm, seed_bytes)
|
|
227
|
+
validate_expanded_key_length!(algorithm, expanded_bytes)
|
|
228
|
+
|
|
229
|
+
OpenSSL::ASN1::Sequence.new([
|
|
230
|
+
OpenSSL::ASN1::OctetString.new(seed_bytes),
|
|
231
|
+
OpenSSL::ASN1::OctetString.new(expanded_bytes),
|
|
232
|
+
]).to_der.b
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def verify_both_consistency!(algorithm, seed, expanded)
|
|
236
|
+
native_method = {
|
|
237
|
+
ml_kem_512: :native_ml_kem_512_keypair_from_seed,
|
|
238
|
+
ml_kem_768: :native_ml_kem_keypair_from_seed,
|
|
239
|
+
ml_kem_1024: :native_ml_kem_1024_keypair_from_seed,
|
|
240
|
+
ml_dsa_44: :native_ml_dsa_44_keypair_from_seed,
|
|
241
|
+
ml_dsa_65: :native_ml_dsa_keypair_from_seed,
|
|
242
|
+
ml_dsa_87: :native_ml_dsa_87_keypair_from_seed,
|
|
243
|
+
}[algorithm]
|
|
244
|
+
return if native_method.nil?
|
|
245
|
+
|
|
246
|
+
_public_key, expected_expanded = PQCrypto.__send__(native_method, seed)
|
|
247
|
+
return if PQCrypto.__send__(:native_ct_equals, expected_expanded, expanded)
|
|
248
|
+
|
|
249
|
+
message = if ml_dsa_algorithm?(algorithm)
|
|
250
|
+
"seed/expandedKey inconsistency in ML-DSA PKCS#8 'both' encoding (RFC 9881 §6)"
|
|
251
|
+
else
|
|
252
|
+
"seed/expandedKey inconsistency in PKCS#8 'both' encoding (RFC 9935 §8)"
|
|
253
|
+
end
|
|
254
|
+
raise SerializationError, message
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def validate_seed_length!(algorithm, seed)
|
|
258
|
+
expected = choice_profile(algorithm).fetch(:seed_bytes)
|
|
259
|
+
return if seed.bytesize == expected
|
|
260
|
+
|
|
261
|
+
raise SerializationError,
|
|
262
|
+
"Invalid #{algorithm.inspect} seed private key length: expected #{expected}, got #{seed.bytesize}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def validate_expanded_key_length!(algorithm, expanded)
|
|
266
|
+
expected = choice_profile(algorithm).fetch(:expanded_bytes)
|
|
267
|
+
return if expanded.bytesize == expected
|
|
268
|
+
|
|
269
|
+
raise SerializationError,
|
|
270
|
+
"Invalid #{algorithm.inspect} expanded private key length: expected #{expected}, got #{expanded.bytesize}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def validate_secret_key_algorithm!(algorithm_symbol, entry)
|
|
274
|
+
return if PRIVATE_KEY_CHOICES.key?(algorithm_symbol) && %i[ml_kem ml_dsa].include?(entry.fetch(:family))
|
|
275
|
+
|
|
276
|
+
raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm_symbol.inspect}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def choice_profile(algorithm)
|
|
280
|
+
PRIVATE_KEY_CHOICES.fetch(algorithm) do
|
|
281
|
+
raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def ensure_format_supported!(algorithm, format)
|
|
286
|
+
if ml_dsa_algorithm?(algorithm) && %i[seed both].include?(format) && !allow_ml_dsa_seed_format
|
|
287
|
+
raise SerializationError,
|
|
288
|
+
"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)"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
profile = choice_profile(algorithm)
|
|
292
|
+
return if profile.fetch(:supported_formats).include?(format)
|
|
293
|
+
|
|
294
|
+
raise SerializationError, "Unsupported PKCS#8 private key format for #{algorithm.inspect}: #{format.inspect}"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ml_dsa_algorithm?(algorithm)
|
|
298
|
+
%i[ml_dsa_44 ml_dsa_65 ml_dsa_87].include?(algorithm)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def encode_tlv(tag, value)
|
|
302
|
+
tag.chr.b + encode_der_length(value.bytesize) + value
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def decode_tlv_value(der, expected_tag:, label:)
|
|
306
|
+
tag = der.getbyte(0)
|
|
307
|
+
unless tag == expected_tag
|
|
308
|
+
raise SerializationError, "PKCS#8 #{label} has unexpected tag: 0x#{tag.to_s(16).rjust(2, '0')}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
length, length_bytes = decode_der_length(der, 1)
|
|
312
|
+
value_offset = 1 + length_bytes
|
|
313
|
+
value_end = value_offset + length
|
|
314
|
+
raise SerializationError, "PKCS#8 #{label} length exceeds available data" if value_end > der.bytesize
|
|
315
|
+
raise SerializationError, "PKCS#8 #{label} contains trailing data" unless value_end == der.bytesize
|
|
316
|
+
|
|
317
|
+
der.byteslice(value_offset, length).b
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def encode_der_length(length)
|
|
321
|
+
raise SerializationError, "Invalid DER length" if length.negative?
|
|
322
|
+
return length.chr.b if length < 0x80
|
|
323
|
+
|
|
324
|
+
encoded = []
|
|
325
|
+
remaining = length
|
|
326
|
+
until remaining.zero?
|
|
327
|
+
encoded.unshift(remaining & 0xff)
|
|
328
|
+
remaining >>= 8
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
(0x80 | encoded.length).chr.b + encoded.pack("C*").b
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def decode_der_length(der, offset)
|
|
335
|
+
first = der.getbyte(offset)
|
|
336
|
+
raise SerializationError, "PKCS#8 DER length is missing" if first.nil?
|
|
337
|
+
|
|
338
|
+
return [first, 1] if first < 0x80
|
|
339
|
+
|
|
340
|
+
length_octets = first & 0x7f
|
|
341
|
+
raise SerializationError, "PKCS#8 DER indefinite length is not allowed" if length_octets.zero?
|
|
342
|
+
raise SerializationError, "PKCS#8 DER length is too large" if length_octets > 4
|
|
343
|
+
if offset + 1 + length_octets > der.bytesize
|
|
344
|
+
raise SerializationError, "PKCS#8 DER length exceeds available data"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
length = 0
|
|
348
|
+
length_octets.times do |i|
|
|
349
|
+
byte = der.getbyte(offset + 1 + i)
|
|
350
|
+
length = (length << 8) | byte
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
|
|
354
|
+
raise SerializationError, "PKCS#8 DER length is not minimally encoded"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
[length, 1 + length_octets]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def encode_base64(bytes)
|
|
361
|
+
[String(bytes).b].pack("m0")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def decode_base64(body)
|
|
365
|
+
compact = body.gsub(/[\r\n]/, "")
|
|
366
|
+
unless compact.match?(/\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/)
|
|
367
|
+
raise SerializationError, "Invalid PKCS#8 PEM: invalid base64"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
compact.unpack1("m0").b
|
|
371
|
+
rescue ArgumentError => e
|
|
372
|
+
raise SerializationError, e.message
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def der_from_pem(pem)
|
|
376
|
+
text = String(pem)
|
|
377
|
+
match = text.match(/\A#{Regexp.escape(PEM_BEGIN)}\r?\n(?<body>[A-Za-z0-9+\/=\r\n]+)\r?\n#{Regexp.escape(PEM_END)}[ \t\r\n]*\z/)
|
|
378
|
+
raise SerializationError, "Invalid PKCS#8 PEM: expected #{PEM_LABEL.inspect} label" unless match
|
|
379
|
+
|
|
380
|
+
body = match[:body]
|
|
381
|
+
raise SerializationError, "Invalid PKCS#8 PEM: embedded NUL in body" if body.include?("\0")
|
|
382
|
+
|
|
383
|
+
decode_base64(body)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
@@ -2,20 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module PQCrypto
|
|
4
4
|
module Serialization
|
|
5
|
-
ALGORITHM_METADATA =
|
|
6
|
-
ml_kem_768: {
|
|
7
|
-
family: :ml_kem,
|
|
8
|
-
oid: "2.25.186599352125448088867056807454444238446",
|
|
9
|
-
}.freeze,
|
|
10
|
-
ml_kem_768_x25519_xwing: {
|
|
11
|
-
family: :ml_kem_hybrid,
|
|
12
|
-
oid: "1.3.6.1.4.1.62253.25722",
|
|
13
|
-
}.freeze,
|
|
14
|
-
ml_dsa_65: {
|
|
15
|
-
family: :ml_dsa,
|
|
16
|
-
oid: "2.25.305232938483772195555080795650659207792",
|
|
17
|
-
}.freeze,
|
|
18
|
-
}.freeze
|
|
5
|
+
ALGORITHM_METADATA = AlgorithmRegistry.legacy_metadata_view.freeze
|
|
19
6
|
|
|
20
7
|
class << self
|
|
21
8
|
def algorithm_metadata(algorithm)
|
data/lib/pq_crypto/signature.rb
CHANGED
|
@@ -6,22 +6,33 @@ module PQCrypto
|
|
|
6
6
|
module Signature
|
|
7
7
|
CANONICAL_ALGORITHM = :ml_dsa_65
|
|
8
8
|
|
|
9
|
-
DETAILS =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
DETAILS = AlgorithmRegistry.details_for_family(:ml_dsa).freeze
|
|
10
|
+
|
|
11
|
+
NATIVE_DISPATCH = {
|
|
12
|
+
ml_dsa_44: {
|
|
13
|
+
keypair: :native_ml_dsa_44_keypair,
|
|
14
|
+
sign: :native_ml_dsa_44_sign,
|
|
15
|
+
verify: :native_ml_dsa_44_verify,
|
|
16
|
+
keypair_from_seed: :native_ml_dsa_44_keypair_from_seed,
|
|
17
|
+
}.freeze,
|
|
18
|
+
ml_dsa_65: {
|
|
19
|
+
keypair: :native_sign_keypair,
|
|
20
|
+
sign: :native_sign,
|
|
21
|
+
verify: :native_verify,
|
|
22
|
+
keypair_from_seed: :native_ml_dsa_keypair_from_seed,
|
|
23
|
+
}.freeze,
|
|
24
|
+
ml_dsa_87: {
|
|
25
|
+
keypair: :native_ml_dsa_87_keypair,
|
|
26
|
+
sign: :native_ml_dsa_87_sign,
|
|
27
|
+
verify: :native_ml_dsa_87_verify,
|
|
28
|
+
keypair_from_seed: :native_ml_dsa_87_keypair_from_seed,
|
|
18
29
|
}.freeze,
|
|
19
30
|
}.freeze
|
|
20
31
|
|
|
21
32
|
class << self
|
|
22
33
|
def generate(algorithm = CANONICAL_ALGORITHM)
|
|
23
|
-
resolve_algorithm!(algorithm)
|
|
24
|
-
public_key, secret_key = PQCrypto.__send__(:
|
|
34
|
+
algorithm = resolve_algorithm!(algorithm)
|
|
35
|
+
public_key, secret_key = PQCrypto.__send__(native_method_for(algorithm, :keypair))
|
|
25
36
|
Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
|
|
26
37
|
end
|
|
27
38
|
|
|
@@ -59,6 +70,26 @@ module PQCrypto
|
|
|
59
70
|
SecretKey.new(resolved_algorithm, bytes)
|
|
60
71
|
end
|
|
61
72
|
|
|
73
|
+
def public_key_from_spki_der(der, algorithm: nil)
|
|
74
|
+
resolved_algorithm, bytes = SPKI.decode_der(der)
|
|
75
|
+
validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
|
|
76
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def public_key_from_spki_pem(pem, algorithm: nil)
|
|
80
|
+
resolved_algorithm, bytes = SPKI.decode_pem(pem)
|
|
81
|
+
validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
|
|
82
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def secret_key_from_pkcs8_der(der)
|
|
86
|
+
secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def secret_key_from_pkcs8_pem(pem)
|
|
90
|
+
secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem))
|
|
91
|
+
end
|
|
92
|
+
|
|
62
93
|
def details(algorithm)
|
|
63
94
|
DETAILS.fetch(resolve_algorithm!(algorithm)).dup
|
|
64
95
|
end
|
|
@@ -75,9 +106,43 @@ module PQCrypto
|
|
|
75
106
|
raise UnsupportedAlgorithmError, "Unsupported signature algorithm: #{algorithm.inspect}"
|
|
76
107
|
end
|
|
77
108
|
|
|
109
|
+
def secret_key_from_decoded_pkcs8(algorithm, format, material)
|
|
110
|
+
algorithm = resolve_algorithm!(algorithm)
|
|
111
|
+
|
|
112
|
+
case format
|
|
113
|
+
when :expanded
|
|
114
|
+
SecretKey.new(algorithm, material)
|
|
115
|
+
when :seed
|
|
116
|
+
_public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
|
|
117
|
+
SecretKey.new(algorithm, expanded)
|
|
118
|
+
when :both
|
|
119
|
+
_seed, expanded = material
|
|
120
|
+
SecretKey.new(algorithm, expanded)
|
|
121
|
+
else
|
|
122
|
+
raise SerializationError, "Unsupported ML-DSA PKCS#8 private key format: #{format.inspect}"
|
|
123
|
+
end
|
|
124
|
+
rescue ArgumentError => e
|
|
125
|
+
raise InvalidKeyError, e.message
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def native_method_for(algorithm, operation)
|
|
129
|
+
NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_algorithm_match!(expected_algorithm, actual_algorithm)
|
|
133
|
+
expected = resolve_algorithm!(expected_algorithm)
|
|
134
|
+
return if expected == actual_algorithm
|
|
135
|
+
|
|
136
|
+
raise SerializationError,
|
|
137
|
+
"Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
|
|
138
|
+
rescue UnsupportedAlgorithmError => e
|
|
139
|
+
raise SerializationError, e.message
|
|
140
|
+
end
|
|
141
|
+
|
|
78
142
|
def _streaming_sign(secret_key, io, chunk_size, context)
|
|
143
|
+
validate_streaming_algorithm!(secret_key.algorithm)
|
|
79
144
|
validate_chunk_size!(chunk_size)
|
|
80
|
-
validate_context!(context)
|
|
145
|
+
context = validate_context!(context)
|
|
81
146
|
validate_io!(io)
|
|
82
147
|
|
|
83
148
|
sk_bytes = secret_key.__send__(:bytes_for_native)
|
|
@@ -87,7 +152,7 @@ module PQCrypto
|
|
|
87
152
|
raise InvalidKeyError, e.message
|
|
88
153
|
end
|
|
89
154
|
|
|
90
|
-
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context
|
|
155
|
+
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
|
|
91
156
|
builder_consumed = false
|
|
92
157
|
mu = nil
|
|
93
158
|
begin
|
|
@@ -103,8 +168,9 @@ module PQCrypto
|
|
|
103
168
|
end
|
|
104
169
|
|
|
105
170
|
def _streaming_verify(public_key, io, signature, chunk_size, context)
|
|
171
|
+
validate_streaming_algorithm!(public_key.algorithm)
|
|
106
172
|
validate_chunk_size!(chunk_size)
|
|
107
|
-
validate_context!(context)
|
|
173
|
+
context = validate_context!(context)
|
|
108
174
|
validate_io!(io)
|
|
109
175
|
|
|
110
176
|
pk_bytes = public_key.__send__(:bytes_for_native)
|
|
@@ -114,7 +180,7 @@ module PQCrypto
|
|
|
114
180
|
raise InvalidKeyError, e.message
|
|
115
181
|
end
|
|
116
182
|
|
|
117
|
-
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context
|
|
183
|
+
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
|
|
118
184
|
builder_consumed = false
|
|
119
185
|
mu = nil
|
|
120
186
|
sig_bytes = String(signature).b
|
|
@@ -162,6 +228,14 @@ module PQCrypto
|
|
|
162
228
|
if ctx.bytesize > 255
|
|
163
229
|
raise ArgumentError, "context must be at most 255 bytes (FIPS 204)"
|
|
164
230
|
end
|
|
231
|
+
ctx
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def validate_streaming_algorithm!(algorithm)
|
|
235
|
+
return if resolve_algorithm!(algorithm) == CANONICAL_ALGORITHM
|
|
236
|
+
|
|
237
|
+
raise UnsupportedAlgorithmError,
|
|
238
|
+
"Streaming sign_io/verify_io currently supports only #{CANONICAL_ALGORITHM.inspect}"
|
|
165
239
|
end
|
|
166
240
|
end
|
|
167
241
|
|
|
@@ -203,8 +277,16 @@ module PQCrypto
|
|
|
203
277
|
Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
204
278
|
end
|
|
205
279
|
|
|
280
|
+
def to_spki_der
|
|
281
|
+
SPKI.encode_der(@algorithm, @bytes)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def to_spki_pem
|
|
285
|
+
SPKI.encode_pem(@algorithm, @bytes)
|
|
286
|
+
end
|
|
287
|
+
|
|
206
288
|
def verify(message, signature)
|
|
207
|
-
PQCrypto.__send__(:
|
|
289
|
+
PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :verify), String(message).b, String(signature).b, @bytes)
|
|
208
290
|
rescue ArgumentError => e
|
|
209
291
|
raise InvalidKeyError, e.message
|
|
210
292
|
end
|
|
@@ -273,8 +355,32 @@ module PQCrypto
|
|
|
273
355
|
Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
274
356
|
end
|
|
275
357
|
|
|
358
|
+
def to_pkcs8_der(format: :expanded)
|
|
359
|
+
case format
|
|
360
|
+
when :expanded
|
|
361
|
+
PKCS8.encode_der(@algorithm, @bytes, format: :expanded)
|
|
362
|
+
when :seed, :both
|
|
363
|
+
raise SerializationError,
|
|
364
|
+
"ML-DSA seed/both PKCS#8 export requires original seed material; use PQCrypto::PKCS8.encode_der/encode_pem directly"
|
|
365
|
+
else
|
|
366
|
+
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def to_pkcs8_pem(format: :expanded)
|
|
371
|
+
case format
|
|
372
|
+
when :expanded
|
|
373
|
+
PKCS8.encode_pem(@algorithm, @bytes, format: :expanded)
|
|
374
|
+
when :seed, :both
|
|
375
|
+
raise SerializationError,
|
|
376
|
+
"ML-DSA seed/both PKCS#8 export requires original seed material; use PQCrypto::PKCS8.encode_der/encode_pem directly"
|
|
377
|
+
else
|
|
378
|
+
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
276
382
|
def sign(message)
|
|
277
|
-
PQCrypto.__send__(:
|
|
383
|
+
PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :sign), String(message).b, @bytes)
|
|
278
384
|
rescue ArgumentError => e
|
|
279
385
|
raise InvalidKeyError, e.message
|
|
280
386
|
end
|