You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Close cross-language byte-parity hole: Python's _reject_floats only walked for
float, while Node's stableStringify hard-rejects integers outside Number.MAX_SAFE_INTEGER
(2^53 - 1). A Python-signed profile with an oversized int produced a valid
self-verifying envelope that Node could not parse. Sign-time rejection on the
Python side now matches Node, plus a checked-in cross-lang fixture
(int-boundary) covers the safe-edge integer for both languages.
Renames _reject_floats to _reject_unsafe_numbers (private; not exported) and
extends it to raise ValueError for any int whose magnitude exceeds 2^53 - 1.
bool subclass of int still allowed; container walking (dict, list, tuple, set,
frozenset) preserved.
Tests: 7 new cases under TestUnsafeNumberRejection (max_safe boundary accept,
min_safe boundary accept, 2^53 reject, 2^60 reject, -(2^53) reject, nested
2^60 reject, bool accept). Existing float rejection tests kept intact under
the broader class name.
Cross-lang fixture: tests/fixtures/cross-lang/{py,node}-int-boundary.json
exercise max_safe_int / min_safe_int / small_int / neg_small_int / zero. Both
fixtures verify in both languages. node-int-boundary.json was generated by the
node-commerce companion script and copied here.
scripts/generate_int_boundary_fixture.py: one-shot regenerator.
Tests: 809 pass + 3 skipped, 95.22% coverage. ruff + ty clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`verify_ucp_profile` enforces the JWS protected header `typ='ucp-profile+jws'`, restricts `alg` to `EdDSA`/`ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`no_signature`/`missing_kid`/`kid_not_found`/`duplicate_kid`/`unsupported_alg`/`wrong_typ`/`signature_invalid`/`body_mismatch`/`malformed_jws`/`malformed_jwks`/`unusable_key`/`unrecognized_critical_header`).
234
234
235
-
`sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`.
235
+
`sign_ucp_profile` rejects profiles containing `float` values and `int` values whose magnitude exceeds `Number.MAX_SAFE_INTEGER` (2^53 - 1): cross-language float canonicalization is not stable, and Python's arbitrary-width ints lose precision when JS verifiers reparse the canonical body. Use decimal strings (e.g. `"9.99"`) for monetary or fractional fields and for any integer that may exceed the safe range.
236
236
237
237
**Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require subclassing the joserfc Key to delegate the sign hook; `OKPKey`/`ECKey` themselves only carry local key material.
0 commit comments