pq_crypto-jwt 0.1.2 → 0.2.1
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/CHANGELOG.md +62 -0
- data/README.md +75 -19
- data/lib/pq_crypto/jwt/errors.rb +1 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_44.rb +2 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_87.rb +2 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_streaming.rb +108 -55
- data/lib/pq_crypto/jwt/jwa.rb +24 -55
- data/lib/pq_crypto/jwt/jwk/akp.rb +39 -43
- data/lib/pq_crypto/jwt/jwk.rb +118 -62
- data/lib/pq_crypto/jwt/jwks.rb +24 -23
- data/lib/pq_crypto/jwt/keys.rb +42 -56
- data/lib/pq_crypto/jwt/version.rb +1 -1
- data/lib/pq_crypto/jwt.rb +10 -36
- metadata +12 -10
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_44.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_65.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_87.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms.rb +0 -3
|
@@ -6,61 +6,50 @@ module JWT
|
|
|
6
6
|
module JWK
|
|
7
7
|
class AKP < KeyBase
|
|
8
8
|
KTY = "AKP".freeze
|
|
9
|
-
KTYS = [
|
|
10
|
-
KTY,
|
|
11
|
-
PQCrypto::Signature::PublicKey,
|
|
12
|
-
JWT::JWK::AKP,
|
|
13
|
-
].freeze
|
|
9
|
+
KTYS = [KTY, PQCrypto::Signature::PublicKey, JWT::JWK::AKP].freeze
|
|
14
10
|
AKP_KEY_ELEMENTS = %i[kty alg pub priv].freeze
|
|
11
|
+
PRIVATE_EXPORT_OPTIONS = %i[private include_private].freeze
|
|
15
12
|
|
|
16
13
|
class NullKidGenerator
|
|
17
14
|
def initialize(_jwk); end
|
|
18
|
-
|
|
19
15
|
def generate = nil
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
def initialize(key, params = nil, options = {})
|
|
23
|
-
params
|
|
24
|
-
options ||= {}
|
|
25
|
-
options = { kid_generator: NullKidGenerator }.merge(options)
|
|
26
|
-
params = { kid: params } if params.is_a?(String)
|
|
27
|
-
key_params = extract_key_params(key)
|
|
19
|
+
params = params.is_a?(String) ? { kid: params } : (params || {})
|
|
28
20
|
params = params.transform_keys(&:to_sym)
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
key_params = extract_key_params(key)
|
|
22
|
+
@checked_public_key, @checked_secret_key = check_jwk_params!(key_params, params)
|
|
23
|
+
super({ kid_generator: NullKidGenerator }.merge(options || {}), key_params.merge(params))
|
|
31
24
|
end
|
|
32
25
|
|
|
33
|
-
def private?
|
|
34
|
-
|
|
35
|
-
end
|
|
26
|
+
def private? = parameters.key?(:priv) && !parameters[:priv].nil?
|
|
27
|
+
def verify_key = public_key
|
|
36
28
|
|
|
37
29
|
def public_key
|
|
38
|
-
@public_key ||= PQCrypto::JWT::JWK.public_key_from_jwk(string_export)
|
|
30
|
+
@public_key ||= @checked_public_key || PQCrypto::JWT::JWK.public_key_from_jwk(string_export)
|
|
39
31
|
end
|
|
40
32
|
|
|
41
|
-
def signing_key
|
|
42
|
-
public_key
|
|
43
|
-
end
|
|
33
|
+
def signing_key = private? ? secret_key : public_key
|
|
44
34
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
end
|
|
35
|
+
def secret_key
|
|
36
|
+
raise JWT::JWKError, "AKP JWK does not contain private material" unless private?
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
parameters.clone.tap { |exported| exported.delete(:priv) }
|
|
38
|
+
@secret_key ||= @checked_secret_key || PQCrypto::JWT::JWK.secret_key_from_jwk(string_export(include_private: true))
|
|
51
39
|
end
|
|
52
40
|
|
|
53
|
-
def
|
|
54
|
-
|
|
41
|
+
def export(options = {})
|
|
42
|
+
include_private = PRIVATE_EXPORT_OPTIONS.any? { |key| (options || {})[key] }
|
|
43
|
+
parameters.clone.tap { |exported| exported.delete(:priv) unless include_private }
|
|
55
44
|
end
|
|
56
45
|
|
|
57
|
-
def
|
|
58
|
-
|
|
46
|
+
def members
|
|
47
|
+
keys = private? ? %i[alg kty pub priv] : %i[alg kty pub]
|
|
48
|
+
keys.each_with_object({}) { |key, out| out[key] = self[key] }
|
|
59
49
|
end
|
|
60
50
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
end
|
|
51
|
+
def key_digest = PQCrypto::JWT::JWK.thumbprint(string_export)
|
|
52
|
+
def jwa = PQCrypto::JWT.algorithm_for(self[:alg]) || super
|
|
64
53
|
|
|
65
54
|
def []=(key, value)
|
|
66
55
|
raise ArgumentError, "cannot overwrite cryptographic key attributes" if AKP_KEY_ELEMENTS.include?(key.to_sym)
|
|
@@ -70,23 +59,25 @@ module JWT
|
|
|
70
59
|
|
|
71
60
|
private
|
|
72
61
|
|
|
73
|
-
def string_export
|
|
74
|
-
export.transform_keys(&:to_s)
|
|
62
|
+
def string_export(include_private: false)
|
|
63
|
+
export(include_private: include_private).transform_keys(&:to_s)
|
|
75
64
|
end
|
|
76
65
|
|
|
77
66
|
def extract_key_params(key)
|
|
78
67
|
case key
|
|
79
|
-
when JWT::JWK::AKP
|
|
80
|
-
|
|
81
|
-
when Hash
|
|
82
|
-
key.transform_keys(&:to_sym)
|
|
68
|
+
when JWT::JWK::AKP then key.export(include_private: key.private?)
|
|
69
|
+
when Hash then key.transform_keys(&:to_sym)
|
|
83
70
|
when PQCrypto::Signature::PublicKey
|
|
84
71
|
PQCrypto::JWT::JWK.from_public_key(key).transform_keys(&:to_sym)
|
|
85
72
|
when PQCrypto::Signature::Keypair, PQCrypto::Signature::SecretKey
|
|
86
|
-
raise JWT::JWKError,
|
|
73
|
+
raise JWT::JWKError,
|
|
74
|
+
"AKP private JWK export from SecretKey requires public seed-export APIs in pq_crypto; " \
|
|
75
|
+
"use PQCrypto::JWT::JWK.from_seed(seed, alg:, public_key:) instead"
|
|
87
76
|
else
|
|
88
|
-
raise ArgumentError, "key must be
|
|
77
|
+
raise ArgumentError, "key must be an AKP JWK Hash or PQCrypto::Signature::PublicKey"
|
|
89
78
|
end
|
|
79
|
+
rescue PQCrypto::JWT::Error => e
|
|
80
|
+
raise JWT::JWKError, e.message
|
|
90
81
|
end
|
|
91
82
|
|
|
92
83
|
def check_jwk_params!(key_params, params)
|
|
@@ -94,11 +85,16 @@ module JWT
|
|
|
94
85
|
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
|
|
95
86
|
raise JWT::JWKError, "AKP JWK alg is required" unless key_params[:alg]
|
|
96
87
|
raise JWT::JWKError, "AKP JWK pub is required" unless key_params[:pub]
|
|
97
|
-
raise JWT::JWKError, "AKP private JWK import is not supported in the first release" if key_params[:priv]
|
|
98
88
|
raise JWT::JWKError, "Unsupported AKP JWK alg: #{key_params[:alg].inspect}" unless PQCrypto::JWT.algorithm_for(key_params[:alg])
|
|
89
|
+
|
|
90
|
+
parsed = key_params.transform_keys(&:to_s)
|
|
91
|
+
[
|
|
92
|
+
PQCrypto::JWT::JWK.public_key_from_jwk(parsed),
|
|
93
|
+
key_params[:priv] ? PQCrypto::JWT::JWK.secret_key_from_jwk(parsed) : nil
|
|
94
|
+
]
|
|
95
|
+
rescue PQCrypto::JWT::Error => e
|
|
96
|
+
raise JWT::JWKError, e.message
|
|
99
97
|
end
|
|
100
98
|
end
|
|
101
99
|
end
|
|
102
100
|
end
|
|
103
|
-
|
|
104
|
-
JWT::JWK.classes.delete(JWT::JWK::AKP)
|
data/lib/pq_crypto/jwt/jwk.rb
CHANGED
|
@@ -10,39 +10,72 @@ module PQCrypto
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
KTY = "AKP".freeze
|
|
13
|
+
SEED_BYTES = defined?(PQCrypto::PKCS8::ML_DSA_SEED_BYTES) ? PQCrypto::PKCS8::ML_DSA_SEED_BYTES : 32
|
|
13
14
|
|
|
14
|
-
def from_public_key(public_key, kid: nil)
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
def from_public_key(public_key, kid: nil, use: nil, key_ops: nil)
|
|
16
|
+
validate_key!(public_key, PQCrypto::Signature::PublicKey)
|
|
17
|
+
public_jwk(public_key.algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops).freeze
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
def from_seed(seed, alg:, kid: nil, public_key: nil, use: nil, key_ops: nil, verify_public: false)
|
|
21
|
+
algorithm = pq_algorithm_from_jose!(alg)
|
|
22
|
+
seed_bytes = validate_seed!(seed, algorithm)
|
|
23
|
+
derived = derive_public_key(algorithm, seed_bytes) if public_key.nil? || verify_public
|
|
24
|
+
public_key ||= derived
|
|
25
|
+
unless public_key
|
|
26
|
+
raise PQCrypto::JWT::UnsupportedFeature,
|
|
27
|
+
"AKP JWK export from seed requires pq_crypto public_key_from_seed/keypair_from_seed or public_key:"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
validate_key!(public_key, PQCrypto::Signature::PublicKey)
|
|
31
|
+
unless public_key.algorithm == algorithm
|
|
32
|
+
raise PQCrypto::JWT::KeyTypeError, "public_key algorithm mismatch: expected #{algorithm.inspect}, got #{public_key.algorithm.inspect}"
|
|
33
|
+
end
|
|
34
|
+
check_seed_matches_public!(public_key, derived) if verify_public
|
|
35
|
+
|
|
36
|
+
public_jwk(algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops)
|
|
37
|
+
.merge!("priv" => base64url(seed_bytes))
|
|
38
|
+
.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def from_secret_key(secret_key, kid: nil, public_key: nil, use: nil, key_ops: nil)
|
|
42
|
+
if secret_key.is_a?(PQCrypto::Signature::Keypair)
|
|
43
|
+
public_key ||= secret_key.public_key
|
|
44
|
+
secret_key = secret_key.secret_key
|
|
45
|
+
end
|
|
46
|
+
validate_key!(secret_key, PQCrypto::Signature::SecretKey)
|
|
47
|
+
|
|
48
|
+
from_seed(seed_from_secret_key!(secret_key),
|
|
49
|
+
alg: jose_alg_for!(secret_key.algorithm),
|
|
50
|
+
kid: kid, public_key: public_key, use: use, key_ops: key_ops)
|
|
22
51
|
end
|
|
23
52
|
|
|
24
53
|
def public_key_from_jwk(hash)
|
|
25
54
|
jwk = normalize_hash!(hash)
|
|
26
|
-
reject_private_material!(jwk)
|
|
27
55
|
algorithm = algorithm_from_jwk!(jwk)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
rescue ArgumentError => e
|
|
56
|
+
PQCrypto::Signature.public_key_from_bytes(algorithm, decode_field!(jwk, "pub", algorithm))
|
|
57
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
31
58
|
raise PQCrypto::JWT::Error, e.message
|
|
32
59
|
end
|
|
33
60
|
|
|
34
|
-
def secret_key_from_jwk(
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
def secret_key_from_jwk(hash, verify_public: false)
|
|
62
|
+
jwk = normalize_hash!(hash)
|
|
63
|
+
algorithm = algorithm_from_jwk!(jwk)
|
|
64
|
+
decode_field!(jwk, "pub", algorithm)
|
|
65
|
+
seed = validate_seed!(base64url_decode(jwk.fetch("priv") { raise PQCrypto::JWT::Error, "JWK priv is required" }), algorithm)
|
|
66
|
+
check_seed_matches_public!(public_key_from_jwk(jwk), derive_public_key(algorithm, seed)) if verify_public
|
|
67
|
+
|
|
68
|
+
PQCrypto::Signature.secret_key_from_seed(algorithm, seed)
|
|
69
|
+
rescue ArgumentError, PQCrypto::Error => e
|
|
70
|
+
raise PQCrypto::JWT::Error, e.message
|
|
37
71
|
end
|
|
38
72
|
|
|
39
73
|
def thumbprint(jwk_hash)
|
|
40
74
|
jwk = normalize_hash!(jwk_hash)
|
|
41
|
-
reject_private_material!(jwk)
|
|
42
75
|
algorithm_from_jwk!(jwk)
|
|
43
76
|
raise PQCrypto::JWT::Error, "JWK pub is required" unless jwk.key?("pub")
|
|
44
77
|
|
|
45
|
-
canonical = JSON.generate(
|
|
78
|
+
canonical = JSON.generate("alg" => jwk.fetch("alg"), "kty" => KTY, "pub" => jwk.fetch("pub"))
|
|
46
79
|
base64url(Digest::SHA256.digest(canonical.b))
|
|
47
80
|
end
|
|
48
81
|
|
|
@@ -51,90 +84,113 @@ module PQCrypto
|
|
|
51
84
|
end
|
|
52
85
|
|
|
53
86
|
def base64url_decode(value)
|
|
54
|
-
|
|
87
|
+
raw = String(value)
|
|
88
|
+
Base64.urlsafe_decode64(raw + ("=" * ((4 - raw.bytesize % 4) % 4)))
|
|
55
89
|
rescue ArgumentError => e
|
|
56
90
|
raise PQCrypto::JWT::Error, "Invalid base64url value: #{e.message}"
|
|
57
91
|
end
|
|
58
92
|
|
|
59
93
|
def normalize_hash!(hash)
|
|
60
|
-
unless hash.respond_to?(:to_hash)
|
|
61
|
-
raise PQCrypto::JWT::Error, "JWK must be a Hash-like object"
|
|
62
|
-
end
|
|
94
|
+
raise PQCrypto::JWT::Error, "JWK must be a Hash-like object" unless hash.respond_to?(:to_hash)
|
|
63
95
|
|
|
64
|
-
hash.to_hash.each_with_object({})
|
|
65
|
-
normalized[String(key)] = value
|
|
66
|
-
end
|
|
96
|
+
hash.to_hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
|
|
67
97
|
end
|
|
68
98
|
|
|
69
|
-
def
|
|
70
|
-
|
|
99
|
+
def algorithm_from_jwk!(jwk)
|
|
100
|
+
raise PQCrypto::JWT::Error, "Unsupported JWK kty: #{jwk['kty'].inspect}" unless jwk["kty"] == KTY
|
|
71
101
|
|
|
72
|
-
|
|
73
|
-
"Private AKP JWK material is not supported in the first release"
|
|
102
|
+
pq_algorithm_from_jose!(jwk["alg"])
|
|
74
103
|
end
|
|
75
|
-
private_class_method :reject_private_material!
|
|
76
104
|
|
|
77
|
-
def
|
|
78
|
-
unless jwk.fetch("kty", nil) == KTY
|
|
79
|
-
raise PQCrypto::JWT::Error, "Unsupported JWK kty: #{jwk.fetch('kty', nil).inspect}"
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
alg = jwk.fetch("alg", nil)
|
|
105
|
+
def pq_algorithm_from_jose!(alg)
|
|
83
106
|
algorithm = PQCrypto::JWT.algorithm_for(alg)
|
|
84
|
-
raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm
|
|
85
|
-
raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm.key_kind == :signature
|
|
107
|
+
raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm&.key_kind == :signature
|
|
86
108
|
|
|
87
109
|
algorithm.pq_crypto_algorithm
|
|
88
110
|
end
|
|
111
|
+
private_class_method :pq_algorithm_from_jose!
|
|
89
112
|
|
|
90
|
-
def
|
|
91
|
-
match = PQCrypto::JWT.signing_algorithms.find { |candidate| candidate.pq_crypto_algorithm ==
|
|
92
|
-
raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported pq_crypto signature algorithm: #{
|
|
113
|
+
def jose_alg_for!(pq_algorithm)
|
|
114
|
+
match = PQCrypto::JWT.signing_algorithms.find { |candidate| candidate.pq_crypto_algorithm == pq_algorithm }
|
|
115
|
+
raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported pq_crypto signature algorithm: #{pq_algorithm.inspect}" unless match
|
|
93
116
|
|
|
94
117
|
match.alg
|
|
95
118
|
end
|
|
119
|
+
private_class_method :jose_alg_for!
|
|
96
120
|
|
|
97
|
-
def
|
|
98
|
-
jwk = {
|
|
99
|
-
"kty" => KTY,
|
|
100
|
-
"alg" => alg_for_algorithm!(algorithm),
|
|
101
|
-
"pub" => base64url(public_bytes),
|
|
102
|
-
}
|
|
121
|
+
def public_jwk(pq_algorithm, public_bytes, kid:, use:, key_ops:)
|
|
122
|
+
jwk = { "kty" => KTY, "alg" => jose_alg_for!(pq_algorithm), "pub" => base64url(public_bytes) }
|
|
103
123
|
jwk["kid"] = String(kid) unless kid.nil?
|
|
124
|
+
jwk["use"] = String(use) unless use.nil?
|
|
125
|
+
jwk["key_ops"] = Array(key_ops).map(&:to_s) unless key_ops.nil?
|
|
104
126
|
jwk
|
|
105
127
|
end
|
|
106
|
-
private_class_method :
|
|
128
|
+
private_class_method :public_jwk
|
|
107
129
|
|
|
108
|
-
def
|
|
130
|
+
def decode_field!(jwk, field, algorithm)
|
|
109
131
|
raise PQCrypto::JWT::Error, "JWK #{field} is required" unless jwk.key?(field)
|
|
110
132
|
|
|
111
133
|
decoded = base64url_decode(jwk.fetch(field))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
unless decoded.bytesize == expected
|
|
115
|
-
raise PQCrypto::JWT::Error,
|
|
116
|
-
"Invalid #{field} length for #{algorithm.inspect}: expected #{expected}, got #{decoded.bytesize}"
|
|
117
|
-
end
|
|
134
|
+
expected = PQCrypto::Signature.details(algorithm).fetch(:public_key_bytes)
|
|
135
|
+
raise PQCrypto::JWT::Error, "Invalid #{field} length for #{algorithm.inspect}: expected #{expected}, got #{decoded.bytesize}" unless decoded.bytesize == expected
|
|
118
136
|
|
|
119
137
|
decoded.b
|
|
120
138
|
end
|
|
121
|
-
private_class_method :
|
|
139
|
+
private_class_method :decode_field!
|
|
140
|
+
|
|
141
|
+
def validate_seed!(seed, algorithm)
|
|
142
|
+
bytes = String(seed).b
|
|
143
|
+
return bytes if bytes.bytesize == SEED_BYTES
|
|
144
|
+
|
|
145
|
+
raise PQCrypto::JWT::Error, "Invalid priv seed length for #{algorithm.inspect}: expected #{SEED_BYTES}, got #{bytes.bytesize}"
|
|
146
|
+
end
|
|
147
|
+
private_class_method :validate_seed!
|
|
148
|
+
|
|
149
|
+
def validate_key!(key, klass)
|
|
150
|
+
raise PQCrypto::JWT::KeyTypeError, "Expected #{klass}" unless key.is_a?(klass)
|
|
151
|
+
return if PQCrypto::JWT.signing_algorithms.any? { |c| c.pq_crypto_algorithm == key.algorithm }
|
|
152
|
+
|
|
153
|
+
raise PQCrypto::JWT::KeyTypeError, "Unsupported signature algorithm: #{key.algorithm.inspect}"
|
|
154
|
+
end
|
|
155
|
+
private_class_method :validate_key!
|
|
156
|
+
|
|
157
|
+
def derive_public_key(algorithm, seed)
|
|
158
|
+
return PQCrypto::Signature.public_key_from_seed(algorithm, seed) if PQCrypto::Signature.respond_to?(:public_key_from_seed)
|
|
159
|
+
return PQCrypto::Signature.keypair_from_seed(algorithm, seed).public_key if PQCrypto::Signature.respond_to?(:keypair_from_seed)
|
|
160
|
+
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
private_class_method :derive_public_key
|
|
122
164
|
|
|
123
|
-
def
|
|
124
|
-
unless
|
|
125
|
-
raise PQCrypto::JWT::
|
|
165
|
+
def check_seed_matches_public!(public_key, derived)
|
|
166
|
+
unless derived
|
|
167
|
+
raise PQCrypto::JWT::UnsupportedFeature,
|
|
168
|
+
"AKP JWK pub/priv consistency verification requires pq_crypto public_key_from_seed/keypair_from_seed"
|
|
126
169
|
end
|
|
170
|
+
return if public_key.to_bytes == derived.to_bytes
|
|
127
171
|
|
|
128
|
-
|
|
172
|
+
raise PQCrypto::JWT::Error, "AKP JWK public_key does not match priv seed"
|
|
129
173
|
end
|
|
130
|
-
private_class_method :
|
|
174
|
+
private_class_method :check_seed_matches_public!
|
|
175
|
+
|
|
176
|
+
def seed_from_secret_key!(secret_key)
|
|
177
|
+
if secret_key.respond_to?(:seed?) && !secret_key.seed?
|
|
178
|
+
raise PQCrypto::JWT::UnsupportedFeature,
|
|
179
|
+
"SecretKey was not created from a seed; cannot export RFC 9964 AKP JWK"
|
|
180
|
+
end
|
|
181
|
+
unless secret_key.respond_to?(:to_seed)
|
|
182
|
+
raise PQCrypto::JWT::UnsupportedFeature,
|
|
183
|
+
"SecretKey seed export requires a public pq_crypto #to_seed API; use from_seed(..., public_key:) instead"
|
|
184
|
+
end
|
|
131
185
|
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
seed = secret_key.to_seed
|
|
187
|
+
raise PQCrypto::JWT::UnsupportedFeature, "SecretKey #to_seed returned nil" if seed.nil?
|
|
134
188
|
|
|
135
|
-
|
|
189
|
+
validate_seed!(seed, secret_key.algorithm)
|
|
190
|
+
rescue PQCrypto::Error => e
|
|
191
|
+
raise PQCrypto::JWT::UnsupportedFeature, "SecretKey seed export failed: #{e.message}"
|
|
136
192
|
end
|
|
137
|
-
private_class_method :
|
|
193
|
+
private_class_method :seed_from_secret_key!
|
|
138
194
|
end
|
|
139
195
|
end
|
|
140
196
|
end
|
data/lib/pq_crypto/jwt/jwks.rb
CHANGED
|
@@ -5,50 +5,51 @@ module PQCrypto
|
|
|
5
5
|
module JWKS
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
+
CACHE_EMPTY = Object.new.freeze
|
|
9
|
+
|
|
8
10
|
def from_keys(public_keys, kids: nil)
|
|
9
11
|
keys = Array(public_keys).each_with_index.map do |public_key, index|
|
|
10
|
-
kid
|
|
11
|
-
PQCrypto::JWT::JWK.from_public_key(public_key, kid: kid)
|
|
12
|
+
PQCrypto::JWT::JWK.from_public_key(public_key, kid: kids&.fetch(index, nil))
|
|
12
13
|
end
|
|
13
14
|
{ "keys" => keys }.freeze
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
def find(jwks, kid: nil, alg: nil)
|
|
17
|
-
keys_from(jwks).find
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
def find(jwks, kid: nil, alg: nil, thumbprint: nil)
|
|
18
|
+
keys_from(jwks).find { |key| match?(key, kid, alg, thumbprint) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_all(jwks, kid: nil, alg: nil, thumbprint: nil)
|
|
22
|
+
keys_from(jwks).select { |key| match?(key, kid, alg, thumbprint) }
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def loader(jwks_hash_or_callable)
|
|
24
|
-
cached =
|
|
26
|
+
cached = CACHE_EMPTY
|
|
25
27
|
lambda do |options = {}|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
cached = nil if options && options[:invalidate]
|
|
31
|
-
cached ||= jwks_hash_or_callable
|
|
28
|
+
options ||= {}
|
|
29
|
+
cached = CACHE_EMPTY if options[:invalidate]
|
|
30
|
+
if cached.equal?(CACHE_EMPTY)
|
|
31
|
+
cached = jwks_hash_or_callable.respond_to?(:call) ? jwks_hash_or_callable.call(options) : jwks_hash_or_callable
|
|
32
32
|
end
|
|
33
|
+
cached
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def keys_from(jwks)
|
|
37
38
|
source = jwks.respond_to?(:to_hash) ? jwks.to_hash : jwks
|
|
38
|
-
keys = source["keys"] || source[:keys]
|
|
39
|
-
Array(keys).map { |key|
|
|
39
|
+
keys = source.respond_to?(:[]) ? (source["keys"] || source[:keys]) : nil
|
|
40
|
+
Array(keys).map { |key| key.to_hash.each_with_object({}) { |(k, v), out| out[String(k)] = v } }
|
|
40
41
|
end
|
|
41
42
|
private_class_method :keys_from
|
|
42
43
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
private_class_method :stringify_hash
|
|
44
|
+
def value_for(hash, key) = hash[key] || hash[key.to_sym]
|
|
45
|
+
private_class_method :value_for
|
|
47
46
|
|
|
48
|
-
def
|
|
49
|
-
|
|
47
|
+
def match?(key, kid, alg, thumbprint)
|
|
48
|
+
(kid.nil? || value_for(key, "kid") == kid) &&
|
|
49
|
+
(alg.nil? || value_for(key, "alg") == alg) &&
|
|
50
|
+
(thumbprint.nil? || PQCrypto::JWT::JWK.thumbprint(key) == thumbprint)
|
|
50
51
|
end
|
|
51
|
-
private_class_method :
|
|
52
|
+
private_class_method :match?
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
end
|
data/lib/pq_crypto/jwt/keys.rb
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
|
-
require "openssl"
|
|
5
|
-
require "set"
|
|
6
|
-
|
|
7
3
|
module PQCrypto
|
|
8
4
|
module JWT
|
|
9
5
|
module Keys
|
|
10
6
|
module_function
|
|
11
7
|
|
|
12
8
|
EXPECT_VALUES = [:auto, :signature].freeze
|
|
9
|
+
LOAD_ERRORS = [PQCrypto::Error, ArgumentError].freeze
|
|
13
10
|
|
|
14
11
|
def generate(alg)
|
|
15
12
|
algorithm = PQCrypto::JWT.algorithm_for(alg)
|
|
@@ -21,79 +18,68 @@ module PQCrypto
|
|
|
21
18
|
|
|
22
19
|
def public_from_pem(pem, expect: :auto)
|
|
23
20
|
validate_expect!(expect)
|
|
24
|
-
return PQCrypto::Signature.public_key_from_spki_pem(pem) if expect == :signature
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
key = begin
|
|
23
|
+
expect == :signature ? PQCrypto::Signature.public_key_from_spki_pem(pem) : PQCrypto::Key.from_pem(pem)
|
|
24
|
+
rescue *LOAD_ERRORS => e
|
|
25
|
+
raise PQCrypto::JWT::Error, e.message
|
|
26
|
+
end
|
|
27
|
+
as_public!(key)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def
|
|
30
|
+
def public_from_der(der, expect: :auto)
|
|
30
31
|
validate_expect!(expect)
|
|
31
|
-
return PQCrypto::Signature.secret_key_from_pkcs8_pem(pem) if expect == :signature
|
|
32
|
-
|
|
33
|
-
dispatch_secret_from_pem(pem)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def validate_expect!(expect)
|
|
37
|
-
return if EXPECT_VALUES.include?(expect)
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
key = begin
|
|
34
|
+
expect == :signature ? PQCrypto::Signature.public_key_from_spki_der(der) : PQCrypto::Key.from_der(der)
|
|
35
|
+
rescue *LOAD_ERRORS => e
|
|
36
|
+
raise PQCrypto::JWT::Error, e.message
|
|
37
|
+
end
|
|
38
|
+
as_public!(key)
|
|
40
39
|
end
|
|
41
|
-
private_class_method :validate_expect!
|
|
42
40
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
return PQCrypto::Signature.public_key_from_spki_pem(pem) if signature_oids.include?(oid.to_s)
|
|
41
|
+
def secret_from_pem(pem, expect: :auto, passphrase: nil)
|
|
42
|
+
validate_expect!(expect)
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
key = begin
|
|
45
|
+
expect == :signature ? PQCrypto::Signature.secret_key_from_pkcs8_pem(pem, passphrase: passphrase) : PQCrypto::Key.from_pem(pem, passphrase: passphrase)
|
|
46
|
+
rescue *LOAD_ERRORS => e
|
|
47
|
+
raise PQCrypto::JWT::Error, e.message
|
|
48
|
+
end
|
|
49
|
+
as_secret!(key)
|
|
48
50
|
end
|
|
49
|
-
private_class_method :dispatch_public_from_pem
|
|
50
51
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
return PQCrypto::Signature.secret_key_from_pkcs8_pem(pem) if signature_oids.include?(oid.to_s)
|
|
52
|
+
def secret_from_der(der, expect: :auto, passphrase: nil)
|
|
53
|
+
validate_expect!(expect)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
key = begin
|
|
56
|
+
expect == :signature ? PQCrypto::Signature.secret_key_from_pkcs8_der(der, passphrase: passphrase) : PQCrypto::Key.from_der(der, passphrase: passphrase)
|
|
57
|
+
rescue *LOAD_ERRORS => e
|
|
58
|
+
raise PQCrypto::JWT::Error, e.message
|
|
59
|
+
end
|
|
60
|
+
as_secret!(key)
|
|
56
61
|
end
|
|
57
|
-
private_class_method :dispatch_secret_from_pem
|
|
58
62
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
sequence.value.fetch(0).value.fetch(0).oid
|
|
62
|
-
rescue StandardError => e
|
|
63
|
-
raise PQCrypto::JWT::Error, "Unable to read SPKI algorithm OID: #{e.message}"
|
|
64
|
-
end
|
|
65
|
-
private_class_method :spki_oid_from_pem
|
|
63
|
+
def validate_expect!(expect)
|
|
64
|
+
return if EXPECT_VALUES.include?(expect)
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
sequence = OpenSSL::ASN1.decode(pem_to_der(pem))
|
|
69
|
-
sequence.value.fetch(1).value.fetch(0).oid
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
raise PQCrypto::JWT::Error, "Unable to read PKCS#8 algorithm OID: #{e.message}"
|
|
66
|
+
raise ArgumentError, "expect: must be one of #{EXPECT_VALUES.map(&:inspect).join(', ')}"
|
|
72
67
|
end
|
|
73
|
-
private_class_method :
|
|
68
|
+
private_class_method :validate_expect!
|
|
74
69
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
Base64.decode64(body)
|
|
78
|
-
end
|
|
79
|
-
private_class_method :pem_to_der
|
|
70
|
+
def as_public!(key)
|
|
71
|
+
return key if key.is_a?(PQCrypto::Signature::PublicKey)
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
@signature_oids ||= PQCrypto::JWT.signing_algorithms.filter_map do |algorithm|
|
|
83
|
-
oid_for_algorithm(algorithm.pq_crypto_algorithm)
|
|
84
|
-
end.to_set
|
|
73
|
+
raise PQCrypto::JWT::KeyTypeError, "Expected PQCrypto::Signature::PublicKey, got #{key.class}"
|
|
85
74
|
end
|
|
86
|
-
private_class_method :
|
|
75
|
+
private_class_method :as_public!
|
|
87
76
|
|
|
88
|
-
def
|
|
89
|
-
return
|
|
77
|
+
def as_secret!(key)
|
|
78
|
+
return key if key.is_a?(PQCrypto::Signature::SecretKey)
|
|
90
79
|
|
|
91
|
-
|
|
92
|
-
oid&.to_s
|
|
93
|
-
rescue StandardError
|
|
94
|
-
nil
|
|
80
|
+
raise PQCrypto::JWT::KeyTypeError, "Expected PQCrypto::Signature::SecretKey, got #{key.class}"
|
|
95
81
|
end
|
|
96
|
-
private_class_method :
|
|
82
|
+
private_class_method :as_secret!
|
|
97
83
|
end
|
|
98
84
|
end
|
|
99
85
|
end
|