From 7013d40676ccab302a3509fb09de7d8171837238 Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sat, 7 Feb 2026 23:08:45 +0530 Subject: [PATCH 1/2] Reject JWKs with conflicting use and key_ops --- lib/src/jsonwebkey.dart | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/src/jsonwebkey.dart b/lib/src/jsonwebkey.dart index 128f0622..ea28ff1a 100644 --- a/lib/src/jsonwebkey.dart +++ b/lib/src/jsonwebkey.dart @@ -61,6 +61,47 @@ final class JsonWebKey { this.k, }); +static void _verifyUseAndKeyOps( + String? use, + List? keyOps, + Map json, +) { + if (use == null || keyOps == null) { + return; // nothing to validate + } + + const encryptionOps = { + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + }; + + const signingOps = { + 'sign', + 'verify', + }; + + Set? allowedOps; + if (use == 'enc') { + allowedOps = encryptionOps; + } else if (use == 'sig') { + allowedOps = signingOps; + } else { + // Unknown "use" values are ignored (spec-compatible) + return; + } + + for (final op in keyOps) { + if (!allowedOps.contains(op)) { + throw FormatException( + 'JWK property "key_ops" conflicts with "use": "$use"', + json, + ); + } + } +} + static JsonWebKey fromJson(Map json) { const stringKeys = [ 'kty', @@ -95,6 +136,8 @@ final class JsonWebKey { } key_ops = (json['key_ops'] as List).map((e) => e as String).toList(); } + _verifyUseAndKeyOps(json['use'] as String?, key_ops, json); + if (json.containsKey('ext') && json['ext'] is! bool) { throw FormatException('JWK entry "ext" must be boolean', json); From 0e0330abfa48126d76236d909097e72d8ebe25f4 Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Tue, 5 May 2026 15:53:25 +0530 Subject: [PATCH 2/2] refactor(jsonwebkey): format code and add JWK validation tests --- lib/src/jsonwebkey.dart | 61 ++++++------- .../regression/jwk_use_key_ops_conflict.dart | 88 +++++++++++++++++++ lib/src/testing/testing.dart | 3 + 3 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 lib/src/testing/regression/jwk_use_key_ops_conflict.dart diff --git a/lib/src/jsonwebkey.dart b/lib/src/jsonwebkey.dart index ea28ff1a..e16ff17a 100644 --- a/lib/src/jsonwebkey.dart +++ b/lib/src/jsonwebkey.dart @@ -61,46 +61,38 @@ final class JsonWebKey { this.k, }); -static void _verifyUseAndKeyOps( - String? use, - List? keyOps, - Map json, -) { - if (use == null || keyOps == null) { - return; // nothing to validate - } + static void _verifyUseAndKeyOps( + String? use, + List? keyOps, + Map json, + ) { + if (use == null || keyOps == null) { + return; // nothing to validate + } - const encryptionOps = { - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - }; + const encryptionOps = {'encrypt', 'decrypt', 'wrapKey', 'unwrapKey'}; - const signingOps = { - 'sign', - 'verify', - }; + const signingOps = {'sign', 'verify'}; - Set? allowedOps; - if (use == 'enc') { - allowedOps = encryptionOps; - } else if (use == 'sig') { - allowedOps = signingOps; - } else { - // Unknown "use" values are ignored (spec-compatible) - return; - } + Set? allowedOps; + if (use == 'enc') { + allowedOps = encryptionOps; + } else if (use == 'sig') { + allowedOps = signingOps; + } else { + // Unknown "use" values are ignored (spec-compatible) + return; + } - for (final op in keyOps) { - if (!allowedOps.contains(op)) { - throw FormatException( - 'JWK property "key_ops" conflicts with "use": "$use"', - json, - ); + for (final op in keyOps) { + if (!allowedOps.contains(op)) { + throw FormatException( + 'JWK property "key_ops" conflicts with "use": "$use"', + json, + ); + } } } -} static JsonWebKey fromJson(Map json) { const stringKeys = [ @@ -138,7 +130,6 @@ static void _verifyUseAndKeyOps( } _verifyUseAndKeyOps(json['use'] as String?, key_ops, json); - if (json.containsKey('ext') && json['ext'] is! bool) { throw FormatException('JWK entry "ext" must be boolean', json); } diff --git a/lib/src/testing/regression/jwk_use_key_ops_conflict.dart b/lib/src/testing/regression/jwk_use_key_ops_conflict.dart new file mode 100644 index 00000000..426e8950 --- /dev/null +++ b/lib/src/testing/regression/jwk_use_key_ops_conflict.dart @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:webcrypto/src/jsonwebkey.dart'; +import '../utils/utils.dart'; + +void main() => tests().runTests(); + +List<({String name, Future Function() test})> tests() { + final tests = <({String name, Future Function() test})>[]; + void test(String name, Future Function() fn) => + tests.add((name: name, test: fn)); + + test('JsonWebKey: rejects enc use with sign/verify key_ops', () async { + bool threw = false; + try { + JsonWebKey.fromJson({ + 'kty': 'RSA', + 'use': 'enc', + 'key_ops': ['sign'], + 'n': 'x', + 'e': 'x', + }); + } on FormatException { + threw = true; + } + check(threw, 'enc use conflicts with sign operation'); + }); + + test('JsonWebKey: rejects sig use with encrypt/decrypt key_ops', () async { + bool threw = false; + try { + JsonWebKey.fromJson({ + 'kty': 'RSA', + 'use': 'sig', + 'key_ops': ['encrypt'], + 'n': 'x', + 'e': 'x', + }); + } on FormatException { + threw = true; + } + check(threw, 'sig use conflicts with encrypt operation'); + }); + + test('JsonWebKey: accepts valid use with matching key_ops', () async { + // enc use with encrypt/decrypt operations + JsonWebKey.fromJson({ + 'kty': 'RSA', + 'use': 'enc', + 'key_ops': ['encrypt', 'decrypt'], + 'n': 'x', + 'e': 'x', + }); + + // sig use with sign/verify operations + JsonWebKey.fromJson({ + 'kty': 'RSA', + 'use': 'sig', + 'key_ops': ['sign', 'verify'], + 'n': 'x', + 'e': 'x', + }); + }); + + test('JsonWebKey: ignores unknown use values', () async { + JsonWebKey.fromJson({ + 'kty': 'RSA', + 'use': 'unknown', + 'key_ops': ['sign', 'encrypt'], + 'n': 'x', + 'e': 'x', + }); + }); + + return tests; +} diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index bf99f9f3..21181312 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -30,6 +30,8 @@ import 'webcrypto/rsassapkcs1v15.dart' as rsassapkcs1v15; // Other test files, that don't use TestRunner import 'webcrypto/random.dart' as random; import 'webcrypto/digest.dart' as digest; +import 'regression/jwk_use_key_ops_conflict.dart' + as jwk_use_key_ops_conflict; /// Test runners from all test files except `digest.dart` and /// `random.dart`, which do not use [TestRunner]. @@ -58,6 +60,7 @@ void runAllTests( for (final r in _testRunners) ...r.tests(), ...random.tests(), ...digest.tests(), + ...jwk_use_key_ops_conflict.tests(), ]; for (final (:name, :test) in allTests) {