Skip to content

feat(sdk): port provenance attestation to the TypeScript, Python, and Go SDKs - #131

Merged
macanderson merged 7 commits into
mainfrom
feat/sdk-attestation-ports-b726a4bc
Aug 30, 2026
Merged

feat(sdk): port provenance attestation to the TypeScript, Python, and Go SDKs#131
macanderson merged 7 commits into
mainfrom
feat/sdk-attestation-ports-b726a4bc

Conversation

@macanderson

@macanderson macanderson commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Pull request

Summary

Ports the SPEC.md §6.5 provenance-attestation constructions to the
TypeScript, Python and Go SDKs — and first fixes the published vectors, which
could not have told a correct port from an incorrect one.

Closes #93

What changed

The vectors were unable to fail a wrong port. Every string in them was
ASCII, so a length prefix counting UTF-16 code units or code points computed
the same bytes; the only multi-leaf Merkle vector had four leaves, where RFC
6962's split and the duplicate-the-last-leaf shortcut agree; and there was no
signature and no inclusion proof, so §6.5.3 and §6.5.4 had no oracle at all.
Fixed first, because a port reconciled against a set that cannot fail proves
nothing:

  • a link whose fields are multi-byte UTF-8 ending in U+1D11E — 24 UTF-8
    bytes reading as 17 either other way, and 10 vs 8 for the astral field;
  • one-, three- and seven-leaf Merkle roots;
  • a seven-leaf inclusion proof, a fixed Ed25519 key with the signature it
    produces, and the seven verdicts;
  • the small-order public key set §6.5.4's strictness rule names but does not
    enumerate.

No previously published value changed — that would be a wire break
(SPEC.md §15).

One copy, four readers. tests/vectors/attestation-vectors.json holds the
vectors as data. The Rust suite still writes them out inline and asserts the
file agrees, so the reference stays readable as a specification while the three
ports cannot reconcile against a stale transcription.

The three ports. Each covers the length-prefixed link encoding, the
source-first chain fold, the frame commitment, the RFC 6962 root and inclusion
proofs, and strict Ed25519 verification with the named verdicts. No port
reaches for a JSON canonicalizer (ADR 0010), and no SDK gains a dependency.

Each carries the trap specific to its language, named in the module header and
the README:

  • TypeScriptString.prototype.length is UTF-16 code units. Lengths come
    off what TextEncoder produced. Node's Ed25519 (OpenSSL) accepts a
    small-order public key, so verifyCommitment declines those, and any key
    whose y is not reduced, before OpenSSL sees them.
  • Pythonlen() on a str is code points. Lengths come off
    .encode("utf-8"). The standard library has no Ed25519 and the SDK promises
    no dependencies, so contextgraph_sdk._ed25519 is a self-contained RFC 8032
    verifier — verification only, never signing — matching dalek's
    verify_strict on the cofactorless equation, reduced S, canonical
    encodings and small-order rejection. See the judgement call below.
  • Go — the native length is already a byte count, but
    contextgraph.Provenance carries its optional fields as string with
    omitempty and cannot tell an absent URI from a present empty one, which is
    exactly the distinction the presence byte makes normative. attest.Link uses
    pointers and LinkFromProvenance states the collapse it performs; the wire
    struct itself is The Go SDK's wire Provenance cannot tell an absent field from an empty one, which the attestation encoding requires #124.

CI. One step per SDK job, plus the Rust one that turned out to matter most:
the attestation feature is off by default and no workspace member enables it,
so the existing workspace-wide run had never compiled
contextgraph_types::attest or its vectors. The oracle three ports now
reconcile against was itself unrun. #117 is the durable version of that finding
and stays open; if #114's features job lands first, my one line is redundant
and I will drop it in a rebase.

Evidence

Every suite, run on this branch. Toolchains present: node v24.18.0, python
3.14.7, go 1.26.5, cargo 1.97.0. No toolchain was missing.

