pq_crypto-jwt 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ded8bedea5417580ef9a3199f47543c037c1506dfe2e7a8db86bb6c1f2f94c7a
4
- data.tar.gz: 716ed896e2f4960dcd3ca0afbd6ef94a43e45e63ea7f7284c1d277bfb2366418
3
+ metadata.gz: 32f1a2663779d332f693d8e7fe38cfdb730ad67aba2303c178a116d75a0656a6
4
+ data.tar.gz: 13b3a76f35d0175700678dd15f6b030ec284c56092a47425a7a1dc5ef5a7e3e5
5
5
  SHA512:
6
- metadata.gz: 371849705f4387e78f3c98d08982274ad072bf519f2699c6d1df7de5254fe70e01c1e3a8d2c3d54cd2fe1573666c9fc871431726089a5907c586f6dbdfe0a7c0
7
- data.tar.gz: 8e408633d2862da41b4e546ad717153a8312e7983698058aeb53da5435523fff670ee1fe4ed2d9cb392b9aed7f655737046122aa9e2e09efe3455eea9f63d941
6
+ metadata.gz: 53333891ef2330c3e871916102ec6d08f8d7753c8b79f405b26f1f142562f5d6a91cb319aabb62499924b7bcd488a37a21f7a30390a250c356dd12ce83d14cb9
7
+ data.tar.gz: 16679d31fb72e7ea1d0563e52cbb12ada1f31ed3e8e3554ead19a81a0f3f62a9336c03ba0b43370fba7280706f7bca7d1bddc3038b739959c4a114cc4f755513
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Fixed
6
+
7
+ - Streaming detached JWS signing now rejects protected `b64` and `crit` headers instead of allowing semantic mismatch with the base64url-encoded signing input.
8
+ - Streaming detached JWS verification now fails closed for critical headers and `b64: false`.
9
+ - Streaming `chunk_size` is now validated as a positive integer before signing, verification, or detached signing-input reads.
10
+ - CI workflow now matches the documented release matrix for Ruby `3.1`, `3.2`, `3.3`, `3.4`, `4.0` crossed with `jwt ~> 3.1.0` and `jwt ~> 3.2.0`.
11
+
12
+ ### Added
13
+
14
+ - Negative streaming tests for `crit`, `b64`, malformed base64url headers, and non-positive chunk sizes.
15
+ - RFC 9964 regression tests for ML-DSA public key/signature sizes and AKP JWK thumbprint canonical members.
16
+ - Explicit tests and documentation for trusted private AKP JWK import without `verify_public: true`.
17
+
3
18
  ## 0.2.0
4
19
 
5
20
  ### Changed
data/README.md CHANGED
@@ -113,7 +113,7 @@ Private AKP JWK uses RFC 9964 seed format: `priv` is the 32 raw-byte ML-DSA seed
113
113
  secret_key = PQCrypto::JWT::JWK.secret_key_from_jwk(private_jwk)
114
114
  ```
115
115
 
116
- With `pq_crypto 0.6.1`, private import trusts the JWK `pub` field as metadata because the parent gem does not expose public seed derivation yet. Callers that need a strict `pub`/`priv` consistency check can request it explicitly; this will raise `UnsupportedFeature` until the parent exposes `Signature.public_key_from_seed` or `Signature.keypair_from_seed`:
116
+ With `pq_crypto 0.6.1`, private import trusts the JWK `pub` field as metadata because the parent gem does not expose public seed derivation yet. `secret_key_from_jwk` without `verify_public: true` is suitable only for trusted private JWK material. It does not prove that `pub` belongs to `priv`. Callers that need a strict `pub`/`priv` consistency check can request it explicitly; this will raise `UnsupportedFeature` until the parent exposes `Signature.public_key_from_seed` or `Signature.keypair_from_seed`:
117
117
 
118
118
  ```ruby
119
119
  secret_key = PQCrypto::JWT::JWK.secret_key_from_jwk(private_jwk, verify_public: true)
@@ -173,6 +173,8 @@ PQCrypto::JWT::JWA::MLDSA87
173
173
 
174
174
  `verify_io` returns `[payload_position, header]` on success. `payload_position` is `nil` for non-seekable streams that do not respond to `#pos`.
175
175
 
176
+ The streaming helper signs the regular compact JWS signing input with a base64url-encoded payload. It does not implement RFC 7797 unencoded payload mode. `sign_io` rejects protected `b64` and `crit` header fields, and `verify_io` fails closed for critical headers or `b64: false`. `chunk_size` must be a positive integer.
177
+
176
178
  ## Non-goals
177
179
 
178
180
  This adapter deliberately does **not** expose:
@@ -19,10 +19,12 @@ module PQCrypto
19
19
  def sign_io(signing_key:, payload_io: nil, io: nil, header_fields: {}, chunk_size: DEFAULT_CHUNK_SIZE)
20
20
  ensure_streaming!
21
21
  ensure_key!(signing_key, PQCrypto::Signature::SecretKey, "signing")
22
+ validate_chunk_size!(chunk_size)
22
23
  source = payload_io || io
23
24
  raise ArgumentError, "payload_io must respond to #read" unless source.respond_to?(:read)
24
25
 
25
- encoded_header = base64url(JSON.generate(header_fields.transform_keys(&:to_s).merge("alg" => alg)))
26
+ header = normalize_signing_header!(header_fields).merge("alg" => alg)
27
+ encoded_header = base64url(JSON.generate(header))
26
28
  input = DetachedSigningInputIO.new(encoded_header, source, chunk_size: chunk_size)
27
29
  signature = signing_key.sign_io(input, chunk_size: chunk_size, context: EMPTY_CONTEXT)
28
30
  "#{encoded_header}..#{base64url(signature)}"
@@ -35,6 +37,7 @@ module PQCrypto
35
37
  def verify_io(verification_key:, token:, payload_io:, chunk_size: DEFAULT_CHUNK_SIZE)
36
38
  ensure_streaming!
37
39
  ensure_key!(verification_key, PQCrypto::Signature::PublicKey, "verification")
40
+ validate_chunk_size!(chunk_size)
38
41
  raise ArgumentError, "token must be a String" unless token.is_a?(String)
39
42
  raise ArgumentError, "payload_io must respond to #read" unless payload_io.respond_to?(:read)
40
43
 
@@ -42,7 +45,7 @@ module PQCrypto
42
45
  return false unless extra.nil? && encoded_payload == "" && encoded_signature
43
46
 
44
47
  header = JSON.parse(Base64.urlsafe_decode64(encoded_header))
45
- return false unless header.is_a?(Hash) && header["alg"] == alg
48
+ return false unless supported_streaming_header?(header)
46
49
 
47
50
  signature = Base64.urlsafe_decode64(encoded_signature)
48
51
  return false unless signature_length_valid?(signature)
@@ -71,10 +74,42 @@ module PQCrypto
71
74
  def base64url(bytes)
72
75
  Base64.urlsafe_encode64(String(bytes).b, padding: false)
73
76
  end
77
+
78
+ def normalize_signing_header!(header_fields)
79
+ unless header_fields.respond_to?(:to_hash)
80
+ raise ArgumentError, "header_fields must be a Hash-like object"
81
+ end
82
+
83
+ header = header_fields.to_hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
84
+ unsupported = %w[b64 crit] & header.keys
85
+ unless unsupported.empty?
86
+ raise ArgumentError, "unsupported protected header#{unsupported.size == 1 ? '' : 's'}: #{unsupported.join(', ')}"
87
+ end
88
+
89
+ header
90
+ end
91
+
92
+ def supported_streaming_header?(header)
93
+ return false unless header.is_a?(Hash) && header["alg"] == alg
94
+ return false if header.key?("crit")
95
+ return false if header.key?("b64") && header["b64"] != true
96
+
97
+ true
98
+ end
99
+
100
+ def validate_chunk_size!(chunk_size)
101
+ return if chunk_size.is_a?(Integer) && chunk_size.positive?
102
+
103
+ raise ArgumentError, "chunk_size must be a positive Integer"
104
+ end
74
105
  end
75
106
 
76
107
  class DetachedSigningInputIO
77
108
  def initialize(encoded_header, payload_io, chunk_size: MLDSAStreaming::DEFAULT_CHUNK_SIZE)
109
+ unless chunk_size.is_a?(Integer) && chunk_size.positive?
110
+ raise ArgumentError, "chunk_size must be a positive Integer"
111
+ end
112
+
78
113
  @prefix = "#{encoded_header}.".b
79
114
  @payload_io = payload_io
80
115
  @chunk_size = chunk_size
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PQCrypto
4
4
  module JWT
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pq_crypto-jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
@@ -100,7 +100,7 @@ licenses:
100
100
  - MIT
101
101
  metadata:
102
102
  source_code_uri: https://github.com/roman-haidarov/pq_crypto-jwt
103
- changelog_uri: https://github.com/roman-haidarov/pq_crypto-jwt/blob/v0.2.0/CHANGELOG.md
103
+ changelog_uri: https://github.com/roman-haidarov/pq_crypto-jwt/blob/v0.2.1/CHANGELOG.md
104
104
  bug_tracker_uri: https://github.com/roman-haidarov/pq_crypto-jwt/issues
105
105
  post_install_message:
106
106
  rdoc_options: []