From 6aef2004f24b62a84d777c54a5cfa4e27cb17ccb Mon Sep 17 00:00:00 2001 From: rebecca <802632-arkandos@users.noreply.gitlab.com> Date: Fri, 29 May 2026 00:05:17 +0200 Subject: [PATCH] :sparkles: reject decoding hmac keys with insufficient key material --- CHANGELOG.md | 2 ++ core/src/ywt/algorithm.gleam | 8 +++++ core/src/ywt/sign_key.gleam | 38 +++++++++++++++-------- test/tests.gleam | 59 ++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894205e..ea87c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Accept RSA verification JWKs and JWKS entries without `alg`; JWT verification specializes those keys from the signed JWT `alg` header, while raw signature verification fails closed. +- Restrict `oct` signing JWKs to HMAC algorithms and reject HMAC keys shorter + than the JWA minimum for their `alg`. - Add `HeaderDecodingError` for JWT headers that are valid JSON but do not match the supported header structure. - Reject JWTs with a `crit` header parameter using the new diff --git a/core/src/ywt/algorithm.gleam b/core/src/ywt/algorithm.gleam index 441e123..629144a 100644 --- a/core/src/ywt/algorithm.gleam +++ b/core/src/ywt/algorithm.gleam @@ -99,6 +99,14 @@ pub fn to_json(alg: Algorithm) -> Json { } } +@internal +pub fn is_hmac(alg: Algorithm) -> Bool { + case alg { + Hs256 | Hs384 | Hs512 -> True + _ -> False + } +} + @internal pub fn digest_type(alg: Algorithm) -> core.DigestType { case alg { diff --git a/core/src/ywt/sign_key.gleam b/core/src/ywt/sign_key.gleam index ae39831..d17cc40 100644 --- a/core/src/ywt/sign_key.gleam +++ b/core/src/ywt/sign_key.gleam @@ -74,10 +74,7 @@ pub opaque type SignKey { pub fn hs256(secret: BitArray) -> Result(SignKey, Nil) { // A key of the same size as the hash output (for instance, 256 bits for // "HS256") or larger MUST be used with this algorithm. (JWA 3.2) - case bit_array.bit_size(secret) >= 256 { - True -> Ok(SignHmac(None, core.Sha256, secret)) - False -> Error(Nil) - } + hmac_key(None, core.Sha256, secret) } /// Creates an HMAC-SHA384 signing key. @@ -90,10 +87,7 @@ pub fn hs256(secret: BitArray) -> Result(SignKey, Nil) { /// let assert Ok(key) = sign_key.hs384(secret_48_bytes) /// ``` pub fn hs384(secret: BitArray) -> Result(SignKey, Nil) { - case bit_array.bit_size(secret) >= 384 { - True -> Ok(SignHmac(None, core.Sha384, secret)) - False -> Error(Nil) - } + hmac_key(None, core.Sha384, secret) } /// Creates an HMAC-SHA512 signing key. @@ -106,8 +100,22 @@ pub fn hs384(secret: BitArray) -> Result(SignKey, Nil) { /// let assert Ok(key) = sign_key.hs512(secret_64_bytes) /// ``` pub fn hs512(secret: BitArray) -> Result(SignKey, Nil) { - case bit_array.bit_size(secret) >= 512 { - True -> Ok(SignHmac(None, core.Sha512, secret)) + hmac_key(None, core.Sha512, secret) +} + +fn hmac_key( + id: Option(String), + digest_type: core.DigestType, + secret: BitArray, +) -> Result(SignKey, Nil) { + let minimum_bits = case digest_type { + core.Sha256 -> 256 + core.Sha384 -> 384 + core.Sha512 -> 512 + } + + case bit_array.bit_size(secret) >= minimum_bits { + True -> Ok(SignHmac(id, digest_type, secret)) False -> Error(Nil) } } @@ -162,8 +170,14 @@ fn oct_decoder(id: Option(String)) -> Decoder(SignKey) { use k <- decode.field("k", core.bits_decoder()) let digest_type = algorithm.digest_type(alg) - - decode.success(SignHmac(id, digest_type, k)) + case algorithm.is_hmac(alg) { + True -> + case hmac_key(id, digest_type, k) { + Ok(key) -> decode.success(key) + Error(_) -> decode.failure(SignHmac(id, digest_type, k), "k") + } + False -> decode.failure(SignHmac(id, digest_type, k), "alg") + } } fn ec_decoder(id: Option(String)) -> Decoder(SignKey) { diff --git a/test/tests.gleam b/test/tests.gleam index 67ffaf9..a7f65d4 100644 --- a/test/tests.gleam +++ b/test/tests.gleam @@ -69,6 +69,15 @@ fn signed_jwt(payload: json.Json, key: SignKey) { resolve(message <> "." <> bit_array.base64_url_encode(signature, False)) } +fn oct_jwk(alg: String, secret: String) -> String { + json.object([ + #("kty", json.string("oct")), + #("alg", json.string(alg)), + #("k", json.string(bit_array.base64_url_encode(<>, False))), + ]) + |> json.to_string +} + // ================================================================================================ // KEY GENERATION AND LOADING HELPERS // ================================================================================================ @@ -909,6 +918,56 @@ pub fn to_jwk_roundtrip_test() { resolve(Nil) } +pub fn oct_sign_key_decoder_accepts_minimum_hmac_keys_test() { + let assert Ok(_) = + json.parse( + oct_jwk("HS256", string.repeat("a", times: 32)), + sign_key.decoder(), + ) + let assert Ok(_) = + json.parse( + oct_jwk("HS384", string.repeat("a", times: 48)), + sign_key.decoder(), + ) + let assert Ok(_) = + json.parse( + oct_jwk("HS512", string.repeat("a", times: 64)), + sign_key.decoder(), + ) + + resolve(Nil) +} + +pub fn oct_sign_key_decoder_rejects_non_hmac_alg_test() { + let secret = string.repeat("a", times: 64) + + let assert Error(_) = json.parse(oct_jwk("RS256", secret), sign_key.decoder()) + let assert Error(_) = json.parse(oct_jwk("ES256", secret), sign_key.decoder()) + let assert Error(_) = json.parse(oct_jwk("PS256", secret), sign_key.decoder()) + + resolve(Nil) +} + +pub fn oct_sign_key_decoder_rejects_short_hmac_keys_test() { + let assert Error(_) = + json.parse( + oct_jwk("HS256", string.repeat("a", times: 31)), + sign_key.decoder(), + ) + let assert Error(_) = + json.parse( + oct_jwk("HS384", string.repeat("a", times: 47)), + sign_key.decoder(), + ) + let assert Error(_) = + json.parse( + oct_jwk("HS512", string.repeat("a", times: 63)), + sign_key.decoder(), + ) + + resolve(Nil) +} + // ================================================================================================ // 9. EC JWK ALGORITHM FIELD VALIDATION TESTS // Ensures the EC key decoder correctly handles the optional "alg" field per RFC 7517 ยง4.4 -- 2.51.2