pq_crypto 0.3.1 → 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 +50 -0
- data/GET_STARTED.md +374 -30
- data/README.md +59 -195
- data/SECURITY.md +101 -82
- data/ext/pqcrypto/extconf.rb +85 -9
- data/ext/pqcrypto/mldsa_api.h +71 -1
- data/ext/pqcrypto/mlkem_api.h +24 -0
- data/ext/pqcrypto/pq_externalmu.c +310 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +784 -85
- data/ext/pqcrypto/pqcrypto_secure.c +179 -72
- data/ext/pqcrypto/pqcrypto_secure.h +103 -7
- data/ext/pqcrypto/pqcrypto_version.h +7 -0
- data/ext/pqcrypto/vendor/.vendored +1 -1
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
- 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_kem/ml-kem-768/clean/Makefile +19 -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-65/clean/Makefile +19 -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 +231 -13
- data/lib/pq_crypto/spki.rb +131 -0
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +90 -19
- data/script/vendor_libs.rb +4 -0
- metadata +99 -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)
|