atproto-attestation#
Utilities for creating and verifying AT Protocol record attestations using the CID-first workflow.
Overview#
A Rust library implementing the CID-first attestation specification for AT Protocol records. This crate provides cryptographic signature creation and verification for records, supporting both inline attestations (signatures embedded directly in records) and remote attestations (separate proof records with strongRef references).
The attestation workflow ensures deterministic signing payloads and prevents replay attacks by:
- Automatically preparing records with
$sigmetadata containing$typeandrepositoryfields - Generating content identifiers (CIDs) using DAG-CBOR serialization
- Signing CID bytes with elliptic curve cryptography (for inline attestations)
- Normalizing signatures to low-S form to prevent malleability attacks
- Embedding signatures or creating proof records with strongRef references
Critical Security Feature: The repository field in $sig metadata binds attestations to specific repositories, preventing replay attacks where an attacker might attempt to clone records from one repository into their own.
Features#
- Inline attestations: Embed cryptographic signatures directly in record structures
- Remote attestations: Create separate proof records with CID-based strongRef references
- CID-first workflow: Deterministic signing based on content identifiers
- Multi-curve support: Full support for P-256, P-384, and K-256 elliptic curves
- Signature normalization: Automatic low-S normalization for ECDSA signatures to prevent malleability
- Flexible input types: Accept records as JSON strings, JSON values, or typed lexicons
- Repository binding: Automatic prevention of replay attacks
CLI Tools#
The following command-line tools are available when built with the clap and tokio features:
atproto-attestation-sign: Sign AT Protocol records with inline or remote attestationsatproto-attestation-verify: Verify cryptographic signatures on AT Protocol records
Library Usage#
Creating Inline Attestations#
Inline attestations embed the signature bytes directly in the record:
use atproto_identity::key::{generate_key, to_public, KeyType};
use atproto_attestation::{create_inline_attestation, input::{AnyInput, PhantomSignature}};
use serde_json::json;
fn main() -> anyhow::Result<()> {
// Generate a signing key
let private_key = generate_key(KeyType::K256Private)?;
let public_key = to_public(&private_key)?;
let key_reference = format!("{}", &public_key);
// The record to sign
let record = json!({
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z"
});
// Repository housing this record (for replay attack prevention)
let repository_did = "did:plc:repo123";
// Attestation metadata (required: $type and key for inline attestations)
// Note: repository field is automatically added during CID generation
let sig_metadata = json!({
"$type": "com.example.inlineSignature",
"key": &key_reference,
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z"
});
// Create inline attestation (repository_did is bound into the CID)
// Signature is automatically normalized to low-S form
let signed_record = create_inline_attestation::<PhantomSignature, PhantomSignature>(
AnyInput::Json(record),
AnyInput::Json(sig_metadata),
repository_did,
&private_key
)?;
println!("{}", serde_json::to_string_pretty(&signed_record)?);
Ok(())
}
The resulting record will have a signatures array:
{
"$type": "app.bsky.feed.post",
"text": "Hello world!",
"createdAt": "2024-01-01T00:00:00.000Z",
"signatures": [
{
"$type": "com.example.inlineSignature",
"key": "did:key:zQ3sh...",
"issuer": "did:plc:issuer123",
"issuedAt": "2024-01-01T00:00:00.000Z",
"cid": "bafyrei...",
"signature": {
"$bytes": "base64-encoded-normalized-signature-bytes"
}
}
]
}
Creating Remote Attestations#
Remote attestations create a separate proof record that must be stored in a repository:
use atproto_attestation::{create_remote_attestation, input::{AnyInput, PhantomSignature}};
use serde_json::json;
fn main() -> anyhow::Result<()> {
let record = json!({
"$type": "app.bsky.feed.post",
"text": "Hello world!"
});
// Repository housing the original record (for replay attack prevention)
let repository_did = "did:plc:repo123";
// DID of the entity creating the attestation (will store the proof record)
let attestor_did = "did:plc:attestor456";
let metadata = json!({
"$type": "com.example.attestation",
"issuer": "did:plc:issuer123",
"purpose": "verification"
});
// Create both the attested record and proof record in one call
// Returns: (attested_record_with_strongRef, proof_record)
let (attested_record, proof_record) = create_remote_attestation::<PhantomSignature, PhantomSignature>(
AnyInput::Json(record),
AnyInput::Json(metadata),
repository_did, // Repository housing the original record
attestor_did // Repository that will store the proof record
)?;
// The proof_record should be stored in the attestor's repository
// The attested_record contains the strongRef reference
println!("Proof record:\n{}", serde_json::to_string_pretty(&proof_record)?);
println!("Attested record:\n{}", serde_json::to_string_pretty(&attested_record)?);
Ok(())
}
Verifying Signatures#
Verify all signatures in a record:
use atproto_attestation::{verify_record, input::AnyInput};
use atproto_identity::key::IdentityDocumentKeyResolver;
use atproto_client::record_resolver::HttpRecordResolver;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Signed record with signatures array
let signed_record = /* ... */;
// The repository DID where this record is stored
// CRITICAL: This must match the repository used during signing to prevent replay attacks
let repository_did = "did:plc:repo123";
// Create resolvers for key and record fetching
let key_resolver = /* ... */; // IdentityDocumentKeyResolver
let record_resolver = HttpRecordResolver::new(/* ... */);
// Verify all signatures with repository validation
verify_record(
AnyInput::Json(signed_record),
repository_did,
key_resolver,
record_resolver
).await?;
println!("✓ All signatures verified successfully");
Ok(())
}
Command Line Usage#
Signing Records#
Inline Attestation#
# Sign with inline attestation (signature embedded in record)
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-sign -- \
inline \
record.json \
did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA \
metadata.json
# Using JSON strings instead of files
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-sign -- \
inline \
'{"$type":"app.bsky.feed.post","text":"Hello!"}' \
did:key:zQ3sh... \
'{"$type":"com.example.sig","key":"did:key:zQ3sh..."}'
# Read record from stdin
cat record.json | cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-sign -- \
inline \
- \
did:key:zQ3sh... \
metadata.json
Remote Attestation#
# Create remote attestation (generates proof record + strongRef)
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-sign -- \
remote \
record.json \
did:plc:repo123... \
metadata.json
# This outputs TWO JSON objects:
# 1. Proof record (store this in the attestor's repository)
# 2. Source record with strongRef attestation
Verifying Signatures#
# Verify all signatures in a record from file
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-verify -- \
./signed_record.json \
did:plc:repo123
# Verify from stdin
cat signed_record.json | cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-verify -- \
- \
did:plc:repo123
# Verify from inline JSON
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-verify -- \
'{"$type":"app.bsky.feed.post","text":"Hello","signatures":[...]}' \
did:plc:repo123
# Verify specific attestation against record
cargo run --package atproto-attestation --features clap,tokio --bin atproto-attestation-verify -- \
./record.json \
did:plc:repo123 \
./attestation.json
Public API#
The crate exposes the following public functions:
Attestation Creation#
-
create_inline_attestation: Create a signed record with embedded signature- Automatically normalizes signatures to low-S form
- Binds attestation to repository DID
- Returns signed record with
signaturesarray
-
create_remote_attestation: Create separate proof record and strongRef- Returns tuple of (attested_record, proof_record)
- Proof record must be stored in attestor's repository
CID Generation#
create_cid: Generate CID for a record with$sigmetadatacreate_dagbor_cid: Generate CID for any serializable datacreate_attestation_cid: High-level CID generation with automatic$sigpreparation
Signature Operations#
normalize_signature: Normalize raw signature bytes to low-S form- Prevents signature malleability attacks
- Supports P-256, P-384, and K-256 curves
Verification#
verify_record: Verify all signatures in a record- Validates repository binding
- Supports both inline and remote attestations
- Requires key and record resolvers
Input Types#
AnyInput: Flexible input enum supporting:String: JSON string to parseJson: serde_json::ValueTypedLexicon: Strongly-typed lexicon records
Attestation Specification#
This crate implements the CID-first attestation specification, which ensures:
- Deterministic signing: Records are serialized to DAG-CBOR with
$sigmetadata, producing consistent CIDs - Content addressing: Signatures are over CID bytes, not the full record
- Repository binding: Every attestation is bound to a specific repository DID to prevent replay attacks
- Signature normalization: ECDSA signatures are normalized to low-S form to prevent malleability
- Flexible metadata: Custom fields in
$sigare preserved and included in the CID calculation - Multiple attestations: Records can have multiple signatures in the
signaturesarray
Signature Structure#
Inline attestation entry:
{
"$type": "com.example.signature",
"key": "did:key:z...",
"issuer": "did:plc:...",
"cid": "bafyrei...",
"signature": {
"$bytes": "base64-normalized-signature"
}
}
Remote attestation entry (strongRef):
{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:repo/com.example.attestation/tid",
"cid": "bafyrei..."
}
Error Handling#
The crate provides structured error types via AttestationError:
RecordMustBeObject: Input must be a JSON objectMetadataMustBeObject: Attestation metadata must be a JSON objectSigMetadataMissing: No$sigfield found in prepared recordSignatureCreationFailed: Key signing operation failedSignatureValidationFailed: Signature verification failedSignatureNotNormalized: ECDSA signature not in low-S formSignatureLengthInvalid: Signature bytes have incorrect lengthKeyResolutionFailed: Could not resolve verification keyUnsupportedKeyType: Key type not supported for signing/verificationRemoteAttestationFetchFailed: Failed to fetch remote proof record
Security Considerations#
Repository Binding and Replay Attack Prevention#
The most critical security feature of this attestation framework is repository binding. Every attestation includes the repository DID in the $sig metadata during CID generation, which:
- Prevents replay attacks: An attacker cannot copy a signed record from one repository to another because the signature is bound to the original repository DID
- Ensures context integrity: Attestations are only valid within their intended repository context
- Automatic enforcement: The library automatically adds the repository field during CID generation
Important: Always verify signatures with the correct repository DID. Verifying with a different repository DID will (correctly) fail validation, as this would indicate a potential replay attack.
Signature Normalization#
All ECDSA signatures are automatically normalized to low-S form to prevent signature malleability attacks:
- The library enforces low-S normalization during signature creation
- Verification accepts only normalized signatures
- This prevents attackers from creating alternate valid signatures for the same content
Key Management Best Practices#
- Private keys: Never log, transmit, or store private keys in plaintext
- Key rotation: Plan for key rotation by using verification method references that can be updated in DID documents
- Key types: The library supports P-256, P-384, and K-256 elliptic curves
- did:key: For testing and simple use cases, did:key identifiers provide self-contained key references
CID Verification#
- Always verify against CIDs: Signatures are over CID bytes, not the original record content
- Deterministic generation: The same record with the same
$sigmetadata always produces the same CID - Content integrity: Any modification to the record will produce a different CID and invalidate signatures
Metadata Validation#
When creating attestations:
- The
$typefield is always required in metadata to scope the attestation - The
repositoryfield is automatically added and must not be manually set - Custom metadata fields are preserved and included in CID calculation
- The
cidfield is automatically added to inline attestation metadata
Remote Attestation Considerations#
- Proof record storage: Store proof records in the attestor's repository with appropriate access controls
- CID matching: Verify that the CID in the proof record matches the computed CID of the attested content
- Record resolution: Use trusted record resolvers when fetching remote proof records
License#
MIT License