|
43 | 43 |
|
44 | 44 | @contextlib.contextmanager |
45 | 45 | def _suppress_joserfc_eddsa_warning() -> Iterator[None]: |
46 | | - """Silence joserfc's exact RFC 9864 EdDSA SecurityWarning. |
| 46 | + """Suppress joserfc's RFC-9864-deprecation SecurityWarning around JWS sign/verify. |
47 | 47 |
|
48 | | - UCP §6 requires EdDSA support and joserfc emits a per-call deprecation |
49 | | - warning. The filter is pinned to the exact message + class |
| 48 | + joserfc emits this on every JWS operation that uses EdDSA, despite EdDSA |
| 49 | + being the actively-recommended-by-IETF algorithm for new deployments. The |
| 50 | + filter is pinned to the exact message + class |
50 | 51 | (``joserfc.errors.SecurityWarning``: ``"EdDSA is deprecated via RFC 9864"``) |
51 | | - so a future, unrelated EdDSA warning still surfaces normally. |
| 52 | + so any other SecurityWarning still surfaces normally. Key generation does |
| 53 | + not emit this warning, so suppression has no effect there. |
52 | 54 | """ |
53 | 55 | from joserfc.errors import SecurityWarning # type: ignore[import-not-found] |
54 | 56 |
|
@@ -138,8 +140,7 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS |
138 | 140 | if alg == "EdDSA": |
139 | 141 | from joserfc.jwk import OKPKey # type: ignore[import-not-found] |
140 | 142 |
|
141 | | - with _suppress_joserfc_eddsa_warning(): |
142 | | - priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) |
| 143 | + priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) |
143 | 144 | elif alg == "ES256": |
144 | 145 | from joserfc.jwk import ECKey # type: ignore[import-not-found] |
145 | 146 |
|
@@ -357,13 +358,22 @@ def verify_ucp_profile( |
357 | 358 | "duplicate_kid", |
358 | 359 | f"JWKS contains {len(matches)} keys with kid={kid!r}; expected exactly one.", |
359 | 360 | ) |
| 361 | + matched = matches[0] |
360 | 362 | # RFC 7517 §4.2: reject keys not intended for signature verification. |
361 | | - matched_use = matches[0].get("use") |
| 363 | + matched_use = matched.get("use") |
362 | 364 | if matched_use is not None and matched_use != "sig": |
363 | 365 | raise UCPVerificationError( |
364 | 366 | "unusable_key", |
365 | 367 | f"JWK with kid={kid!r} has use={matched_use!r}; expected 'sig'.", |
366 | 368 | ) |
| 369 | + # RFC 7517 §4.4: a JWK with declared `alg` constrains its use to that algorithm. |
| 370 | + header_alg = header.get("alg") |
| 371 | + matched_alg = matched.get("alg") |
| 372 | + if matched_alg is not None and matched_alg != header_alg: |
| 373 | + raise UCPVerificationError( |
| 374 | + "unusable_key", |
| 375 | + f"JWK alg {matched_alg!r} does not match JWS header alg {header_alg!r}.", |
| 376 | + ) |
367 | 377 |
|
368 | 378 | stripped = {k: v for k, v in signed_profile.items() if k != "signature"} |
369 | 379 | expected_payload = _canonicalize_profile(stripped) |
|
0 commit comments