$ cargo test -p contextgraph-types --features attestation --test attestation_vectors
running 12 tests
test the_link_encoding_is_length_prefixed_exactly_as_specified ... ok
test the_length_prefix_counts_utf8_bytes_not_code_units ... ok
test an_empty_provenance_chain_hashes_to_the_published_genesis ... ok
test the_published_unicode_chain_vector_holds ... ok
test the_published_chain_vectors_hold ... ok
test the_published_frame_commitment_vector_holds ... ok
test the_published_merkle_vectors_hold ... ok
test the_published_inclusion_proof_vector_holds ... ok
test the_published_odd_leaf_merkle_vectors_hold ... ok
test the_shared_fixture_publishes_exactly_these_values ... ok
test the_published_verdict_vocabulary_is_reproducible_offline ... ok
test the_published_signature_vector_holds ... ok
test result: ok. 12 passed; 0 failed; 0 ignored
$ cd sdk/typescript && npx tsc && node --test "dist/test/*.test.js"
✔ the link encoding matches the published bytes
✔ the length prefix counts UTF-8 bytes, not what .length returns
✔ an absent field never encodes like an empty one
✔ the chain heads match the published vectors
✔ reordering the chain changes the head
✔ the frame commitment matches the published vector
✔ the Merkle roots match the published vectors, odd leaf counts included
✔ the inclusion proof matches the published vector and recomputes the root
✔ a proof does not validate a commitment that was not in the set
✔ the published signature verifies against the published key
✔ perturbing one byte of the commitment is caught as a mismatch, not a bad signature
✔ perturbing one byte of the signature is caught as a bad signature
✔ perturbing one byte of the frame is caught through verifyFrameAttestation
✔ every failure is named rather than collapsed into a boolean
✔ a strict verifier declines a small-order or non-canonical public key
✔ hex is accepted in exactly one spelling
ℹ tests 16   ℹ pass 16   ℹ fail 0
$ cd sdk/python && python3 -m unittest discover -s tests -v
test_a_plain_dict_frame_commits_identically ... ok
test_chain_heads_match_the_published_vectors ... ok
test_frame_commitment_matches_the_published_vector ... ok
test_reordering_the_chain_changes_the_head ... ok
test_the_two_verifiers_agree_on_every_case ... ok
test_a_non_reduced_scalar_is_rejected ... ok
test_every_perturbation_of_an_rfc_vector_is_rejected ... ok
test_rfc_8032_vectors_verify ... ok
test_the_published_small_order_table_is_exactly_the_small_order_set ... ok
test_an_absent_field_never_encodes_like_an_empty_one ... ok
test_an_explicit_none_is_absent ... ok
test_link_encoding_matches_the_published_bytes ... ok
test_the_length_prefix_counts_utf8_bytes_not_len ... ok
test_a_proof_does_not_validate_an_outsider ... ok
test_inclusion_proof_matches_and_recomputes_the_root ... ok
test_out_of_range_index_has_no_proof ... ok
test_roots_match_the_published_vectors_odd_counts_included ... ok
test_a_strict_verifier_declines_weak_keys ... ok
test_hex_is_accepted_in_exactly_one_spelling ... ok
test_every_failure_is_named ... ok
test_perturbing_the_commitment_is_a_mismatch_not_a_bad_signature ... ok
test_perturbing_the_frame_is_caught_through_verify_frame_attestation ... ok
test_perturbing_the_signature_is_a_bad_signature ... ok
test_the_published_signature_verifies ... ok

Ran 24 tests in 0.208s
OK

test_the_two_verifiers_agree_on_every_case ran rather than skipped here —
cryptography 49.0.0 is installed on this machine. On CI it will skip, and the
RFC 8032 vectors are what run there.

$ cd sdk/go && go test -count=1 -v ./contextgraph/attest/
--- PASS: TestLinkEncodingMatchesThePublishedBytes
--- PASS: TestTheLengthPrefixCountsUTF8Bytes
--- PASS: TestAnAbsentFieldNeverEncodesLikeAnEmptyOne
--- PASS: TestLinkFromProvenanceStatesItsCollapse
--- PASS: TestChainHeadsMatchThePublishedVectors
--- PASS: TestReorderingTheChainChangesTheHead
--- PASS: TestFrameCommitmentMatchesThePublishedVector
--- PASS: TestMerkleRootsMatchThePublishedVectors
--- PASS: TestInclusionProofMatchesAndRecomputesTheRoot
--- PASS: TestAProofDoesNotValidateAnOutsider
--- PASS: TestOutOfRangeIndexHasNoProof
--- PASS: TestThePublishedSignatureVerifies
--- PASS: TestPerturbingTheCommitmentIsAMismatch
--- PASS: TestPerturbingTheSignatureIsABadSignature
--- PASS: TestPerturbingTheFrameIsCaughtThroughVerifyFrameAttestation
--- PASS: TestEveryFailureIsNamed
--- PASS: TestAStrictVerifierDeclinesWeakKeys
--- PASS: TestHexIsAcceptedInExactlyOneSpelling
PASS
ok  github.com/macanderson/context-graph-protocol/sdk/go/contextgraph/attest
$ cd sdk/python && mypy --strict --python-version 3.10 contextgraph_sdk examples
Success: no issues found in 9 source files
   (mypy 2.3.1, the pin #106 added to main while this branch was open)

$ cd sdk/typescript && npm ci && npx tsc --noEmit                                        # exit 0
$ python3 .github/scripts/check-sdk-version-pins.py                                      # exit 0

$ cargo fmt --all -- --check                                                            # exit 0
$ cargo clippy -p contextgraph-types --features attestation --all-targets -- -D warnings # exit 0
$ cd sdk/go && go vet ./... && gofmt -l ./contextgraph/                                  # exit 0, no output

The negative direction

A suite that only ever passes proves nothing, so each port was broken on
purpose and the vectors were asked to notice.

TypeScript — the UTF-16 length. bytes.lengths.length in
encodeString:

✖ the link encoding matches the published bytes
  actual:   0000000466696c650100000011646f63732f6e61c3af...0100000006 72c3a9...0100000007 f09d849e...
  expected: 0000000466696c650100000018646f63732f6e61c3af...0100000008 72c3a9...010000000a f09d849e...
✖ the chain heads match the published vectors
ℹ pass 13   ℹ fail 2

0x11 is 17, the UTF-16 count; 0x18 is 24, the UTF-8 count. Note which tests
stayed green: the frame commitment and every Merkle root, because their inputs
are ASCII. That is exactly the blind spot the old vector set had.

Python — the code-point length. len(raw)len(s) in _enc_str:

FAIL: test_link_encoding_matches_the_published_bytes
  '...0100000011646f63732f...0100000006...0100000007...'
!= '...0100000018646f63732f...0100000008...010000000a...'
Ran 23 tests — FAILED (failures=2)

Go — the endianness. binary.BigEndianbinary.LittleEndian:

--- FAIL: TestLinkEncodingMatchesThePublishedBytes
     got 0400000066696c6501010000006100000000
    want 0000000466696c6501000000016100000000
--- FAIL: TestChainHeadsMatchThePublishedVectors
     got sha256:3c64583876d8437f3d53c0c10ce2175f5bbf5615c8a482946adf2ad609f8c389
    want sha256:ac5418d723088033179a2671d17cd08d3e082eefa783e3eeb1a5145f83592178

All three files were restored and re-run green before committing; the working
tree contains none of these edits.

One judgement call worth a reviewer's attention

Python carries an Ed25519 verifier. The three alternatives were each worse:
a hard dependency breaks the zero-dependency promise for every user including
those who never verify; an optional one makes a verifier answer differently
depending on what happens to be installed; and none at all leaves the Python
SDK unable to do the half of §6.5 that turns a trace into evidence.

It verifies and never signs — no key material, no nonces — and it is held to
three independent oracles: RFC 8032 §7.1's own published vectors, this
repository's dalek-produced signature, and a differential test against
cryptography covering rejections as well as acceptances. Say so if you would
rather have the dependency; #127 is where the signing side is tracked.

Checklist

  • One logical change per PR (smaller lands faster)
  • Gate is green locally — fmt, clippy -D warnings, scoped test.
    SCR-001 forbids a full workspace run here; the scoped transcripts are
    above and CI runs the rest.
  • A witness test is included — each SDK's vector suite cannot compile on
    main (the modules do not exist), and the deliberate-bug transcripts
    above show the vectors failing a wrong implementation rather than only
    passing a right one
  • Docs updated in the same PR — three SDK READMEs, tests/vectors/README.md
  • All commits signed off (git commit -s, DCO)
  • CHANGELOG.md updated under [Unreleased]

No test was deleted.

Registry submission (only if adding a row to docs/registry.md)

  • Not applicable — this PR does not add/change a conformance registry entry

Protocol-stability impact (if a spec/wire change)

  • Additive (new optional field/check) — safe within contextgraph/1

New vectors are published; no existing vector value changes and the encoding
is untouched. Publishing a case the set could not previously distinguish is
additive. A diff to a published value would not be, and there is none — the
Rust suite's parity test would fail if there were.

CI, and what did not review this PR

All 24 checks pass on 37fb994 (run 33292431566), including the four new
steps — the three SDK jobs each ran ... attestation port reproduces the published vectors and the test job ran
cargo test -p contextgraph-types --features attestation, all success.

Sourcery did not review this PR. The Sourcery review check reports
skipping: the repository's review budget is exhausted for roughly the next
sixteen hours. It did post a Reviewer's Guide comment, and that comment
carries an "Assessment against linked issues" table with three ✅ rows and no
❌ — but a guide is not a review, and I am not reporting it as one. There are
no ❌ rows to settle because no review ran to produce them.

Rebased onto #106

#106 landed mypy --strict over the Python SDK and tsc --noEmit over the
TypeScript one while this branch was open. Rebased onto it; the two new jobs
are green above, and one commit here types the attestation surface
(LinkLike, FrameLike) because a bare dict leaks Any across the boundary
a py.typed package promises to keep. The ci.yml edits merged without
conflict.

One incidental change

sdk/typescript/package-lock.json moves 1.0.02.0.0. It was stale on
mainpackage.json says 2.0.0 and the lock still said 1.0.0 — and
npm install synced it while I was adding the test script. Left in rather than
reverted, because a lockfile that disagrees with its manifest is a defect
whoever notices it should fix. It is the whole of the diff to that file.

Merge-conflict note

.github/workflows/ci.yml and CHANGELOG.md are also touched by #106 and
#114. My edits to both are additive at distinct anchors, kept deliberately
small for that reason. Happy to rebase behind either.

Residue filed

Already filed by others and not duplicated: #117 (a feature-gated test file
reports green when the feature is off) and #123 (two PRs can claim the same
ADR number — three open PRs currently claim 0012).

License

By submitting this pull request, I agree to dual-license this contribution
under MIT OR Apache-2.0, as certified by my DCO sign-off.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 16 hours and 38 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@macanderson
macanderson force-pushed the feat/sdk-attestation-ports-b726a4bc branch from 93d53d4 to 5a291cb Compare August 30, 2026 04:13
@sourcery-ai

sourcery-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR ports SPEC §6.5 provenance attestation to the TypeScript, Python, and Go SDKs, including canonical commitments, Merkle proofs, and strict Ed25519 verification, then validates all implementations against shared adversarial vectors wired into CI.

Sequence diagram for frame attestation verification

sequenceDiagram
    participant Auditor
    participant SDK
    participant Commitment
    participant Verifier

    Auditor->>SDK: verifyFrameAttestation(providerID, frame, attestation, publicKey)
    SDK->>Commitment: frameCommitment(providerID, frame)
    Commitment->>Commitment: provenanceChainHead(links)
    Commitment-->>SDK: expected commitment
    SDK->>Verifier: verifyCommitment(expected, attestation, publicKey)
    Verifier-->>SDK: named verdict
    SDK-->>Auditor: AttestationVerdict
Loading

Flow diagram for provenance commitment and Merkle proof validation

flowchart TD
    Link[Provenance links] --> Encode[encodeProvenanceLink]
    Encode --> Chain[provenanceChainHead]
    Frame[Frame identity and content digest] --> Commitment[frameCommitment]
    Chain --> Commitment
    Commitment --> Leaves[Frame commitments]
    Leaves --> Root[merkleRoot]
    Leaves --> Proof[inclusionProof]
    Proof --> Recompute[rootFromProof]
    Root --> Compare[Compare recomputed root]
    Recompute --> Compare
Loading

File-Level Changes

Change Details Files
Port the complete provenance-attestation construction to all three SDKs with language-specific encoding and strict-verification safeguards.
  • Add canonical UTF-8 length-prefixed link encoding, source-first chain heads, frame commitments, RFC 6962 Merkle roots and inclusion proofs.
  • Expose detached Ed25519 verification with named verdicts and strict rejection of malformed, non-canonical, and small-order keys.
  • Use TypeScript byte lengths, Python’s dependency-free RFC 8032 verifier, and Go pointer-based links to preserve each language’s protocol edge cases.
  • Export the new APIs and document verification, signing boundaries, presence semantics, and implementation traps.
