If you've been following along with my previous posts about decentralized identity and cryptographic proofs in ATProtocol, you know I've been exploring how we can build verifiable trust into decentralized systems. Today, I'm excited to share the formal specification for ATProtocol attestations - a framework that brings cryptographic signatures to ATProtocol records in a way that's both secure and surprisingly simple.
The full technical specification is available for those who want the complete implementation details, but let me walk you through the key concepts and design decisions that make this framework powerful.
The Problem We're Solving
In a decentralized ecosystem, we need ways to prove that specific identities have endorsed, verified, or made claims about content. Think of it like a digital notary stamp, but one that's cryptographically verifiable and impossible to forge. Whether it's an organization verifying someone's profile, a badge issuer confirming an achievement, or multiple authors signing off on a collaborative document, we need a robust way to create these proofs.
The challenge is doing this while preventing replay attacks - where someone might try to copy a valid signature from one place and reuse it somewhere else. We also need to balance simplicity with security, because overly complex systems tend to have more attack surfaces and implementation bugs.
Two Patterns, One Framework
The attestation spec defines two complementary patterns that share the same underlying CID generation process but serve different use cases.
Inline attestations embed signatures directly in records. When you create an inline attestation, you're adding a signature object right into the record's signatures
array. The signature itself is generated by signing the CID of the record content, and the whole thing travels together. Here's what that looks like:
{
"$type": "app.example.record",
"createdAt": "2025-10-14T12:00:00Z",
"text": "Example content that is being attested",
"signatures": [
{
"$type": "com.example.inlineSignature",
"signature": {"$bytes": "MzQ2Y2U4ZDNhYmM5NjU0Mzk5NWJmNjJkOGE4..."},
"key": "did:web:example.com#signing1"
}
]
}
The beauty here is radical simplicity. An inline attestation only needs three required fields: the $type
to identify what kind of signature it is, the signature
bytes themselves, and a key
reference telling us which cryptographic key was used.
Remote attestations take a different approach. Instead of embedding the signature, you create a separate proof record in your own repository that contains the CID of the content you're attesting to. The original record then references your proof using a strong reference:
{
"$type": "com.example.proof",
"cid": "bafyreifsqhrnlciktfxkz4yiqw5wtx6xvods67aicqt5tc7cly24dmhv3e"
}
This pattern shines when you need revocable attestations or when the attestor wants to maintain control over their proofs. Since the proof lives in the attestor's repository, they can delete it if needed, effectively revoking the attestation.
The Magic of CID Generation
Both patterns rely on content-addressable storage through CIDs (Content Identifiers). The CID generation process is where the security magic happens, and it's the same for both attestation types.
When generating a CID for attestation, we temporarily inject a special $sig
metadata object that includes critical security information. This object must always contain a $type
field and, crucially, a repository
field containing the DID of the repository housing the record. This repository binding is what prevents replay attacks - an attacker can't just copy your signed record into their repository because the CID would change.
Here's the process in pseudocode:
FUNCTION generateRecordCID(recordData, sigMetadata, repositoryDID):
encodingData = copy(recordData)
DELETE encodingData["signatures"] # Remove signatures field
# Add $sig metadata with repository binding
sigMetadata["repository"] = repositoryDID # CRITICAL: prevents replay attacks
encodingData["$sig"] = sigMetadata
# Encode to DAG-CBOR and generate CID
cborBytes = encodeDAGCBORCanonical(encodingData)
cidBytes = generateCIDFromCBOR(cborBytes)
RETURN cidBytes
END FUNCTION
The $sig
object is only present during CID generation - it never gets stored in the final record. For inline attestations, we sign these CID bytes to create the signature. For remote attestations, we store the CID directly in the proof record. Same CID generation, different usage patterns.
Union Types and the Signatures Array
The signatures
array uses ATProtocol's union type system, which means it can contain different types of objects as long as they include a $type
field. This allows us to mix inline signatures and strong references to remote attestations in the same array:
"signatures": [
{
"$type": "com.example.inlineSignature",
"signature": {"$bytes": "..."},
"key": "did:plc:signer1#method"
},
{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:verifier/com.example.proof/3m3i7e3uhrocj",
"cid": "bafyreig5ug2vj63ag5b6okth3roujv2lngxnyssxeylfcmmqiznfje4enu"
}
]
This flexibility means applications can choose the right attestation pattern for their needs, or even combine both in a single record.
Real-World Examples
Let me show you how this works in practice with a profile verification scenario. Imagine a verification service wants to confirm someone's identity. They create a proof record in their repository:
{
"$type": "network.bsky.verification.proof",
"cid": "bafyreig7w5q432clkzxn5azlybqi37lnuvxvl3uucbqojgew4cujyoamzq",
"type": "individual"
}
The user's profile then references this proof:
{
"$type": "app.bsky.actor.profile",
"displayName": "Nick Gerakines",
"description": "Protocols, platforms, and machine learning",
"signatures": [
{
"$type": "com.atproto.repo.strongRef",
"cid": "bafyreigk73rnjpjfjjeeii25w2cczdq7tpzwrv4xeyo7gs47m75pqshbau",
"uri": "at://did:plc:verify.bsky.network/network.bsky.verification.proof/3m3i7it5ya7cq"
}
]
}
Now anyone can verify that the verification service has indeed attested to this profile. They reconstruct the CID using the same process (including the repository binding in $sig
), and check that it matches the CID stored in the proof record.
For inline attestations, consider a badge system where achievements are signed directly:
{
"$type": "community.lexicon.badge.award",
"badge": {
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:issuer/community.lexicon.badge.definition/3ltwfsgx3vu2a",
"cid": "bafyreibnfpriilyjmssycvlkcp46cmoscwon7okbfvhjmobggisinerj5e"
},
"did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
"issued": "2025-07-14T12:00:00.000Z",
"signatures": [
{
"$type": "community.lexicon.badge.proof",
"key": "did:plc:issuer#badge",
"signature": {"$bytes": "JjLKuf35PstZQhef36SHtGrPrlvWy6+Qt6xI2zINOBNAxh4pAAaq..."}
}
]
}
The signature was created by the badge issuer signing the CID of the award record (with the appropriate $sig
metadata including the repository DID). Anyone can verify this signature using the public key referenced in the key
field.
Cryptographic Details
The spec uses standard elliptic curve cryptography with two supported curves: P-256 (the NIST standard curve that's WebCrypto compatible) and K-256 (the Bitcoin/Ethereum curve that's default in ATProtocol). All signatures use ECDSA with the "low-S" variant as specified in BIP-0062, which helps prevent signature malleability attacks.
The verification process follows a straightforward path. For inline attestations, you reconstruct the record with the $sig
metadata (including the repository DID), generate the CID, and verify the signature against that CID using the public key from the referenced DID document. For remote attestations, you generate the CID the same way and check that it matches the CID stored in the proof record.
Application Extensibility
One of the powerful aspects of this design is how applications can define their own attestation types. The $type
field in signatures lets you create application-specific semantics. You might have com.example.contentModeration
for moderation decisions, org.university.transcript
for academic credentials, or app.medical.prescription
for healthcare attestations.
Applications can also inject custom metadata into the $sig
object during CID generation. This lets you bind attestations to specific contexts, add expiration semantics, or include other application-specific data that becomes part of the cryptographic commitment.
Reference Implementation
All of the examples in the spec are functional. The reference implementation in the atproto-attestation crate at https://tangled.org/@smokesignal.events/atproto-identity-rs contains everything needed to create and validate both inline and remote attestations. The crate also provides the atproto-attestation-sign and atproto-attestation-verify programs.
To run these tools, check out the repository and build the docker container with your tool of choice (podman build -t docker.io/ngerakines/atproto-tools:0.14.0-rc.1 .) and then adjust the commands in this post (podman run docker.io/ngerakines/atproto-tools:0.14.0-rc.1 atproto-attestation-verify -h).
For a real-world example, let's create a record that will have a remote attestation:
{
"$type": "me.ngerakines.foo",
"foo": "bar"
}
I'll use the atproto-attestation-sign program to create a remote attestation of the type me.ngerakiens.baz:
atproto-attestation-sign remote did:plc:cbkjy5n7bk3ax2wplmtjofq2 \
'{"$type": "me.ngerakines.foo", "foo": "bar"}' \
did:plc:cbkjy5n7bk3ax2wplmtjofq2 \
'{"$type": "me.ngerakiens.baz"}'
This will output both the Attested Record (with strongRef) and the Proof Record (store in repository).
Now I'm going to publish the attested record to my repository with pdsls.dev:
{
"$type": "me.ngerakines.foo",
"foo": "bar",
"signatures": [
{
"$type": "com.atproto.repo.strongRef",
"cid": "bafyreicfipxcm3essd2xybdamukruigy6umyxgjz66wjvehseqbcpkkffa",
"uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/me.ngerakiens.baz/3m3ic7nxjxhrp"
}
]
}
The proof record points back to a CID of just the content that is being signed. I'm going to publish that record to my repository, honoring both the collection ($type that I provided) and the record key:
{
"$type": "me.ngerakiens.baz",
"cid": "bafyreicf2rsd4ggjvrejt4ycndnpqw6lf5khpwwncs6nljyhxhdqsqkxkq"
}
Now I can use the atproto-attestation-verify command to ensure that everything validates:
atproto-attestation-verify \
at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/me.ngerakines.foo/3m3fslrmd7j2o \
did:plc:cbkjy5n7bk3ax2wplmtjofq2
Looking Forward
This attestation framework provides the cryptographic foundation for trust in the ATProtocol ecosystem. By keeping the core specification minimal while allowing application-level extensibility, we get both security and flexibility. The repository binding prevents replay attacks, the CID-based approach ensures content integrity, and the dual pattern support accommodates different operational needs.
As we build more sophisticated applications on ATProtocol, these attestations will become the building blocks for verified credentials, trusted content, and authenticated interactions. Whether you're building a verification service, a badge system, or any application that needs cryptographic proofs, this framework provides the tools you need.
The full technical specification dives deeper into the implementation details, including complete algorithms, security considerations, and integration patterns. I'm excited to see what the community builds with these cryptographic primitives, and I'll be sharing more about practical implementations in upcoming posts.