pq_crypto 0.1.0
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 +7 -0
- data/.github/workflows/ci.yml +37 -0
- data/CHANGELOG.md +29 -0
- data/GET_STARTED.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +135 -0
- data/SECURITY.md +57 -0
- data/ext/pqcrypto/extconf.rb +157 -0
- data/ext/pqcrypto/mldsa_api.h +51 -0
- data/ext/pqcrypto/mlkem_api.h +21 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +889 -0
- data/ext/pqcrypto/pqcrypto_secure.c +1178 -0
- data/ext/pqcrypto/pqcrypto_secure.h +135 -0
- data/ext/pqcrypto/vendor/.vendored +5 -0
- data/ext/pqcrypto/vendor/pqclean/common/aes.c +639 -0
- data/ext/pqcrypto/vendor/pqclean/common/aes.h +64 -0
- data/ext/pqcrypto/vendor/pqclean/common/compat.h +73 -0
- data/ext/pqcrypto/vendor/pqclean/common/crypto_declassify.h +7 -0
- data/ext/pqcrypto/vendor/pqclean/common/fips202.c +928 -0
- data/ext/pqcrypto/vendor/pqclean/common/fips202.h +166 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/feat.S +168 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.c +684 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.h +60 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SIMD256.c +1028 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SnP.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-unrolling.macros +198 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile.Microsoft_nmake +8 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/SIMD256-config.h +3 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/align.h +34 -0
- data/ext/pqcrypto/vendor/pqclean/common/keccak4x/brg_endian.h +142 -0
- data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.c +101 -0
- data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.h +39 -0
- data/ext/pqcrypto/vendor/pqclean/common/randombytes.c +355 -0
- data/ext/pqcrypto/vendor/pqclean/common/randombytes.h +27 -0
- data/ext/pqcrypto/vendor/pqclean/common/sha2.c +769 -0
- data/ext/pqcrypto/vendor/pqclean/common/sha2.h +173 -0
- data/ext/pqcrypto/vendor/pqclean/common/sp800-185.c +156 -0
- data/ext/pqcrypto/vendor/pqclean/common/sp800-185.h +27 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.c +83 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.c +299 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.c +188 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.c +799 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.c +92 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric.h +34 -0
- data/lib/pq_crypto/errors.rb +10 -0
- data/lib/pq_crypto/hybrid_kem.rb +106 -0
- data/lib/pq_crypto/kem.rb +199 -0
- data/lib/pq_crypto/serialization.rb +102 -0
- data/lib/pq_crypto/signature.rb +198 -0
- data/lib/pq_crypto/version.rb +5 -0
- data/lib/pq_crypto.rb +177 -0
- data/lib/pqcrypto.rb +3 -0
- data/script/vendor_libs.rb +199 -0
- metadata +195 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
4
|
+
module HybridKEM
|
|
5
|
+
CANONICAL_ALGORITHM = :ml_kem_768_x25519_hkdf_sha256
|
|
6
|
+
|
|
7
|
+
DETAILS = {
|
|
8
|
+
CANONICAL_ALGORITHM => {
|
|
9
|
+
name: CANONICAL_ALGORITHM,
|
|
10
|
+
family: Serialization.algorithm_to_family(CANONICAL_ALGORITHM),
|
|
11
|
+
oid: Serialization.algorithm_to_oid(CANONICAL_ALGORITHM),
|
|
12
|
+
public_key_bytes: HYBRID_KEM_PUBLIC_KEY_BYTES,
|
|
13
|
+
secret_key_bytes: HYBRID_KEM_SECRET_KEY_BYTES,
|
|
14
|
+
ciphertext_bytes: HYBRID_KEM_CIPHERTEXT_BYTES,
|
|
15
|
+
shared_secret_bytes: HYBRID_KEM_SHARED_SECRET_BYTES,
|
|
16
|
+
description: "Hybrid KEM: ML-KEM-768 + X25519 combined via transcript-bound HKDF-SHA256.",
|
|
17
|
+
}.freeze,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def generate(algorithm = CANONICAL_ALGORITHM)
|
|
22
|
+
algorithm = resolve_algorithm!(algorithm)
|
|
23
|
+
public_key, secret_key = PQCrypto.__send__(:native_hybrid_kem_keypair)
|
|
24
|
+
Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def public_key_from_bytes(algorithm, bytes)
|
|
28
|
+
PublicKey.new(resolve_algorithm!(algorithm), bytes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def secret_key_from_bytes(algorithm, bytes)
|
|
32
|
+
SecretKey.new(resolve_algorithm!(algorithm), bytes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def public_key_from_pqc_container_der(der, algorithm = nil)
|
|
36
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
|
|
37
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def public_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
41
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
|
|
42
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def secret_key_from_pqc_container_der(der, algorithm = nil)
|
|
46
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
|
|
47
|
+
SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def secret_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
51
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
|
|
52
|
+
SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def details(algorithm)
|
|
56
|
+
DETAILS.fetch(resolve_algorithm!(algorithm)).dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def supported
|
|
60
|
+
DETAILS.keys.dup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def resolve_algorithm!(algorithm)
|
|
66
|
+
return algorithm if DETAILS.key?(algorithm)
|
|
67
|
+
|
|
68
|
+
raise UnsupportedAlgorithmError, "Unsupported hybrid KEM algorithm: #{algorithm.inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class Keypair < KEM::Keypair; end
|
|
73
|
+
class EncapsulationResult < KEM::EncapsulationResult; end
|
|
74
|
+
|
|
75
|
+
class PublicKey < KEM::PublicKey
|
|
76
|
+
def encapsulate
|
|
77
|
+
ciphertext, shared_secret = PQCrypto.__send__(:native_hybrid_kem_encapsulate, @bytes)
|
|
78
|
+
EncapsulationResult.new(ciphertext, shared_secret)
|
|
79
|
+
rescue ArgumentError => e
|
|
80
|
+
raise InvalidKeyError, e.message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def validate_length!
|
|
86
|
+
expected = HybridKEM.details(@algorithm).fetch(:public_key_bytes)
|
|
87
|
+
raise InvalidKeyError, "Invalid hybrid KEM public key length" unless @bytes.bytesize == expected
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class SecretKey < KEM::SecretKey
|
|
92
|
+
def decapsulate(ciphertext)
|
|
93
|
+
PQCrypto.__send__(:native_hybrid_kem_decapsulate, String(ciphertext).b, @bytes)
|
|
94
|
+
rescue ArgumentError => e
|
|
95
|
+
raise InvalidCiphertextError, e.message
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def validate_length!
|
|
101
|
+
expected = HybridKEM.details(@algorithm).fetch(:secret_key_bytes)
|
|
102
|
+
raise InvalidKeyError, "Invalid hybrid KEM secret key length" unless @bytes.bytesize == expected
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
4
|
+
module KEM
|
|
5
|
+
CANONICAL_ALGORITHM = :ml_kem_768
|
|
6
|
+
|
|
7
|
+
DETAILS = {
|
|
8
|
+
CANONICAL_ALGORITHM => {
|
|
9
|
+
name: CANONICAL_ALGORITHM,
|
|
10
|
+
family: Serialization.algorithm_to_family(CANONICAL_ALGORITHM),
|
|
11
|
+
oid: Serialization.algorithm_to_oid(CANONICAL_ALGORITHM),
|
|
12
|
+
public_key_bytes: ML_KEM_PUBLIC_KEY_BYTES,
|
|
13
|
+
secret_key_bytes: ML_KEM_SECRET_KEY_BYTES,
|
|
14
|
+
ciphertext_bytes: ML_KEM_CIPHERTEXT_BYTES,
|
|
15
|
+
shared_secret_bytes: ML_KEM_SHARED_SECRET_BYTES,
|
|
16
|
+
description: "Pure ML-KEM-768 primitive (FIPS 203).",
|
|
17
|
+
}.freeze,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def generate(algorithm = CANONICAL_ALGORITHM)
|
|
22
|
+
algorithm = resolve_algorithm!(algorithm)
|
|
23
|
+
public_key, secret_key = PQCrypto.__send__(:native_ml_kem_keypair)
|
|
24
|
+
Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def public_key_from_bytes(algorithm, bytes)
|
|
28
|
+
PublicKey.new(resolve_algorithm!(algorithm), bytes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def secret_key_from_bytes(algorithm, bytes)
|
|
32
|
+
SecretKey.new(resolve_algorithm!(algorithm), bytes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def public_key_from_pqc_container_der(der, algorithm = nil)
|
|
36
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
|
|
37
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def public_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
41
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
|
|
42
|
+
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def secret_key_from_pqc_container_der(der, algorithm = nil)
|
|
46
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
|
|
47
|
+
SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def secret_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
51
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
|
|
52
|
+
SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def details(algorithm)
|
|
56
|
+
DETAILS.fetch(resolve_algorithm!(algorithm)).dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def supported
|
|
60
|
+
DETAILS.keys.dup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def resolve_algorithm!(algorithm)
|
|
66
|
+
return algorithm if DETAILS.key?(algorithm)
|
|
67
|
+
|
|
68
|
+
raise UnsupportedAlgorithmError, "Unsupported KEM algorithm: #{algorithm.inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class Keypair
|
|
73
|
+
attr_reader :public_key, :secret_key
|
|
74
|
+
|
|
75
|
+
def initialize(public_key, secret_key)
|
|
76
|
+
@public_key = public_key
|
|
77
|
+
@secret_key = secret_key
|
|
78
|
+
|
|
79
|
+
unless @public_key.algorithm == @secret_key.algorithm
|
|
80
|
+
raise InvalidKeyError, "KEM keypair algorithms do not match"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def algorithm
|
|
85
|
+
@public_key.algorithm
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class PublicKey
|
|
90
|
+
attr_reader :algorithm
|
|
91
|
+
|
|
92
|
+
def initialize(algorithm, bytes)
|
|
93
|
+
@algorithm = algorithm
|
|
94
|
+
@bytes = String(bytes).b
|
|
95
|
+
validate_length!
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_bytes
|
|
99
|
+
@bytes.dup
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def to_pqc_container_der
|
|
103
|
+
Serialization.public_key_to_pqc_container_der(@algorithm, @bytes)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_pqc_container_pem
|
|
107
|
+
Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def encapsulate
|
|
111
|
+
ciphertext, shared_secret = PQCrypto.__send__(:native_ml_kem_encapsulate, @bytes)
|
|
112
|
+
EncapsulationResult.new(ciphertext, shared_secret)
|
|
113
|
+
rescue ArgumentError => e
|
|
114
|
+
raise InvalidKeyError, e.message
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def encapsulate_to_bytes
|
|
118
|
+
result = encapsulate
|
|
119
|
+
[result.ciphertext, result.shared_secret]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def ==(other)
|
|
123
|
+
other.is_a?(PublicKey) && other.algorithm == algorithm && other.to_bytes == @bytes
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
alias eql? ==
|
|
127
|
+
|
|
128
|
+
def hash
|
|
129
|
+
[self.class, algorithm, @bytes].hash
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def validate_length!
|
|
135
|
+
expected = KEM.details(@algorithm).fetch(:public_key_bytes)
|
|
136
|
+
raise InvalidKeyError, "Invalid KEM public key length" unless @bytes.bytesize == expected
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class SecretKey
|
|
141
|
+
attr_reader :algorithm
|
|
142
|
+
|
|
143
|
+
def initialize(algorithm, bytes)
|
|
144
|
+
@algorithm = algorithm
|
|
145
|
+
@bytes = String(bytes).b
|
|
146
|
+
validate_length!
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_bytes
|
|
150
|
+
@bytes.dup
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def to_pqc_container_der
|
|
154
|
+
Serialization.secret_key_to_pqc_container_der(@algorithm, @bytes)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_pqc_container_pem
|
|
158
|
+
Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def decapsulate(ciphertext)
|
|
162
|
+
PQCrypto.__send__(:native_ml_kem_decapsulate, String(ciphertext).b, @bytes)
|
|
163
|
+
rescue ArgumentError => e
|
|
164
|
+
raise InvalidCiphertextError, e.message
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def wipe!
|
|
168
|
+
PQCrypto.secure_wipe(@bytes)
|
|
169
|
+
self
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ==(other)
|
|
173
|
+
other.is_a?(SecretKey) && other.algorithm == algorithm && other.to_bytes == @bytes
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
alias eql? ==
|
|
177
|
+
|
|
178
|
+
def hash
|
|
179
|
+
[self.class, algorithm, @bytes].hash
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def validate_length!
|
|
185
|
+
expected = KEM.details(@algorithm).fetch(:secret_key_bytes)
|
|
186
|
+
raise InvalidKeyError, "Invalid KEM secret key length" unless @bytes.bytesize == expected
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class EncapsulationResult
|
|
191
|
+
attr_reader :ciphertext, :shared_secret
|
|
192
|
+
|
|
193
|
+
def initialize(ciphertext, shared_secret)
|
|
194
|
+
@ciphertext = String(ciphertext).b
|
|
195
|
+
@shared_secret = String(shared_secret).b
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
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_hkdf_sha256: {
|
|
11
|
+
family: :ml_kem_hybrid,
|
|
12
|
+
oid: "2.25.260242945110721168101139140490528778800",
|
|
13
|
+
}.freeze,
|
|
14
|
+
ml_dsa_65: {
|
|
15
|
+
family: :ml_dsa,
|
|
16
|
+
oid: "2.25.305232938483772195555080795650659207792",
|
|
17
|
+
}.freeze,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def algorithm_metadata(algorithm)
|
|
22
|
+
metadata = ALGORITHM_METADATA[algorithm]
|
|
23
|
+
raise SerializationError, "Unsupported serialization algorithm: #{algorithm.inspect}" unless metadata
|
|
24
|
+
|
|
25
|
+
metadata
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def algorithm_to_oid(algorithm)
|
|
29
|
+
algorithm_metadata(algorithm).fetch(:oid)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def algorithm_to_family(algorithm)
|
|
33
|
+
algorithm_metadata(algorithm).fetch(:family)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def public_key_to_pqc_container_der(algorithm, bytes)
|
|
37
|
+
PQCrypto.__send__(:native_public_key_to_pqc_container_der, String(algorithm), String(bytes).b)
|
|
38
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
39
|
+
raise SerializationError, e.message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def public_key_to_pqc_container_pem(algorithm, bytes)
|
|
43
|
+
PQCrypto.__send__(:native_public_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
|
|
44
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
45
|
+
raise SerializationError, e.message
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def secret_key_to_pqc_container_der(algorithm, bytes)
|
|
49
|
+
PQCrypto.__send__(:native_secret_key_to_pqc_container_der, String(algorithm), String(bytes).b)
|
|
50
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
51
|
+
raise SerializationError, e.message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def secret_key_to_pqc_container_pem(algorithm, bytes)
|
|
55
|
+
PQCrypto.__send__(:native_secret_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
|
|
56
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
57
|
+
raise SerializationError, e.message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def public_key_from_pqc_container_der(expected_algorithm, der)
|
|
61
|
+
algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_der, String(der).b)
|
|
62
|
+
validate_algorithm_expectation!(expected_algorithm, algorithm)
|
|
63
|
+
[algorithm, bytes]
|
|
64
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
65
|
+
raise SerializationError, e.message
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def public_key_from_pqc_container_pem(expected_algorithm, pem)
|
|
69
|
+
algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_pem, String(pem).b)
|
|
70
|
+
validate_algorithm_expectation!(expected_algorithm, algorithm)
|
|
71
|
+
[algorithm, bytes]
|
|
72
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
73
|
+
raise SerializationError, e.message
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def secret_key_from_pqc_container_der(expected_algorithm, der)
|
|
77
|
+
algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_der, String(der).b)
|
|
78
|
+
validate_algorithm_expectation!(expected_algorithm, algorithm)
|
|
79
|
+
[algorithm, bytes]
|
|
80
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
81
|
+
raise SerializationError, e.message
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def secret_key_from_pqc_container_pem(expected_algorithm, pem)
|
|
85
|
+
algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_pem, String(pem).b)
|
|
86
|
+
validate_algorithm_expectation!(expected_algorithm, algorithm)
|
|
87
|
+
[algorithm, bytes]
|
|
88
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
89
|
+
raise SerializationError, e.message
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validate_algorithm_expectation!(expected_algorithm, actual_algorithm)
|
|
95
|
+
return if expected_algorithm.nil? || expected_algorithm == actual_algorithm
|
|
96
|
+
|
|
97
|
+
raise SerializationError,
|
|
98
|
+
"Expected #{expected_algorithm.inspect}, got #{actual_algorithm.inspect} (serialized key algorithm mismatch)"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
4
|
+
module Signature
|
|
5
|
+
CANONICAL_ALGORITHM = :ml_dsa_65
|
|
6
|
+
|
|
7
|
+
DETAILS = {
|
|
8
|
+
CANONICAL_ALGORITHM => {
|
|
9
|
+
name: CANONICAL_ALGORITHM,
|
|
10
|
+
family: Serialization.algorithm_to_family(CANONICAL_ALGORITHM),
|
|
11
|
+
oid: Serialization.algorithm_to_oid(CANONICAL_ALGORITHM),
|
|
12
|
+
public_key_bytes: SIGN_PUBLIC_KEY_BYTES,
|
|
13
|
+
secret_key_bytes: SIGN_SECRET_KEY_BYTES,
|
|
14
|
+
signature_bytes: SIGN_BYTES,
|
|
15
|
+
description: "ML-DSA-65 signature primitive (FIPS 204).",
|
|
16
|
+
}.freeze,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def generate(algorithm = CANONICAL_ALGORITHM)
|
|
21
|
+
validate_algorithm!(algorithm)
|
|
22
|
+
public_key, secret_key = PQCrypto.__send__(:native_sign_keypair)
|
|
23
|
+
Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def public_key_from_bytes(algorithm, bytes)
|
|
27
|
+
validate_algorithm!(algorithm)
|
|
28
|
+
PublicKey.new(algorithm, bytes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def secret_key_from_bytes(algorithm, bytes)
|
|
32
|
+
validate_algorithm!(algorithm)
|
|
33
|
+
SecretKey.new(algorithm, bytes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def public_key_from_pqc_container_der(der, algorithm = nil)
|
|
37
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
|
|
38
|
+
validate_algorithm!(resolved_algorithm)
|
|
39
|
+
PublicKey.new(resolved_algorithm, bytes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def public_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
43
|
+
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
|
|
44
|
+
validate_algorithm!(resolved_algorithm)
|
|
45
|
+
PublicKey.new(resolved_algorithm, bytes)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def secret_key_from_pqc_container_der(der, algorithm = nil)
|
|
49
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
|
|
50
|
+
validate_algorithm!(resolved_algorithm)
|
|
51
|
+
SecretKey.new(resolved_algorithm, bytes)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def secret_key_from_pqc_container_pem(pem, algorithm = nil)
|
|
55
|
+
resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
|
|
56
|
+
validate_algorithm!(resolved_algorithm)
|
|
57
|
+
SecretKey.new(resolved_algorithm, bytes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def details(algorithm)
|
|
61
|
+
DETAILS.fetch(validate_algorithm!(algorithm)).dup
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def supported
|
|
65
|
+
DETAILS.keys.dup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def validate_algorithm!(algorithm)
|
|
71
|
+
return algorithm if DETAILS.key?(algorithm)
|
|
72
|
+
|
|
73
|
+
raise UnsupportedAlgorithmError, "Unsupported signature algorithm: #{algorithm.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Keypair
|
|
78
|
+
attr_reader :public_key, :secret_key
|
|
79
|
+
|
|
80
|
+
def initialize(public_key, secret_key)
|
|
81
|
+
@public_key = public_key
|
|
82
|
+
@secret_key = secret_key
|
|
83
|
+
|
|
84
|
+
unless @public_key.algorithm == @secret_key.algorithm
|
|
85
|
+
raise InvalidKeyError, "Signature keypair algorithms do not match"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def algorithm
|
|
90
|
+
@public_key.algorithm
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class PublicKey
|
|
95
|
+
attr_reader :algorithm
|
|
96
|
+
|
|
97
|
+
def initialize(algorithm, bytes)
|
|
98
|
+
@algorithm = algorithm
|
|
99
|
+
@bytes = String(bytes).b
|
|
100
|
+
validate_length!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_bytes
|
|
104
|
+
@bytes.dup
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_pqc_container_der
|
|
108
|
+
Serialization.public_key_to_pqc_container_der(@algorithm, @bytes)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_pqc_container_pem
|
|
112
|
+
Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def verify(message, signature)
|
|
116
|
+
PQCrypto.__send__(:native_verify, String(message).b, String(signature).b, @bytes)
|
|
117
|
+
rescue PQCrypto::VerificationError
|
|
118
|
+
false
|
|
119
|
+
rescue ArgumentError => e
|
|
120
|
+
raise InvalidKeyError, e.message
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def verify!(message, signature)
|
|
124
|
+
ok = verify(message, signature)
|
|
125
|
+
raise PQCrypto::VerificationError, "Verification failed" unless ok
|
|
126
|
+
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ==(other)
|
|
131
|
+
other.is_a?(PublicKey) && other.algorithm == algorithm && other.to_bytes == @bytes
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
alias eql? ==
|
|
135
|
+
|
|
136
|
+
def hash
|
|
137
|
+
[self.class, algorithm, @bytes].hash
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def validate_length!
|
|
143
|
+
expected = Signature.details(@algorithm).fetch(:public_key_bytes)
|
|
144
|
+
raise InvalidKeyError, "Invalid signature public key length" unless @bytes.bytesize == expected
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class SecretKey
|
|
149
|
+
attr_reader :algorithm
|
|
150
|
+
|
|
151
|
+
def initialize(algorithm, bytes)
|
|
152
|
+
@algorithm = algorithm
|
|
153
|
+
@bytes = String(bytes).b
|
|
154
|
+
validate_length!
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_bytes
|
|
158
|
+
@bytes.dup
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_pqc_container_der
|
|
162
|
+
Serialization.secret_key_to_pqc_container_der(@algorithm, @bytes)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def to_pqc_container_pem
|
|
166
|
+
Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def sign(message)
|
|
170
|
+
PQCrypto.__send__(:native_sign, String(message).b, @bytes)
|
|
171
|
+
rescue ArgumentError => e
|
|
172
|
+
raise InvalidKeyError, e.message
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def wipe!
|
|
176
|
+
PQCrypto.secure_wipe(@bytes)
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def ==(other)
|
|
181
|
+
other.is_a?(SecretKey) && other.algorithm == algorithm && other.to_bytes == @bytes
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
alias eql? ==
|
|
185
|
+
|
|
186
|
+
def hash
|
|
187
|
+
[self.class, algorithm, @bytes].hash
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def validate_length!
|
|
193
|
+
expected = Signature.details(@algorithm).fetch(:secret_key_bytes)
|
|
194
|
+
raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|