sdk/typescript/src/attest.ts
sdk/typescript/src/index.ts
sdk/typescript/test/attest.test.ts
sdk/typescript/README.md
sdk/python/contextgraph_sdk/attest.py
sdk/python/contextgraph_sdk/_ed25519.py
sdk/python/contextgraph_sdk/__init__.py
sdk/python/tests/test_attest.py
sdk/python/README.md
sdk/go/contextgraph/attest/attest.go
sdk/go/contextgraph/attest/vectors_test.go
sdk/go/README.md
Replace the insufficient attestation test coverage with shared, adversarial cross-language vectors and Rust parity checks.
  • Publish Unicode, odd-leaf Merkle, inclusion-proof, signature, verdict, and strictness vectors without changing existing published values.
  • Make Rust assert that its inline reference values match the shared JSON fixture consumed by the SDK suites.
  • Test positive and negative cases including ordering, absent versus empty fields, outsider proofs, tampering, malformed inputs, weak keys, and non-reduced signatures.
  • Document fixture ownership, stability rules, and why the vectors live outside the record fixtures directory.
tests/vectors/attestation-vectors.json
tests/vectors/README.md
contextgraph-types/tests/attestation_vectors.rs
Integrate attestation validation into CI and package test workflows.
  • Run the feature-gated Rust attestation suite explicitly.
  • Run TypeScript, Python, and Go attestation tests in their respective SDK CI jobs.
  • Add the TypeScript test script and include tests in its TypeScript compilation.
  • Record the SDK feature and vector additions in the changelog.
.github/workflows/ci.yml
sdk/typescript/package.json
sdk/typescript/tsconfig.json
sdk/typescript/package-lock.json
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#93 Publish robust, shared provenance-attestation vectors covering UTF-8 byte-length encoding, odd-sized RFC 6962 Merkle trees, inclusion proofs, Ed25519 signatures and strict verification, while preserving existing vector values.
#93 Implement the complete provenance-attestation construction and verification in the TypeScript, Python and Go SDKs, export the APIs appropriately, avoid JSON canonicalizers and third-party dependencies, and reproduce the shared vectors in each SDK's tests.
#93 Integrate the attestation suites into CI and document the SDK APIs, language-specific pitfalls, shared vectors and the new functionality.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Mac Anderson added 7 commits August 29, 2026 21:23
… can actually fail

The published set could not separate a correct port from an incorrect one.
Every string in it was ASCII, so a length prefix counting UTF-16 code units
or code points computed the same bytes; the only multi-leaf Merkle vector had
four leaves, where RFC 6962's split and the duplicate-the-last-leaf shortcut
agree; and there was no signature and no inclusion proof at all, so the two
halves of section 6.5.4 had no oracle.

Adds a link whose fields are multi-byte UTF-8 ending in an astral-plane
character (24 UTF-8 bytes reading as 17 either other way), one-, three- and
seven-leaf roots, a seven-leaf inclusion proof, a fixed Ed25519 key with the
signature it produces, and the verdict vocabulary. No existing value changes.

Mirrors every value into tests/vectors/attestation-vectors.json, which the
TypeScript, Python and Go suites read, and asserts here that the mirror agrees
— a digest transcribed into four languages is four things that can drift.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
… the published vectors

The SPEC.md 6.5 constructions in TypeScript: the length-prefixed link
encoding, the source-first chain fold, the frame commitment, the RFC 6962 root
and inclusion proofs, and strict Ed25519 verification with the seven named
verdicts.

Every length prefix is measured off the bytes TextEncoder produced, never off
String.prototype.length. With the published unicode vector wired in, swapping
the two turns the suite red on a 0x11 where the vector says 0x18 — which is
exactly the divergence an ASCII-only vector set could not see.

Node's Ed25519 (OpenSSL) accepts a small-order public key, which 6.5.4 says a
strict verifier should not, so verifyCommitment rejects the eight small-order
encodings and any key whose y is not reduced before OpenSSL ever sees it.

The new script compiles test/attest.test.ts and runs it against
tests/vectors/attestation-vectors.json under node --test.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
… published vectors

The same constructions in Python, with the length prefix taken off
s.encode("utf-8") rather than len(s) — len counts code points, so the
published unicode vector separates the two.

Ed25519 verification needed a decision: the standard library has none, and the
SDK promises zero dependencies. contextgraph_sdk._ed25519 is a self-contained
RFC 8032 verifier — verification only, never signing — matching dalek's
verify_strict on all four counts: the cofactorless equation, a reduced S,
canonical encodings, and small-order rejection. It is checked against RFC 8032
7.1's own vectors, against this repository's dalek-produced signature, and
differentially against the cryptography package wherever that is installed.

Because it has real field arithmetic, the Python suite is also what proves the
small-order table the TypeScript and Go ports carry: it recomputes 8P =
identity for every entry rather than trusting the list.

Runs on a bare interpreter with python3 -m unittest discover -s tests.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
…ines

SPEC.md 6.5.4 says a verifier should reject small-order public keys and
non-canonical encodings, and named neither set. The Rust reference gets both
from ed25519_dalek::verify_strict; a port on Node's OpenSSL or Go's
crypto/ed25519 gets neither, because both accept a small-order key.

So the set is published rather than left to each port to rediscover: the eight
canonical encodings of a point P with 8P = identity, plus the two
non-canonical y values a verifier that reduces mod p would misread as y = 0
and y = 1. Labelled as verifier guidance rather than a wire vector, because it
constrains what a verifier accepts and not what anything encodes.

The Python suite recomputes 8P = identity for every entry from its own field
arithmetic, so the table is checked rather than trusted.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
Go is the one language of the three where the native string length is already
a UTF-8 byte count, so the port is short — but it has its own trap the others
do not: contextgraph.Provenance carries its optional fields as string with
omitempty and cannot tell an absent URI from a present empty one, which is
exactly the distinction the SPEC.md 6.5.1 presence byte makes normative.
attest.Link takes pointers, and LinkFromProvenance states the collapse it
performs rather than hiding it.

Go's crypto/ed25519 accepts a small-order public key, as Node's OpenSSL does,
so VerifyCommitment declines the published small-order set and any key whose y
is not reduced before the standard library sees them.

CI gains one step per SDK job plus, on the Rust side, the one that turned out
to matter most: the attestation feature is off by default and no workspace
member enables it, so the existing workspace-wide run had never compiled
contextgraph_types::attest or its vectors. The oracle three ports now
reconcile against was itself unrun.

Closes #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
SPEC.md's digest grammar is 64 lowercase hex characters, and
contextgraph_types::is_well_formed_digest enforces it. The ports were
inconsistent with each other: TypeScript rejected uppercase, Python's
bytes.fromhex accepted it and also skipped whitespace between byte pairs, and
Go's encoding/hex accepted it. Three implementations now agree, and each has a
named test.

The reference itself is the remaining outlier — attest::from_hex accepts A-F,
so an uppercase signed_commitment verifies in Rust and is malformed_commitment
in all three SDKs. Filed as #145 rather than changed here, because it is a
behaviour change to a published Rust function and belongs in its own diff.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
#106 landed `mypy --strict` over the Python SDK while this branch was open,
and a bare `dict` in a signature fails `--strict`: the package ships
`py.typed`, so an unparameterized mapping leaks `Any` across the exact
boundary that promise covers.

`LinkLike` and `FrameLike` name the two shapes the attestation surface accepts
— the typed one and any decoded JSON mapping — so a caller reads what is
allowed instead of inferring it, and the aliases carry the absent-vs-empty
rule the encoding depends on.

Refs #93

Signed-off-by: Mac Anderson <ops@oxagen.sh>
@macanderson
macanderson force-pushed the feat/sdk-attestation-ports-b726a4bc branch from d9b297f to 37fb994 Compare August 30, 2026 04:24
@macanderson
macanderson merged commit dada945 into main Aug 30, 2026
27 checks passed
@macanderson
macanderson deleted the feat/sdk-attestation-ports-b726a4bc branch August 30, 2026 04:38
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.

Port provenance attestation to the TypeScript, Python, and Go SDKs

1 participant