diff --git a/lib/src/jsonwebkey.dart b/lib/src/jsonwebkey.dart index 128f0622..e16ff17a 100644 --- a/lib/src/jsonwebkey.dart +++ b/lib/src/jsonwebkey.dart @@ -61,6 +61,39 @@ 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 +128,7 @@ 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); 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) {