Skip to content

NON-BREAKING: [HYPER-181] add cryptographic signature support to all lexicons#170

Merged
s-adamantine merged 11 commits into
mainfrom
feat/signature-support
May 21, 2026
Merged

NON-BREAKING: [HYPER-181] add cryptographic signature support to all lexicons#170
s-adamantine merged 11 commits into
mainfrom
feat/signature-support

Conversation

@satyam-mishra-pce

@satyam-mishra-pce satyam-mishra-pce commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds optional cryptographic signature support to all 21 record lexicons, enabling platform attestation and verification that records were created through trusted services. The on-the-wire shape, signing procedure, and verification procedure all conform to Nick Gerakines' ATProtocol Attestation Specification at badge.blue (see also the accompanying blog post).

In particular, signatures sign the CID of the record (not its raw bytes), and CID generation injects a temporary $sig object carrying the housing repository's DID so signatures cannot be replayed across content versions or across repositories.

Changes

New lexicons

  • app.certified.signature.defs — shared type definitions for signatures. Provides:
    • #list — open union array of inline signatures and com.atproto.repo.strongRef references to remote proofs
    • #inline — inline signature object with two required fields: signature (raw ECDSA r,s bytes, low-S per BIP-0062, signing the record's 36-byte CIDv1) and key (full DID verification method reference, format did:{method}:{id}#{fragment}). The signing curve is canonically derived from the multicodec prefix on the verification method's publicKeyMultibase, so no separate algorithm-tag field is carried on the signature itself.
  • app.certified.signature.proof — remote attestation proof record holding the CID of the attested content (computed with the spec's $sig-repository binding). Lives in the attestor's repository; referenced from the attested record via strongRef. Optional application-specific fields: note, createdAt.

Modified lexicons

All 21 record lexicons gain an optional signatures property (a ref to app.certified.signature.defs#list), placed directly on the record with no wrapper object so callers access them as record.signatures[]:

  • org.hypercerts.claim.* (activity, contribution, contributorInformation, rights)
  • org.hypercerts.collection
  • org.hypercerts.context.* (acknowledgement, attachment, evaluation, measurement)
  • org.hypercerts.funding.receipt
  • org.hypercerts.workscope.tag
  • org.hyperboards.* (board, displayProfile)
  • app.certified.badge.* (award, definition, response)
  • app.certified.actor.* (profile, organization)
  • app.certified.location
  • app.certified.graph.follow
  • app.certified.link.evm

Note on app.certified.link.evm: this record carries two orthogonal integrity primitives. The EIP-712 proof field proves wallet consent (the EVM key holder agreed to be linked to the DID). The signatures array proves record provenance (e.g. that a platform UI minted the record, defending against replay of harvested EIP-712 signatures). Both are useful and they do not conflict. Bridging the signatures[] machinery to EVM keys carried in link.evm is a follow-up; see the dedicated issue.

Design notes

This is a non-breaking extension — signatures are optional on all records. Two signature patterns are supported:

  1. Inline signatures — embedded directly in the record via app.certified.signature.defs#inline
  2. Remote attestations — references to app.certified.signature.proof records in other repositories, via com.atproto.repo.strongRef

Signing algorithm

The spec mandates ECDSA with the low-S variant per BIP-0062. The signing curve is determined by the multicodec prefix on the verification method's publicKeyMultibase:

Multicodec prefix Curve JOSE alg
0xE701 secp256k1 (K-256) ES256K
0x1200 P-256 (secp256r1) ES256

There is deliberately no signatureType field on the inline signature: the algorithm is canonically derived from the resolved key, and a separate tag would risk disagreeing with the multicodec.

Signing & verification procedure (summary)

To sign:

  1. Take the record without its signatures field.
  2. Insert a temporary $sig object containing $type and the housing repository DID.
  3. Encode as canonical DAG-CBOR (sorted keys by CBOR byte length then lexically, definite-length items, shortest integer encodings).
  4. SHA-256 hash → 36-byte CIDv1 (0x01 0x71 0x12 0x20 + 32-byte hash).
  5. ECDSA-sign those 36 bytes (low-S) with the private key matching the verification method named in key.

To verify, reconstruct the same CID using the housing repo's DID, resolve key to a public key via the DID document, and check the ECDSA signature against the CID bytes.

Documentation tests

The signing example in README.md and the building-with-hypercerts-lexicons skill is no longer just illustrative prose: a new test (tests/validate-signing-example.test.ts) extracts the TypeScript code blocks from the "Cryptographic Signatures" section of each markdown file, concatenates them into a runnable ESM module, dynamically imports the result, and asserts:

  • The example runs end-to-end without throwing.
  • The produced CID is a 36-byte CIDv1 with the dag-cbor / SHA-256 prefix.
  • Secp256k1Keypair.sign() returns 64 raw r,s bytes.
  • The inline signature has the correct $type and shape.
  • The resulting record passes lexicon validation.
  • The produced signature verifies via @atproto/crypto's verifySignature (sign + verify round-trip).

This makes documentation drift structurally impossible: any edit to the docs example that breaks the procedure fails CI, because the docs are the test source. @atproto/crypto and @ipld/dag-cbor were added as devDependencies to support this test.

Example usage

import { Secp256k1Keypair } from "@atproto/crypto";
import * as dagCbor from "@ipld/dag-cbor";
import { CID } from "multiformats/cid";
import { sha256 } from "multiformats/hashes/sha2";
import { ACTIVITY_NSID } from "@hypercerts-org/lexicon";

const recordToSign = {
  $type: ACTIVITY_NSID,
  title: "Verified Reforestation Project",
  shortDescription: "Planted 1,000 trees in partnership with local community",
  createdAt: "2026-05-21T12:00:00.000Z",
};

// Insert temporary $sig metadata: attestation type + housing repo DID.
const platformDid = "did:plc:platform123";
const cborInput = {
  ...recordToSign,
  $sig: {
    $type: "app.certified.signature.defs#inline",
    repository: platformDid,
  },
};

// Encode canonically, hash, build the 36-byte CIDv1.
const cborBytes = dagCbor.encode(cborInput);
const hash = await sha256.digest(cborBytes);
const cid = CID.createV1(dagCbor.code, hash);

// ECDSA-sign the CID bytes (Keypair.sign handles low-S per BIP-0062).
const keypair = await Secp256k1Keypair.create({ exportable: false });
const signatureBytes = await keypair.sign(cid.bytes);

const signedActivity = {
  ...recordToSign,
  signatures: [
    {
      $type: "app.certified.signature.defs#inline",
      signature: signatureBytes,
      key: `${platformDid}#signing`,
    },
    {
      $type: "com.atproto.repo.strongRef",
      uri: "at://did:plc:verifier/app.certified.signature.proof/abc123",
      cid: "bafy...",
    },
  ],
};

References

Checklist

  • npm run check passes
  • Changeset added
  • Documentation updated (README.md, SCHEMAS.md regenerated, SKILL.md)
  • Tests pass (182 tests, including the new docs-extraction integration test)

Summary by CodeRabbit

Release Notes

  • New Features

    • Records now support optional cryptographic signatures for content attestation across all record types
    • Two signature patterns available: inline signatures and remote attestations via references
    • Signature capability added to 21 existing record types spanning Hypercerts, Certified, and Hyperboards
  • Documentation

    • Updated reference guides with signature specifications and workflow examples
  • Tests

    • Added comprehensive signature validation test coverage
  • Chores

    • Updated development dependencies

Review Change Stack

@changeset-bot

changeset-bot Bot commented Mar 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: efa9b4f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hypercerts-org/lexicon Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c6849e3b-81d7-4eef-9dc3-d82cf8ccd694

📥 Commits

Reviewing files that changed from the base of the PR and between 9355f01 and efa9b4f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • .agents/skills/building-with-hypercerts-lexicons/SKILL.md
  • .changeset/add-signature-support.md
  • .gitignore
  • AGENTS.md
  • ERD.puml
  • README.md
  • SCHEMAS.md
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/badge/award.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/response.json
  • lexicons/app/certified/graph/follow.json
  • lexicons/app/certified/link/evm.json
  • lexicons/app/certified/location.json
  • lexicons/app/certified/signature/defs.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/org/hypercerts/workscope/tag.json
  • package.json
  • tests/validate-signature-defs-inline.test.ts
  • tests/validate-signature-defs.test.ts
  • tests/validate-signature-proof.test.ts
  • tests/validate-signing-example.test.ts

📝 Walkthrough

Walkthrough

This PR introduces optional cryptographic signature support to the Hypercerts lexicon by defining new signature lexicons, extending 21 existing record schemas with optional signatures fields, and providing comprehensive documentation and validation tests for inline and remote attestation signing workflows.

Changes

Cryptographic Signature Attestation

Layer / File(s) Summary
Signature lexicon definitions and schema
lexicons/app/certified/signature/defs.json, lexicons/app/certified/signature/proof.json, package.json, .changeset/add-signature-support.md
Creates app.certified.signature.defs (inline signatures and list union) and app.certified.signature.proof (remote attestation proof with cid), adds @atproto/crypto dev dependency, and documents the feature in changeset.
Record schema updates across 21 lexicons
lexicons/org/hypercerts/**/*.json, lexicons/app/certified/**/*.json, lexicons/org/hyperboards/**/*.json, SCHEMAS.md
Adds optional signatures field (ref to app.certified.signature.defs#list) to activity, contribution, contributor information, rights, collection, context records (acknowledgement/attachment/evaluation), measurement, funding receipt, workscope tag, location, badge (definition/award/response), actor (organization/profile), graph follow, link.evm, board, and displayProfile lexicons.
Documentation and developer guidance
README.md, .agents/skills/building-with-hypercerts-lexicons/SKILL.md, AGENTS.md, ERD.puml, .gitignore
Surfaces new signature lexicons in Lexicon Map, adds "Signatures" and "Cryptographic Signatures" sections explaining attestation patterns (inline vs remote), provides step-by-step signing and proof-creation examples in SKILL.md, updates test naming guidance, documents ERD omissions, and adds extraction test directory to .gitignore.
Signature validation and signing workflow tests
tests/validate-signature-defs-inline.test.ts, tests/validate-signature-defs.test.ts, tests/validate-signature-proof.test.ts, tests/validate-signing-example.test.ts
Validates inline signature schemas and proof records, tests Activity records with no/inline/remote/mixed signatures, and provides an end-to-end test that extracts signing code from documentation, verifies CID/signature generation, lexicon validation, and cryptographic verification via @atproto/crypto.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • aspiers
  • holkexyz

Poem

🐰 A rabbit hops through lexicons wide,
Adding signatures, cryptographic pride,
Inline and remote attestations take flight,
Records now carry proof of their right,
Secp256k1 dance in the moonlit night! 🌙✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Lexicon Documentation Sync ❌ Error SCHEMAS.md documents signature property as 'signatureData' while README.md correctly uses 'signatures', creating critical documentation mismatch across 20 modified record lexicons. Update all property documentation tables in SCHEMAS.md to replace 'signatureData' with 'signatures' and verify alignment with actual JSON lexicon files.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Lexicons Styleguide Compliance ✅ Passed All three new signature lexicons fully comply with ATProto Lexicon Style Guide with zero style check issues and proper descriptions, naming, constraints, and union composition.
Title check ✅ Passed The title accurately describes the main change: adding cryptographic signature support to all lexicons, marking it as non-breaking.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/signature-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@satyam-mishra-pce
satyam-mishra-pce marked this pull request as ready for review March 18, 2026 04:42
@satyam-mishra-pce

Copy link
Copy Markdown
Contributor Author

Fixes HYPER-181

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
lexicons/org/hypercerts/claim/rights.json (1)

46-51: LGTM!

The optional signatureData field is correctly defined and follows the established pattern.

Consider adding test coverage for the new field. The existing tests in tests/validate-rights.test.ts don't validate behavior with signatureData present or absent. While not blocking, adding a test case with a valid signatureData payload would improve validation coverage.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lexicons/org/hypercerts/claim/rights.json` around lines 46 - 51, Add test
coverage for the new optional signatureData field by updating
tests/validate-rights.test.ts: add at least one positive test that includes a
valid signatureData object matching the "app.certified.signature.list" ref
schema and asserts the rights validation succeeds, and optionally a negative
test or absence case to assert behavior when signatureData is missing or
malformed; locate the payload construction and validation calls in the file and
extend them to include a signatureData property referencing the same sample
structure used by the "app.certified.signature.list" definition.
lexicons/app/certified/signature/list.json (1)

9-21: Require signatures when signatureData is present.

Right now {} is valid for app.certified.signature.list, which permits empty attestation envelopes. Requiring signatures (and ideally at least one item) makes the schema intent explicit.

Proposed schema tweak
   "defs": {
     "main": {
       "type": "object",
       "description": "Container object with a signatures array. Referenced by all record lexicons to provide optional cryptographic attestation support.",
+      "required": ["signatures"],
       "properties": {
         "signatures": {
           "type": "array",
           "description": "Array of cryptographic signatures attesting to record content. Uses an open union to support inline signatures and strong references to remote proof records.",
           "items": {
             "type": "union",
             "refs": [
               "app.certified.signature.inline",
               "com.atproto.repo.strongRef"
             ]
           },
+          "minLength": 1,
           "maxLength": 100
         }
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lexicons/app/certified/signature/list.json` around lines 9 - 21, The schema
for app.certified.signature.list currently allows an empty object; update the
schema so that when signatureData is present the signatures property is required
and contains at least one item: add a conditional clause (JSON Schema if/then)
that checks for the presence of "signatureData" and in the then branch requires
"signatures", and also set "signatures" to have "minItems": 1; make these
changes inside the same object that defines "properties" for "signatures" in the
app.certified.signature.list schema to ensure inline/strongRef entries are
enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/fuzzy-pandas-smile.md:
- Around line 17-37: The release notes incorrectly state "All 19 record
lexicons" and omit app.certified.link.evm; update the header count to "All 20
record lexicons" and add app.certified.link.evm to the enumerated list of record
lexicons (ensure the list includes the existing 19 entries plus
app.certified.link.evm and adjust any surrounding wording to match).

In `@lexicons/app/certified/signature/list.json`:
- Around line 1-25: ERD.puml is missing the new structural relationships for
signatureData that point to app.certified.signature.list; update ERD.puml to add
association lines from each lexicon that now contains a signatureData field
(e.g., activity, attachment, measurement, evaluation, acknowledgement, rights,
and the ~20+ other lexicons) to the app.certified.signature.list entity,
ensuring the relationship reflects the array/collection semantics and does not
include facet-only fields; also ensure the signature lexicons
(app.certified.signature.inline and app.certified.signature.proof) are
represented as related entities per the regenerated SCHEMAS.md so the diagram
shows the full signatureData → app.certified.signature.list → (inline | proof)
relationships.

In `@lexicons/app/certified/signature/proof.json`:
- Line 4: Update the CID canonicalization wording to refer to the new
signatureData structure instead of signatures: replace any mention of
"signatures" with "signatureData" and clarify that canonicalization should strip
or reference the signatureData property (which itself contains nested
"signatures") when computing the CID; ensure the example language in
proof.json's description and the CID canonicalization section explicitly states
"signatureData (object containing nested 'signatures')" so implementers
hash/sign the correct payload.

---

Nitpick comments:
In `@lexicons/app/certified/signature/list.json`:
- Around line 9-21: The schema for app.certified.signature.list currently allows
an empty object; update the schema so that when signatureData is present the
signatures property is required and contains at least one item: add a
conditional clause (JSON Schema if/then) that checks for the presence of
"signatureData" and in the then branch requires "signatures", and also set
"signatures" to have "minItems": 1; make these changes inside the same object
that defines "properties" for "signatures" in the app.certified.signature.list
schema to ensure inline/strongRef entries are enforced.

In `@lexicons/org/hypercerts/claim/rights.json`:
- Around line 46-51: Add test coverage for the new optional signatureData field
by updating tests/validate-rights.test.ts: add at least one positive test that
includes a valid signatureData object matching the
"app.certified.signature.list" ref schema and asserts the rights validation
succeeds, and optionally a negative test or absence case to assert behavior when
signatureData is missing or malformed; locate the payload construction and
validation calls in the file and extend them to include a signatureData property
referencing the same sample structure used by the "app.certified.signature.list"
definition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 934048b3-708e-42f4-968e-ebfa308f98a1

📥 Commits

Reviewing files that changed from the base of the PR and between 81b1213 and cb7411e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • .changeset/fuzzy-pandas-smile.md
  • README.md
  • SCHEMAS.md
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/badge/award.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/response.json
  • lexicons/app/certified/link/evm.json
  • lexicons/app/certified/location.json
  • lexicons/app/certified/signature/inline.json
  • lexicons/app/certified/signature/list.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/org/hypercerts/workscope/tag.json
  • package.json

Comment thread .changeset/fuzzy-pandas-smile.md Outdated
Comment thread lexicons/app/certified/signature/list.json Outdated
Comment thread lexicons/app/certified/signature/proof.json Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
lexicons/app/certified/signature/proof.json (1)

14-18: Clarify CID canonicalization details to avoid cross-implementation drift.

Line 17 states what is attested, but not the exact canonicalization/encoding process. A short pointer to the precise canonicalization rule (or explicit algorithm wording) would reduce interoperability risk.

📝 Suggested wording refinement
-            "description": "CID of the attested content (record without signatureData field, with $sig metadata including repository DID)."
+            "description": "CID of the attested content, computed using the attestation canonicalization rules (remove signatureData before hashing and include $sig metadata with repository DID as defined by the attestation spec)."

As per coding guidelines, “Lexicon files in /lexicons must comply with the atproto lexicon style guide … developers may deviate if they have good reasons, but must document deviations in the lexicon JSON file itself.”


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3391f0b-5cea-4ef7-bb29-a9dec04d9f8b

📥 Commits

Reviewing files that changed from the base of the PR and between cb7411e and 4a5e234.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • .changeset/fuzzy-pandas-smile.md
  • ERD.puml
  • README.md
  • SCHEMAS.md
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/badge/award.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/response.json
  • lexicons/app/certified/link/evm.json
  • lexicons/app/certified/location.json
  • lexicons/app/certified/signature/inline.json
  • lexicons/app/certified/signature/list.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/org/hypercerts/workscope/tag.json
  • package.json
🚧 Files skipped from review as they are similar to previous changes (18)
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/app/certified/badge/response.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/signature/inline.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/app/certified/actor/organization.json
  • lexicons/org/hypercerts/collection.json
  • README.md
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/signature/list.json
  • lexicons/org/hypercerts/claim/contribution.json
  • package.json
  • lexicons/app/certified/location.json

@aspiers

aspiers commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

I'm about to meet Nick and will ask him about the signatures object question.

@aspiers

aspiers commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Nick pointed me to the fact that you can have a ref to a definition which is of type array. This is actually a very well-hidden aspect of the spec:

Refs can not be declared as top-level named types in a schema defs array. This means that a ref can not point to another ref, nor a union. They can reference container types that contain nested ref types, for example an object.

That last sentence including the "for example" is the only clue given that it can also be array. And the Overview of Types section further up confirms that array and object are the only two container types.

@aspiers

aspiers commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Flatten signatures array directly onto records

Core change: app.certified.signature.list is now "type": "array" instead of "type": "object". All record lexicons use "signatures" (not "signatureData") as a direct ref to this array type.

Before: record.signatureData.signatures[0] (extra nesting via wrapper object)
After: record.signatures[0] (flat, matches the ATProto Attestation Spec)

Generated TypeScript:

// list.ts — now a type alias, not an interface
export type Main = (
  | $Typed<AppCertifiedSignatureInline.Main>
  | $Typed<ComAtprotoRepoStrongRef.Main>
  | { $type: string }
)[]

// activity.ts — flat property
export interface Main {
  // ...
  signatures?: AppCertifiedSignatureList.Main
}

Files changed: 26 files — the list.json lexicon, proof.json description fix, all 20 record lexicons (signatureDatasignatures), ERD, README, SCHEMAS.md, and changeset.

All checks pass: lint, typecheck, build, 11/11 tests.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
SCHEMAS.md (1)

508-511: Enhance documentation completeness for array-type lexicon.

The app.certified.signature.list documentation is notably sparse compared to other lexicons in this file. While it's correct that this is an array type (not an object) and thus has no traditional "properties," the documentation should still convey the array constraints and structure that developers need.

Based on the lexicon definition (context snippet 2), this section is missing:

  • The maxLength: 100 constraint on the array
  • Explicit mention that this is an array type (not just implied by description)
  • Clear documentation that items are a union of app.certified.signature.inline and com.atproto.repo.strongRef
📋 Suggested documentation enhancement

Consider expanding this section to include:

 ### `app.certified.signature.list`
 
 **Description:** Array of cryptographic signatures attesting to record content. Uses an open union to support inline signatures and strong references to remote proof records.
+
+**Type:** Array (maxLength: 100)
+
+#### Array Items
+
+| Type    | Description                                                                                           |
+| ------- | ----------------------------------------------------------------------------------------------------- |
+| `union` | Each item can be either an inline signature (`app.certified.signature.inline`) or a strong reference (`com.atproto.repo.strongRef`) to a remote proof record. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SCHEMAS.md` around lines 508 - 511, Update the `app.certified.signature.list`
documentation to explicitly state that it is an array type (not an object),
document the array constraint `maxLength: 100`, and describe that each item is a
union of `app.certified.signature.inline` and `com.atproto.repo.strongRef`;
reference the lexicon name `app.certified.signature.list` and the item types so
readers can locate the related schemas and understand the expected structure and
limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@SCHEMAS.md`:
- Around line 508-511: Update the `app.certified.signature.list` documentation
to explicitly state that it is an array type (not an object), document the array
constraint `maxLength: 100`, and describe that each item is a union of
`app.certified.signature.inline` and `com.atproto.repo.strongRef`; reference the
lexicon name `app.certified.signature.list` and the item types so readers can
locate the related schemas and understand the expected structure and limits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c474b99f-f6d5-4443-ad45-caea8276191f

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5e234 and 5e45b78.

📒 Files selected for processing (26)
  • .changeset/fuzzy-pandas-smile.md
  • ERD.puml
  • README.md
  • SCHEMAS.md
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/badge/award.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/response.json
  • lexicons/app/certified/link/evm.json
  • lexicons/app/certified/location.json
  • lexicons/app/certified/signature/list.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/org/hypercerts/workscope/tag.json
✅ Files skipped from review due to trivial changes (9)
  • lexicons/org/hypercerts/workscope/tag.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/app/certified/signature/list.json
  • lexicons/org/hypercerts/funding/receipt.json
  • .changeset/fuzzy-pandas-smile.md
🚧 Files skipped from review as they are similar to previous changes (14)
  • lexicons/app/certified/badge/definition.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/app/certified/actor/organization.json
  • lexicons/org/hyperboards/board.json
  • lexicons/app/certified/badge/award.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/app/certified/location.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/app/certified/badge/response.json
  • README.md
  • ERD.puml
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hypercerts/context/evaluation.json

@aspiers

aspiers commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Sent to Nick:

Hey, couple more questions about the attestations spec before I forget!

Remote attestations aren't actually signatures, right? This seems a little odd to me, because

1. it means you can have `signatures[0]` being a strongRef to a non-signature, and
2. inline attestations have much stronger integrity guarantees than remote ones thanks to the signature

Or am I misunderstanding? Thanks!

I also find the term "proof" a bit of a misnomer because an attestation is really just a claim not a hard proof of anything.

@satyam-mishra-pce

Copy link
Copy Markdown
Contributor Author

@aspiers

I agree that we can use "array" type directly for a "definition", but my claim was for a "lexicon". You can not define a lexicon that has type "array" directly.
Defining a definition was not what we needed (as I thought earlier), because if we defined a lexicon instead, we can store the signature list in a record which we can reference to, later in other records.

But as I write the comment, I realize signatures would generally be different for all these records, so there doesn't stand any scenario where we might need to store the signature list in a record and then reference it.
Did that make sense?

Moreovere, if you still find a use case (I couldn't) where we might need to reference only the signature list so that it can be referenced in other records, than we need to get back to the lexicon definition.

@holkexyz

holkexyz commented Apr 1, 2026

Copy link
Copy Markdown
Member

Do all these schemas really need the signatures? Or should we limit it to those where we will actively use them?

@holkexyz

holkexyz commented Apr 1, 2026

Copy link
Copy Markdown
Member

@aspiers is this also something that we want to add to this lexicon update? Is this mostly done? (I haven't analyzed the approach myself really)

@aspiers

aspiers commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

I resolved my remaining concerns with Nick. I think we have a straightforward plan for the way forward with this - it's probably already mostly ready and just needs rebasing. I definitely think we want to include this, ideally as part of the current round of updates, although it's a non-breaking change so could be added in a fast follow-up 1.1.0 release if necessary.

@aspiers
aspiers force-pushed the feat/signature-support branch from 09afb0f to 887bb98 Compare April 7, 2026 20:52

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/validate-signatures-on-record.test.ts`:
- Around line 5-10: The test file tests/validate-signatures-on-record.test.ts is
cross-lexicon but must follow the per-lexicon convention; rename the file to
validate-<lexicon-slug>.test.ts (e.g., validate-activity.test.ts) and change the
test suite description from "signatures property on records" to a lexicon-scoped
title (e.g., "signatures property on activity records"), or split the assertions
into separate lexicon-specific files if multiple lexicons are covered; update
any imports/exports or test runner references that point to
tests/validate-signatures-on-record.test.ts and ensure the describe block and
any helper usages (the suite name and test selectors) reflect the chosen lexicon
symbol so the file is a single-lexicon test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81442dda-bb97-46df-8818-47b76120f589

📥 Commits

Reviewing files that changed from the base of the PR and between 9355f01 and 887bb98.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • .changeset/add-signature-support.md
  • ERD.puml
  • README.md
  • SCHEMAS.md
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/app/certified/badge/award.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/response.json
  • lexicons/app/certified/link/evm.json
  • lexicons/app/certified/location.json
  • lexicons/app/certified/signature/inline.json
  • lexicons/app/certified/signature/list.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hyperboards/displayProfile.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/claim/contribution.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/org/hypercerts/workscope/tag.json
  • package.json
  • tests/validate-signature-inline.test.ts
  • tests/validate-signature-proof.test.ts
  • tests/validate-signatures-on-record.test.ts
✅ Files skipped from review due to trivial changes (20)
  • package.json
  • lexicons/app/certified/actor/organization.json
  • lexicons/app/certified/link/evm.json
  • lexicons/org/hypercerts/context/acknowledgement.json
  • lexicons/app/certified/actor/profile.json
  • lexicons/org/hypercerts/context/attachment.json
  • lexicons/app/certified/badge/definition.json
  • lexicons/app/certified/badge/award.json
  • lexicons/org/hypercerts/claim/contributorInformation.json
  • lexicons/app/certified/location.json
  • lexicons/org/hypercerts/collection.json
  • lexicons/org/hypercerts/workscope/tag.json
  • lexicons/org/hypercerts/context/evaluation.json
  • lexicons/org/hypercerts/funding/receipt.json
  • lexicons/app/certified/signature/list.json
  • lexicons/app/certified/signature/inline.json
  • lexicons/org/hypercerts/claim/rights.json
  • lexicons/app/certified/signature/proof.json
  • lexicons/app/certified/badge/response.json
  • lexicons/org/hyperboards/displayProfile.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • lexicons/org/hyperboards/board.json
  • lexicons/org/hypercerts/claim/activity.json
  • lexicons/org/hypercerts/context/measurement.json
  • lexicons/org/hypercerts/claim/contribution.json
  • .changeset/add-signature-support.md
  • ERD.puml
  • README.md
  • SCHEMAS.md

Comment thread tests/validate-signatures-on-record.test.ts Outdated
@aspiers
aspiers force-pushed the feat/signature-support branch from 887bb98 to e354b5f Compare April 7, 2026 20:59
@aspiers

aspiers commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

"array" directly. Defining a definition was not what we needed (as I thought earlier), because if we defined a lexicon instead, we can store the signature list in a record which we can reference to, later in other records.

But as I write the comment, I realize signatures would generally be different for all these records, so there doesn't stand any scenario where we might need to store the signature list in a record and then reference it. Did that make sense?

Moreovere, if you still find a use case (I couldn't) where we might need to reference only the signature list so that it can be referenced in other records, than we need to get back to the lexicon definition.

Sorry @satyam-mishra-pce I missed these good points before due to the craziness of Vancouver. I think you are right. We don't have any immediate need for a lexicon which is purely a signature list, but if we ever needed it in the future it would be easy to construct using the def we're adding here. That said, I wonder if we should avoid putting it in app.certified.signature.list because that makes it look like a normal lexicon. Maybe app.certified.signature.defs#list is better. I will do a quick bit of research into that.

@aspiers
aspiers force-pushed the feat/signature-support branch from e354b5f to 05e69fc Compare April 7, 2026 22:07
@aspiers aspiers changed the title feat: add cryptographic signature support to all lexicons NON-BREAKING: add cryptographic signature support to all lexicons Apr 7, 2026
@aspiers aspiers changed the title NON-BREAKING: add cryptographic signature support to all lexicons NON-BREAKING: [HYPER-181] add cryptographic signature support to all lexicons Apr 29, 2026
Add optional cryptographic signature support to record lexicons,
enabling platform attestation and verification that records were
created through trusted services.

Inspired by:
- ATProtocol Attestation Specification by Nick Gerakines
- The platformAttestation pattern from app.bumicerts.link.evm

New lexicons:
- app.certified.signature.inline: Inline signature with JOSE algorithm
  identifiers (ES256K, ES256, Ed25519) per ATProto convention
- app.certified.signature.defs: Shared type definitions providing the
  `#list` array def (an open union of inline signatures and strongRefs
  to remote attestation proofs). Follows the atproto convention of
  putting reusable defs in a dedicated `*.defs` lexicon; see the
  lexicon spec section on "Lexicon Files" and the style guide's
  recommendation for `.defs` schemas.
- app.certified.signature.proof: Remote attestation proof record
  holding the CID of attested content, hosted in the attestor's
  repository

19 record lexicons gain an optional `signatures` property (a flat
array referencing app.certified.signature.defs#list) so callers access
them as record.signatures[] with no wrapper nesting. This matches the
ATProtocol Attestation Specification while keeping the array
definition DRY via a single shared lexicon def.

Lexicons updated:
- org.hypercerts.claim.* (activity, contribution, contributorInformation,
  rights)
- org.hypercerts.collection
- org.hypercerts.context.* (acknowledgement, attachment, evaluation,
  measurement)
- org.hypercerts.funding.receipt
- org.hypercerts.workscope.tag
- org.hyperboards.* (board, displayProfile)
- app.certified.badge.* (award, definition, response)
- app.certified.actor.* (profile, organization)
- app.certified.location

app.certified.link.evm is deliberately excluded: it already carries
its own EIP-712 wallet-ownership proof in the `proof` field, which is
the integrity mechanism for its semantic DID ↔ wallet claim. The
EIP-712 signature is incompatible with the ATProto attestation spec
(different preimage, hash function, signature format, and binding
model), and adding a second signing mechanism on the same record
would provide no additional trust while inviting confusion about
which signature is authoritative.

Includes validation tests for inline signatures, proof records, and
signatures attached to records, plus regenerated SCHEMAS.md, ERD.puml,
README.md, and a changeset.
@aspiers

aspiers commented May 21, 2026

Copy link
Copy Markdown
Contributor

Spec-conformance pass

Audited the signature lexicons against the upstream ATProtocol Attestation Specification and tightened our wording from "inspired by" to a real conformance claim. Documenting the decisions here since several earlier design choices turned out to be unjustified or actively misaligned.

What changed

  1. Removed signatureType from app.certified.signature.inline. The original PR carried an optional JOSE-style algorithm tag (ES256, ES256K, Ed25519). The spec has no such field — by design, the algorithm is canonically derived from the multicodec prefix of the publicKeyMultibase on the verification method that key resolves to. A separate tag is redundant in the happy path and a downgrade-attack vector in the adversarial path (an attacker could set signatureType: "ES256K" while key points at a P-256 method). Spec verifiers correctly trust the multicodec, so the field added nothing and could disagree with the canonical source. Now removed.

  2. Dropped Ed25519 from the algorithm story altogether. EdDSA isn't excluded by normative MUST/SHALL language in the spec, but the spec's signing primitive is ECDSA + BIP-0062 low-S, and its multicodec→curve table only covers 0xE701 (K-256) and 0x1200 (P-256). An Ed25519-signed record would be schema-valid but unverifiable by any spec-conforming verifier — 0xED has no spec-defined branch. Generally, Ed25519 is widely used in did:key, SSH, sigstore, JOSE/JWT (EdDSA), and some COSE/WebAuthn paths — but not in @atproto/crypto, not in spec verifier pseudocode, and not in passkeys (passkeys are ES256 / P-256). Keeping it in knownValues was just a forward-looking gesture with no concrete Hypercerts use case. Dropped.

  3. Removed the maxLength: 100 cap on the signatures array. Re-read the original commit, PR design notes, and CodeRabbit summary: no rationale was ever given for the bound. Spec doesn't restrict array length. Cap dropped.

  4. README rewritten to claim spec conformance explicitly: signed input is the 36-byte CIDv1, low-S ECDSA per BIP-0062, $sig repository binding for cross-repo replay defense, and a short signing/verification summary. Dropped the JOSE algorithm-identifiers table since the field it documented is gone.

  5. Changeset and PR-description language updated: "inspired by" → "conforms to" / "implements". This is the actual claim now, not a soft attribution.

What I did not change

  • signatureType removal is breaking to the unreleased PR shape, not to any published consumer. Since this PR hasn't shipped, treating it as part of the same minor bump rather than introducing a separate breaking-change exit ramp.
  • The note and createdAt fields on app.certified.signature.proof stay. Spec only requires cid, but explicitly allows additional application-specific metadata on proof records.
  • app.certified.link.evm exclusion stays — its EIP-712 wallet-ownership proof is the right integrity primitive for that DID↔wallet semantic.

Audit clean against the spec

Spec requirement Status
Inline signature carries only $type, signature, key
signature is bytes (raw ECDSA r,s)
key is a DID verification method reference
Signatures array is open union of inline + strongRef
Proof record requires cid (additional fields allowed)
Algorithm constraint surfaces only at DID/multicodec layer ✅ (lexicon stays schema-agnostic)
Replay defense via CID + $sig.repository documented in README

Open questions / follow-ups welcome.

@aspiers

aspiers commented May 21, 2026

Copy link
Copy Markdown
Contributor

Re: my earlier comment about whether to move the list definition into a defs lexicon — this is already done. The shared list lives at app.certified.signature.defs#list and all 19 record lexicons reference it via that path. Closing that thread out.

…tion

Tighten the signature lexicons added in this PR to conform strictly to the
ATProtocol Attestation Specification, rather than carrying speculative
extensions inherited from the original design draft.

- Remove signatureType from app.certified.signature.inline. The spec has no
  such field; the signing curve is canonically derived from the multicodec
  prefix on the verification method's publicKeyMultibase. A separate tag
  was redundant in the happy path and a downgrade-attack vector in the
  adversarial path (could disagree with the multicodec).

- Drop Ed25519 from the documented algorithm story. EdDSA isn't excluded
  by normative MUST/SHALL language in the spec but the spec's signing
  primitive is ECDSA + BIP-0062 low-S and the multicodec->curve table
  only covers 0xE701 (K-256) and 0x1200 (P-256). Ed25519-signed records
  would be schema-valid but unverifiable by spec-conforming verifiers.

- Remove maxLength: 100 cap on app.certified.signature.defs#list. No
  rationale was ever recorded for the bound and the spec does not
  restrict array length.

- Update lexicon descriptions to use spec terminology: 36-byte CIDv1,
  canonical DAG-CBOR, BIP-0062 low-S, \$sig repository binding.

- Rewrite README signatures section to claim conformance explicitly,
  drop the now-stale JOSE algorithm-identifiers table, add a brief
  signing/verification procedure summary, and explain replay defense
  via CID + \$sig.repository.

- Update changeset language from "inspired by" to "conforms to".

- Drop signatureType usages from tests and remove the now-meaningless
  open-knownValues test case.

- Regenerate SCHEMAS.md.
aspiers added 5 commits May 21, 2026 14:49
Bring the agent-facing skill guide in line with the rest of the
signature work on this PR. Adds:

- Signatures subsection in Lexicon Overview covering signature.defs
  and signature.proof
- Note in the EVM Link row that link.evm can additionally carry
  signatures[] for record provenance on top of EIP-712 wallet consent
- Updated Relationship Map listing signature/defs and signature/proof,
  with a note explaining why the per-record signatures arrows aren't
  drawn
- "Attaching Cryptographic Signatures" example showing mixed inline
  + remote-attestation usage, plus a short signing-procedure summary
  pointing at the ATProto Attestation Specification
- "Creating a Remote Attestation Proof" example for the proof record
The Leaflet URL is a useful background blog post but not the
normative specification document. Update all doc references so the
primary link is the canonical spec at tangled.org and the blog post
appears as a secondary "see also" link. Affects README, changeset,
and the building-with-hypercerts-lexicons skill.
Previously the cryptographic signatures example presented a record
with signatures already embedded, using a placeholder comment for the
ECDSA bytes and explaining the signing procedure only afterward. That
reads backwards: a developer wanting to attach a signature needs to
see how the bytes are produced first.

Rewrite both README and SKILL.md so the example walks step by step:

1. Build the inline signature object — compute the spec-defined CID
   over the record (insert temporary \$sig with repository DID, encode
   canonical DAG-CBOR, hash to 36-byte CIDv1) and ECDSA-sign with
   @atproto/crypto's Secp256k1Keypair.sign() which enforces low-S
   automatically.
2. Build a remote attestation strongRef (optional).
3. Attach either or both to the record via spread.

Uses canonical ATProto + IPLD libraries (@atproto/crypto,
@ipld/dag-cbor, multiformats) rather than placeholder pseudocode.
multiformats is already a runtime dep of this package; the others
are shown as consumer imports.
Defends against docs/test divergence by making the docs themselves
the test source. The new test reads README.md and the
building-with-hypercerts-lexicons skill, finds the "Cryptographic
Signatures" section in each, extracts every fenced TypeScript code
block under it, concatenates them into one ESM module (deduping
imports and appending synthetic exports for the identifiers we want
to inspect), writes the result to tests/extracted/ (gitignored), and
dynamically imports.

For each source file, asserts:

- Extraction + execution succeeds without throwing
- The produced CID is a 36-byte CIDv1 with the dag-cbor/SHA-256 prefix
- Secp256k1Keypair.sign() returns 64 raw r,s bytes
- The inline signature has the correct \$type and references the
  signature bytes
- The remote-attestation strongRef has the correct \$type and at://
  URI
- Both signatures attach to the final record in order
- The resulting record (with a syntactically valid CID substituted
  for the "bafy..." placeholder) passes lexicon validation
- The produced signature verifies via @atproto/crypto verifySignature

Adds @atproto/crypto and @ipld/dag-cbor to devDependencies (only used
by this test; not exposed to package consumers).
Line 51 restated what line 13's #inline description already says
about ECDSA + low-S + multicodec-derived curve. Addresses review
comment on PR #170.
@s-adamantine
s-adamantine enabled auto-merge May 21, 2026 16:56
@s-adamantine
s-adamantine self-requested a review May 21, 2026 16:56
@s-adamantine
s-adamantine merged commit 478f5be into main May 21, 2026
6 checks passed
@s-adamantine
s-adamantine deleted the feat/signature-support branch May 21, 2026 16:57
@hypercerts-release-bot hypercerts-release-bot Bot mentioned this pull request May 21, 2026
@aspiers

aspiers commented May 21, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread lexicons/app/certified/link/evm.json
aspiers added a commit that referenced this pull request May 25, 2026
Align with the per-lexicon naming convention in AGENTS.md
("one file per lexicon, named validate-<lexicon-slug>.test.ts").
The tests cover the array def at app.certified.signature.defs#list
(open union of inline + strongRef, optional/empty/mixed array
semantics), so signature-defs is the lexicon under test. Activity
is just the representative carrier — all 19 record lexicons share
the same def.

Addresses CodeRabbit feedback on PR #170.
aspiers added a commit that referenced this pull request May 25, 2026
…g tests

The "one file per lexicon" rule is a default, not a strict requirement.
When a test exercises behavior that spans multiple lexicons or code
files (shared *.defs lexicons, cross-cutting union semantics, etc.),
pick the clearest name for what's under test rather than duplicating
near-identical tests across every carrier.

Cite validate-signature-defs.test.ts as an illustrative example.

Surfaced by CodeRabbit feedback on PR #170.
aspiers added a commit that referenced this pull request May 25, 2026
Line 51 restated what line 13's #inline description already says
about ECDSA + low-S + multicodec-derived curve. Addresses review
comment on PR #170.
@gotjoshua

Copy link
Copy Markdown

@s-adamantine and crew
I just came across your work after applying for some maearth.com funding and i am really impressed!

I am trying to understand where are the private keys actually stored, and how can an app use certified.app or ePDS to offer login in a way that the user can maintain local-first private keys?

Maybe I am missing something, but i don't see that in the docs (yet), so i'm asking here as its the most relevant thread i found so far.

Context: I develop local-first ipfs based apps, so i am very familiar with the crypto primitives and did system you are using, and would love to collaborate and/or rely on the certified stack as long as each agent can generate and own the private keys.

@aspiers

aspiers commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Hi @gotjoshua thanks for your interest! Firstly please note this PR is superseded by #219.

Secondly, we don't yet have any infrastructure to allow users to store local-only private keys, or even specify where private keys should live. This lexicon schema extension is just the foundation layer providing somewhere for attestation signatures to live.

Passkeys and crypto wallets are obvious candidate solutions for this, as we certainly don't want to badly reinvent wheels, especially when storage of private keys is such a security-sensitive matter. However see #217 for some of the challenges around using crypto wallets.

Like you we are very keen on the idea of adding support for users to login and attest to things using their own self-custodied private keys, and have already done some design in these areas. But I think we have some other urgent work to finish before we can go too much deeper into this.

@gotjoshua

Copy link
Copy Markdown

thanks @aspiers

don't yet have any infrastructure to allow users to store local-only private keys

ok, but does that mean that the certified servers store the private key associated with each did they create?

aspiers commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Yes this is how almost every PDS in the ATmosphere currently works. E.g. if you sign up for an account on bsky.social, you don't get given a private key to store in the browser or in a wallet.

@gotjoshua

Copy link
Copy Markdown

thanks for the clarification @aspiers !

so its a bit like a "custodial" crypto wallet situation. users trust the app servers to keep the keys and to sign on their behalf, true?

have you looked into UCAN at all?

@aspiers

aspiers commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Yes it's similar to that but users have the option to set a recovery key and migrate their account elsewhere. Yes we are familiar with UCAN. Happy to discuss further but I recommend joining the Hypercerts Telegram community and/or filing separate GitHub issues, as this PR is already closed and we're getting a bit off-topic now ;)

aspiers added a commit that referenced this pull request Jun 24, 2026
Align with the per-lexicon naming convention in AGENTS.md
("one file per lexicon, named validate-<lexicon-slug>.test.ts").
The tests cover the array def at app.certified.signature.defs#list
(open union of inline + strongRef, optional/empty/mixed array
semantics), so signature-defs is the lexicon under test. Activity
is just the representative carrier — all 19 record lexicons share
the same def.

Addresses CodeRabbit feedback on PR #170.
aspiers added a commit that referenced this pull request Jun 24, 2026
…g tests

The "one file per lexicon" rule is a default, not a strict requirement.
When a test exercises behavior that spans multiple lexicons or code
files (shared *.defs lexicons, cross-cutting union semantics, etc.),
pick the clearest name for what's under test rather than duplicating
near-identical tests across every carrier.

Cite validate-signature-defs.test.ts as an illustrative example.

Surfaced by CodeRabbit feedback on PR #170.
aspiers added a commit that referenced this pull request Jun 24, 2026
Line 51 restated what line 13's #inline description already says
about ECDSA + low-S + multicodec-derived curve. Addresses review
comment on PR #170.
aspiers added a commit that referenced this pull request Jun 24, 2026
[HYPER-181] add cryptographic signature support to record lexicons (resubmission of #170)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants