Skip to content

Security/v1 1 canonicalization#3

Merged
jaschadub merged 3 commits into
mainfrom
security/v1_1-canonicalization
May 15, 2026
Merged

Security/v1 1 canonicalization#3
jaschadub merged 3 commits into
mainfrom
security/v1_1-canonicalization

Conversation

@jaschadub
Copy link
Copy Markdown
Contributor

No description provided.

jaschadub added 3 commits May 14, 2026 16:09
Audit-identified protocol issues that require a coordinated wire-format
break across all reference implementations:

  - v not in signed payload (downgrade vector)
  - kid not in signed payload (cross-key swap)
  - no domain separator (cross-protocol signature lift)
  - float canonicalization underspecified (NaN, +/-0, denormals)
  - string canonicalization underspecified (NFC only for source)
  - extra value types underspecified
  - timestamp format underspecified
  - unknown top-level fields permitted
  - size limits absent

Spec (docs/spec.md):

  - Bump protocol version field to 2; the wire format is not compatible
    with v1.
  - Domain separator: signed_bytes = b"vectorpin/v2\x00" || canonical_json
    (13-byte tag, exact).
  - Canonical header now includes EVERY field except sig — v, kid, and
    all others are signed.
  - NFC normalization mandatory for every string-typed field; control
    chars and bidi overrides rejected.
  - Vectors with NaN/Inf rejected at sign time; +0.0 and -0.0 distinct.
  - extra is strictly map<string, string>; non-string values reject.
  - ts must match exactly /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.
  - Unknown top-level fields rejected by parsers.
  - §4.3 size limits MUST: 64 KiB pin, 32 extra entries, etc.
  - §5 verifier failure mode list expanded with KEY_EXPIRED,
    PARSE_ERROR, RECORD/COLLECTION/TENANT_MISMATCH.
  - §12 new: explicit migration notes from v1 to v2.

Python (src/vectorpin/, tests/):

  - PinHeader carries kid; canonicalize() prepends DOMAIN_TAG and emits
    sorted-keys NFC-normalized JSON over all fields including v and kid.
  - Signer rejects NaN/Inf; formats ts strictly; NFC-normalizes inputs.
  - Default Verifier accepts only v == 2.
  - LegacyV1Verifier (opt-in) byte-for-byte preserves v1 canonicalization
    for migration of existing pins.
  - KeyEntry carries (valid_from, valid_until); KEY_EXPIRED fires when
    pin.ts is outside the registered window.
  - verify(...) accepts expected_record_id/collection_id/tenant_id and
    enforces them per §5 step 8.
  - 120 tests pass (4 new test files cover v2 canonicalization, legacy
    v1 verification, replay protection, and the test-vector fixtures).

Rust (rust/vectorpin/):

  - Mirror of the Python wire format; same DOMAIN_TAG bytes, same
    canonicalization, same failure-mode taxonomy.
  - Cross-language anchor: rust/vectorpin/tests/cross_lang.rs loads
    testvectors/v2.json and asserts byte-for-byte equality of canonical
    bytes, pin_json, and Ed25519 signatures against the Python
    reference, for every fixture.
  - VerifyOptions builder for replay-protection inputs.
  - LegacyV1Verifier mirrors the Python opt-in path.
  - 50 tests pass; clippy --all-targets -- -D warnings clean.

TypeScript (typescript/):

  - Async signing/verifying API throughout (carried from P2 hardening
    branch).
  - DOMAIN_TAG enforced at module load. NFC normalization via built-in
    String.normalize. No new dependencies.
  - Cross-language anchor: typescript/test/cross-lang.test.ts asserts
    byte-for-byte equality against testvectors/v2.json on canonical
    bytes, pin_json, and Ed25519 signature.
  - 83 tests pass; npm run typecheck clean.

Test vectors:

  - testvectors/v2.json — 4 positive fixtures (f32, f64, model_hash,
    extra+record_id).
  - testvectors/negative_v2.json — 17 fixtures covering tampered vector,
    source mismatch, model mismatch, wrong v, wrong kid, bit-flipped
    sig, wrong sig length, unknown top-level field, non-string extra
    value, NaN in vector, NFD source, fractional-seconds ts, offset ts,
    lowercase t/z ts, record_id mismatch, oversize JSON.
  - testvectors/v1.json and testvectors/negative_v1.json left as-is for
    LegacyV1Verifier coverage.
  - Deterministic seed; reproducible via scripts/generate_test_vectors.py.

Migration: v1 pins do not verify under the strict v2 verifier in any of
the three languages. Use LegacyV1Verifier (Python, Rust, TypeScript) to
read v1 pins during migration; re-sign with the v2 Signer to produce
new v2 pins.
main now contains the p3-spec additions (size limits, key revocation,
key distribution, expanded failure-mode taxonomy). The v2 spec on this
branch is a complete rewrite that already subsumes every p3-spec
addition with v2-aware language:

  - §4.3 size limits — present in v2 spec verbatim
  - §7 revocation with (valid_from, valid_until) — present in v2 spec
  - §10 key distribution (fingerprint, transparency log, TOFU) — present
  - §5 KEY_EXPIRED / RECORD/COLLECTION/TENANT_MISMATCH / PARSE_ERROR
    failure codes — present, augmented by v2 wire-format requirements

Conflict resolved by taking ours (the v2 spec). All tests still pass:
Python 120, Rust 50 across targets, TypeScript 83.
@jaschadub jaschadub merged commit 0865f5b into main May 15, 2026
5 checks passed
@jaschadub jaschadub deleted the security/v1_1-canonicalization branch May 15, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant