SIG Specification
The complete protocol specification for Signed Identity Graph.
SIG Specification
SIG v0.1
Signed Identity Graph (SIG) is a web-native protocol for organizations to publish signed relationship attestations (e.g., employee, founder, contractor, advisor) and revocations in an append-only event log.
This spec is designed to be:
- Verifiable (cryptographic signatures + key discovery)
- Revocable (relationship changes are explicit events)
- Web-native (
.well-known, HTTPS, JSON, JWKS) - Minimal but valuable (usable for graph sync and status checks on day one)
- Easy to implement (especially in Rust)
Table of Contents
- Overview
- Scope Boundary
- Goals and Non-Goals
- Terminology
- Protocol Model
- Discovery and Endpoints
- Data Model
- Event Types
- Signing and Verification
- Consumer State Derivation
- HTTP and Sync Semantics
- Privacy Model
- Conformance
- Phased Rollout Plan
- Implementation Steps
- Schemas and Example Code
- End-to-End Example
- Security Considerations
- Future Extensions
- Application and Multi-Issuer Chain (Informative)
- Deployment Modes (Informative)
- Local Authn/Authz Attestation Chain (Informative)
- Reference Web UI Addendum (Informative)
- Reference Chains: ID and AUTH (Informative)
Overview
Modern people/company graphs are often built from scraped bios, user-entered data, or inferred relationships. These graphs drift, become stale, and require continual manual maintenance.
SIG introduces a simple primitive:
Organizations publish signed relationship events.
Consumers can then verify and derive current state (active, revoked, expired) without scraping social sources.
Scope Boundary
This specification defines SIG Core only: low-level protocol mechanics for signed relationship events, discovery, verification, replay, and revocation.
This specification does not define product-level login UX, session management, SDK packaging, commercial APIs, or hosting offerings. Those belong to implementation chains (informative sections) and product documentation (for example Authkeep).
Goals and Non-Goals
Goals
- Cryptographically verifiable relationship records
- Append-only event log with deterministic replay
- Revocation-first design
- Public-safe (supports opt-in publication)
- Interoperable via standard web and JOSE/JWKS patterns
- Incrementally adoptable (can start with one org and one feed)
Non-Goals (v0.1)
- Zero-knowledge proofs / selective disclosure cryptography
- Cross-org federation semantics and trust scoring
- On-chain anchoring or blockchain dependencies
- Mandatory subject countersignatures
- Rich HR schema standardization
- Full DID method support beyond
did:web - A requirement to use any specific marketplace or hosted service
- Product-level login UX and session semantics
Terminology
Issuer
The organization publishing and signing SIG events.
Example: did:web:cowboy.com
Subject
The entity the relationship applies to (typically a person).
Examples:
did:key:z6Mk...did:web:alice.exampleacct:[email protected](allowed in v0.1, but DID is recommended)
Relationship
A typed attestation from issuer to subject, such as employee or contractor.
Event Log
An append-only sequence of signed events describing creation, updates, and revocations.
Registry Consumer
Any system that fetches SIG resources, verifies signatures, and derives state.
Principal
The root identity that owns long-term reputation and can delegate work to agents.
Agent (Delegate)
An autonomous or assisted actor that performs actions on behalf of a principal under signed delegation policy.
Verification Issuer
An independent issuer that publishes signed claims about identity or assurance level (for example human-verification status).
Protocol Model
SIG consists of four layers:
1. Identity Layer
The issuer has a canonical identifier (v0.1: did:web).
2. Trust/Key Layer
The issuer publishes a JWKS containing public signing keys.
3. Relationship Attestation Layer
The issuer emits signed events (relationship.upsert, relationship.revoke).
4. Distribution Layer
The issuer exposes a public event feed (JSONL/NDJSON) and metadata in .well-known URLs.
Design Principle
SIG treats relationships as time-varying claims, not static identity fields.
Discovery and Endpoints
For an issuer DID did:web:example.com, the issuer MUST host the following resources over HTTPS.
Required Endpoints
DID Document
https://example.com/.well-known/did.json
JWKS
https://example.com/.well-known/jwks.json
SIG Metadata
https://example.com/.well-known/sig.json
SIG Public Event Feed
https://example.com/.well-known/sig/events.jsonl
SIG Metadata (sig.json)
The SIG metadata document describes where the registry resources live and what protocol version is supported.
{
"spec_version": "sig/0.1",
"issuer": "did:web:example.com",
"jwks_uri": "https://example.com/.well-known/jwks.json",
"events_uri": "https://example.com/.well-known/sig/events.jsonl",
"public_only": true,
"algorithms_supported": ["EdDSA"],
"event_serialization": "jws-json-flattened+ndjson"
}
SIG Metadata Fields
spec_version(string, REQUIRED) — protocol version string (v0.1:sig/0.1)issuer(string, REQUIRED) — issuer DIDjwks_uri(string, REQUIRED) — HTTPS URL for public keysevents_uri(string, REQUIRED) — HTTPS URL for public event feedpublic_only(boolean, REQUIRED) — whether the feed is only public assertionsalgorithms_supported(array[string], REQUIRED) — allowed signing algorithmsevent_serialization(string, OPTIONAL) — event line serialization format hint
Data Model
Common Event Payload Fields
All SIG events MUST include the following fields in the payload.
{
"spec_version": "sig/0.1",
"event_id": "evt_01JXYZ...",
"event_type": "relationship.upsert",
"issuer": "did:web:cowboy.com",
"issued_at": "2026-02-26T23:00:00Z",
"sequence": 42,
"relationship_id": "rel_01JXYZ...",
"subject": "did:key:z6Mkabc123...",
"visibility": "public"
}
Field Definitions
spec_version(string, REQUIRED)- MUST equal
sig/0.1for this spec version.
- MUST equal
event_id(string, REQUIRED)- Globally unique event identifier.
- SHOULD be stable and collision-resistant (e.g., ULID/UUIDv7).
event_type(string, REQUIRED)- Event type identifier.
- v0.1 requires
relationship.upsertandrelationship.revoke.
issuer(string, REQUIRED)- Issuer DID.
- MUST match SIG metadata
issuer.
issued_at(string, REQUIRED)- RFC3339 timestamp in UTC.
sequence(integer, REQUIRED)- Monotonic sequence number for the public feed.
- MUST be strictly increasing and unique.
relationship_id(string, REQUIRED)- Stable identifier for a logical relationship record.
subject(string, REQUIRED)- Subject identifier (recommended: DID).
visibility(string, REQUIRED)- One of:
public,private - Public feed entries MUST use
public.
- One of:
Event Types
relationship.upsert (REQUIRED)
Creates or updates a relationship record and sets it active.
Additional Required Fields
relationship_type(string)status(string, MUST beactivein v0.1 upsert events)roles(array[string])valid_from(RFC3339 string ornull)valid_until(RFC3339 string ornull)
Optional Fields
display(object) — public hints (title, department, label)reason(string)metadata(object) — issuer-defined, non-normative extras
Allowed relationship_type Values (v0.1)
employeefoundercontractoradvisorinvestoradmin_delegateother
Example
{
"spec_version": "sig/0.1",
"event_id": "evt_01JXYZ1",
"event_type": "relationship.upsert",
"issuer": "did:web:cowboy.com",
"issued_at": "2026-02-26T23:00:00Z",
"sequence": 1,
"relationship_id": "rel_01JXYZA",
"subject": "did:key:z6Mkabc123...",
"visibility": "public",
"relationship_type": "employee",
"status": "active",
"roles": ["engineering"],
"valid_from": "2026-02-01T00:00:00Z",
"valid_until": null,
"display": {
"title": "Software Engineer",
"department": "Engineering"
}
}
relationship.revoke (REQUIRED)
Revokes a relationship record (or marks it ended), preserving audit history.
Additional Required Fields
revokes_relationship_id(string)reason_code(string)effective_at(RFC3339 UTC string)
Optional Fields
reason(string)metadata(object)
Recommended reason_code Values
employment_endedcontract_endedpermission_revokedsupersededadmin_actionerror_correctionother
Example
{
"spec_version": "sig/0.1",
"event_id": "evt_01JXYZ2",
"event_type": "relationship.revoke",
"issuer": "did:web:cowboy.com",
"issued_at": "2026-08-30T18:20:00Z",
"sequence": 2,
"relationship_id": "rel_01JXYZA",
"revokes_relationship_id": "rel_01JXYZA",
"subject": "did:key:z6Mkabc123...",
"visibility": "public",
"reason_code": "employment_ended",
"effective_at": "2026-08-30T18:00:00Z",
"reason": "Offboarded"
}
Optional v0.1 Event Types (MAY implement)
relationship.visibility
Adjusts visibility for a relationship record (primarily useful in private/internal feeds; public feed only carries public assertions).
relationship.reinstate
Reactivates a previously revoked relationship record.
Note: v0.1 public feed baseline does not require these events.
Signing and Verification
SIG v0.1 uses JWS JSON Flattened Serialization for each event line.
Each line of events.jsonl MUST be a JSON object with:
protectedpayloadsignature
Event Envelope Example (JWS Flattened)
{
"protected": "eyJhbGciOiJFZERTQSIsImtpZCI6Im9yc2lnbi0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9",
"payload": "eyJzcGVjX3ZlcnNpb24iOiJvcnIvMC4xIiwiZXZlbnRfaWQiOiJldnRfMDFKWFlaMSIsLi4ufQ",
"signature": "L6g6...snip..."
}
Protected Header Requirements
The protected header MUST include:
alg— v0.1 requiresEdDSAkid— key identifier matching a JWK in the issuer JWKStyp— MUST besig-event+jws
Consumers MUST reject:
alg: none- unsupported algorithms
- malformed base64url fields
JWKS Requirements
Issuer MUST publish signing public keys in JWKS format.
Example Ed25519 JWK:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "orgsign-1",
"use": "sig",
"alg": "EdDSA",
"x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ"
}
]
}
Verification Procedure (Consumer)
For each event line:
- Parse JWS flattened envelope.
- Decode protected header and payload (base64url).
- Check
algis allowed (EdDSA). - Resolve
kidin issuer JWKS. - Verify JWS signature.
- Parse payload JSON.
- Validate schema and issuer consistency.
- Validate sequence ordering.
- Apply reducer/state transition.
Issuer Consistency Rules
- Payload
issuerMUST equal SIG metadataissuer. - For
did:web, the DID domain MUST match the HTTPS host serving SIG resources.
Consumer State Derivation
Consumers reconstruct current registry state by replaying verified events in ascending sequence order.
Ordering Rules
- Primary ordering key:
sequenceascending - Duplicate
sequencevalues: feed MUST be treated as invalid - Gaps in sequence values: consumer SHOULD treat feed as incomplete and retry/fetch again
Derived Relationship State
For each relationship_id, a consumer may derive:
activerevokedexpired(derived ifvalid_untilhas passed and not revoked)
Reduction Rules (v0.1)
On relationship.upsert
- Create or replace current relationship attributes for
relationship_id - Set derived status to
active(subject to expiration checks)
On relationship.revoke
- Mark relationship as
revoked - Record
effective_at,reason_code, and optionalreason
Expiration
If current time > valid_until and relationship is not revoked, derived status becomes expired.
Public Feed Absence Rule
Absence in the public SIG feed is not proof of non-membership.
Public feeds may omit:
- private relationships
- non-consenting subjects
- internal-only roles
Decision Checks (App-Facing)
Consumers MAY expose an app-facing decision helper (CLI/API) that answers:
"Given a
subjectand required predicates, should access be allowed?"
Recommended evaluation flow:
- Verify metadata, JWKS, and all feed signatures.
- Replay feed and derive current relationship state.
- Filter relationships where
subjectmatches exactly. - Only consider relationships with derived status
active. - Evaluate required predicates (for example
relationship=employee,role=engineering). - Return
allowif any active relationship satisfies all predicates; otherwisedeny.
A CLI surface for this helper can be:
ak check <sig_metadata> --subject <did> --require relationship=employee --require role=engineeringak check ... --explainfor human-readable debugging output
Recommended CLI exit behavior:
0=allow1=deny2= verification/input/runtime failure
HTTP and Sync Semantics
Content Types
Issuers SHOULD use:
application/jsonforsig.jsonapplication/jwk-set+jsonorapplication/jsonforjwks.jsonapplication/x-ndjsonforevents.jsonl
Caching
Issuers SHOULD support:
ETagLast-ModifiedCache-Control
Consumers SHOULD use conditional requests (If-None-Match, If-Modified-Since) for efficient syncing.
Recommended Consumer Sync Loop
- Fetch SIG metadata (
sig.json) - Fetch JWKS (
jwks.json) - Fetch event feed (
events.jsonl) - Verify and replay all events
- Persist derived state + latest
sequence - Re-sync on interval or cache invalidation
Partial Failure Handling
Consumers SHOULD:
- fail closed on signature verification failure for an event line
- log and quarantine malformed lines
- retry on sequence gaps before marking feed invalid
- preserve last known-good state if a fresh fetch fails
Privacy Model
SIG v0.1 defines a simple visibility model:
publicprivate
Public Feed Semantics
The public feed contains only events where visibility = public.
Internal Registry Support
An issuer MAY maintain a superset internal registry for:
- private employees
- contractors
- internal revocations
- compliance and security automation
The same event model can power both public and private registries.
SIG v0.1 standardizes the public feed baseline. Private-feed auth and transport are out of scope.
Conformance
Machine-Tracked Requirement IDs (SIG Core v0.1)
The following requirement IDs are the normative baseline used for automated conformance checks in this repository.
| Requirement ID | Level | Requirement | Spec section |
|---|---|---|---|
SIG-DISC-001 |
MUST | Issuer hosts /.well-known/sig.json over HTTPS. |
Discovery and Endpoints |
SIG-DISC-002 |
MUST | Issuer hosts /.well-known/jwks.json over HTTPS. |
Discovery and Endpoints |
SIG-DISC-003 |
MUST | Issuer hosts /.well-known/sig/events.jsonl over HTTPS. |
Discovery and Endpoints |
SIG-EVT-001 |
MUST | Event payload spec_version equals sig/0.1. |
Data Model |
SIG-EVT-002 |
MUST | Event payload issuer equals SIG metadata issuer. |
Signing and Verification |
SIG-EVT-003 |
MUST | Event sequence is strictly increasing and unique in feed order. |
Data Model / Signing and Verification |
SIG-EVT-004 |
MUST | Public feed entries use visibility = public. |
Data Model / Privacy Model |
SIG-EVT-005 |
MUST | relationship.upsert uses status = active in v0.1. |
Event Types |
SIG-JWS-001 |
MUST | JWS protected typ equals sig-event+jws. |
Signing and Verification |
SIG-JWS-002 |
MUST | Consumers reject alg=none and unsupported algorithms. |
Signing and Verification / Security Considerations |
SIG-JWS-003 |
MUST | JWS kid resolves to a signing key in issuer JWKS. |
Signing and Verification |
SIG-SEQ-001 |
MUST | Consumers validate sequence monotonicity and reject sequence integrity violations. | Signing and Verification / Security Considerations |
SIG-STATE-001 |
MUST | Consumers derive revocation state from relationship.revoke events. |
Consumer State Derivation |
SIG-AUTH-001 |
MUST | SPEC includes auth challenge schema fields: challenge_id, audience, requested_scopes, nonce, issued_at, expires_at. |
Local Authn/Authz Attestation Chain (Informative) |
SIG-AUTH-002 |
MUST | SPEC includes auth attestation schema fields: challenge_id, subject, audience, approved_scopes, nonce, issued_at, expires_at. |
Local Authn/Authz Attestation Chain (Informative) |
SIG-AUTH-003 |
MUST | SPEC states passkeys are authenticators for a stable principal, not the principal identifier. | Local Authn/Authz Attestation Chain (Informative) |
SIG-AUTH-004 |
MUST | Repository includes a reference authn demo doc with ak init, ak auth inspect, ak auth attest, and ak auth verify flow. |
Local Authn/Authz Attestation Chain (Informative) |
SIG-AUTH-005 |
MUST | SPEC includes a Reference Web UI addendum for "Login with SIG protocol", repository includes docs/demo-auth-web-ui.md plus runnable crates/authkeep-cli/web-ui/, ak web-ui exports that UI, and authkeep-web serves it with reference auth endpoints. |
Reference Web UI Addendum (Informative) |
Repository conformance artifacts are tracked in:
conformance/requirements.yamlscripts/conformance-check.shdocs/conformance-report.md(generated)
SIG v0.1 Public Feed Compatible (Issuer)
An issuer implementation is SIG v0.1 Public Feed Compatible if it supports all of the following:
did:webissuer identifier/.well-known/sig.json/.well-known/jwks.json/.well-known/sig/events.jsonl- Ed25519 signatures in JWS (
EdDSA) relationship.upsertrelationship.revoke- Strictly monotonic
sequence - Public-only feed entries (
visibility = public)
SIG v0.1 Public Feed Compatible (Consumer)
A consumer implementation is SIG v0.1 Public Feed Compatible if it can:
- Discover SIG metadata and JWKS
- Verify JWS signatures using issuer JWKS
- Validate payload fields and issuer consistency
- Replay events in sequence order
- Derive current relationship state (
active,revoked,expired) - Ignore unknown event types safely (forward-safe evolution)
Phased Rollout Plan
This section defines a practical delivery roadmap. Each phase adds value while minimizing implementation risk.
Phase 0 — Prototype (Single-Org, Local)
Goal: Prove the model and reducer logic.
Scope
- Static local JWKS
- Locally signed sample events
- No HTTP server required (files only)
- One consumer reducer implementation
Deliverables
- Event payload structs
- JWS sign/verify functions
- Reducer deriving current state
- Golden test vectors
Success Criteria
- Given a feed of signed events, consumer derives deterministic final state
- Revocation updates are reflected correctly
Phase 1 — Public Feed MVP (Web-Native)
Goal: Publish a real SIG public feed from a domain.
Scope
did:webissuer.well-known/sig.json.well-known/jwks.json.well-known/sig/events.jsonl- Ed25519 signing
relationship.upsert+relationship.revoke
Deliverables
- Issuer server or static site output pipeline
- Consumer verifier library / CLI
- Feed sync and replay
Success Criteria
- Third-party verifier can discover, verify, and replay the feed over HTTPS
- Key rotation plan documented
Phase 2 — Operator UX and Internal Pipeline
Goal: Make issuance and revocation operationally useful.
Scope
- Admin tooling (CLI or internal dashboard)
- Event append workflow with audit logs
- Basic validation before signing (schema + policy)
- Optional import/export from HR/IT systems
Deliverables
- Append-only writer tool
- Role/status templates
- Sequence management and locking
Success Criteria
- Operators can issue and revoke without manual JSON editing
- Feed remains valid under concurrent updates (via serialization/locking)
Phase 3 — Ecosystem Integrations
Goal: Make SIG useful to graph apps, access systems, and communities.
Scope
- SDKs (Rust/TypeScript)
- Webhook or poller-based sync integrations
- Graph visualization consumption
- Status-check API wrappers
Deliverables
is_active_relationship(subject, issuer, relationship_type)helper- Indexer/cache service
- Example integrations (community gating, people graph sync)
Success Criteria
- At least one external app consumes SIG feed successfully
- Revocations propagate within acceptable sync windows
Phase 4 — Advanced Trust Features (Post-v0.1)
Goal: Expand privacy and interoperability.
Scope (future)
- Subject countersignatures / acceptance
- Private feed authentication chain
- Incremental feed pagination
- Merkle checkpoints
- Rich relationship namespaces
Deliverables
- SIG v0.2 proposal drafts
- Migration guidance from v0.1
Implementation Steps
This section gives a concrete build sequence for a Rust implementation.
Step 1 — Define Core Types
Implement payload structs and enums:
EventTypeRelationshipTypeVisibilityCommonFieldsUpsertEventRevokeEventSignedEnvelope
Step 2 — Implement JWS Sign/Verify
- Generate Ed25519 keypair
- Emit JWK/JWKS with
kid - Sign payload bytes into JWS flattened JSON
- Verify envelope using JWKS-resolved key
Step 3 — Implement Reducer
Input: Vec<VerifiedEvent>
Output:
HashMap<relationship_id, DerivedRelationshipState>last_sequence
Reducer must enforce:
- monotonic sequences
- event-type validation
- relationship transition rules
Step 4 — Add Discovery Artifacts
Serve or generate:
.well-known/sig.json.well-known/jwks.json.well-known/sig/events.jsonl
Step 5 — Build Consumer CLI
Commands (example):
ak verify https://cowboy.com/.well-known/sig.jsonak dump-state https://cowboy.com/.well-known/sig.jsonak check https://cowboy.com/.well-known/sig.json --subject did:key:z6MkAlice --require relationship=employee --require role=engineeringak check https://cowboy.com/.well-known/sig.json --subject did:key:z6MkAlice --require relationship=employee --require role=engineering --explain
Step 6 — Add Operational Safety
- file locking / transactional append
- schema validation before signing
- duplicate
event_iddetection - sequence consistency checks
Step 7 — Add Key Rotation Process
- publish new key in JWKS before use
- sign new events with new
kid - retain old keys long enough for historical verification
Schemas and Example Code
JSON Schema (Illustrative) — Common Base
These schemas are illustrative and may be split per event type in implementation.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/sig-event-base.json",
"title": "SIG Event Base",
"type": "object",
"required": [
"spec_version",
"event_id",
"event_type",
"issuer",
"issued_at",
"sequence",
"relationship_id",
"subject",
"visibility"
],
"properties": {
"spec_version": { "type": "string", "const": "sig/0.1" },
"event_id": { "type": "string", "minLength": 1 },
"event_type": { "type": "string", "minLength": 1 },
"issuer": { "type": "string", "minLength": 1 },
"issued_at": { "type": "string", "format": "date-time" },
"sequence": { "type": "integer", "minimum": 1 },
"relationship_id": { "type": "string", "minLength": 1 },
"subject": { "type": "string", "minLength": 1 },
"visibility": {
"type": "string",
"enum": ["public", "private"]
}
},
"additionalProperties": true
}
JSON Schema (Illustrative) — relationship.upsert
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/sig-relationship-upsert.json",
"allOf": [
{ "$ref": "sig-event-base.json" },
{
"type": "object",
"required": [
"relationship_type",
"status",
"roles",
"valid_from",
"valid_until"
],
"properties": {
"event_type": { "const": "relationship.upsert" },
"relationship_type": {
"type": "string",
"enum": [
"employee",
"founder",
"contractor",
"advisor",
"investor",
"admin_delegate",
"other"
]
},
"status": { "type": "string", "const": "active" },
"roles": {
"type": "array",
"items": { "type": "string" }
},
"valid_from": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
]
},
"valid_until": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
]
},
"display": { "type": "object" },
"reason": { "type": "string" },
"metadata": { "type": "object" }
}
}
]
}
JSON Schema (Illustrative) — relationship.revoke
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/sig-relationship-revoke.json",
"allOf": [
{ "$ref": "sig-event-base.json" },
{
"type": "object",
"required": [
"revokes_relationship_id",
"reason_code",
"effective_at"
],
"properties": {
"event_type": { "const": "relationship.revoke" },
"revokes_relationship_id": { "type": "string", "minLength": 1 },
"reason_code": { "type": "string", "minLength": 1 },
"effective_at": { "type": "string", "format": "date-time" },
"reason": { "type": "string" },
"metadata": { "type": "object" }
}
}
]
}
Rust Example Types (Serde)
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Private,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EventType {
#[serde(rename = "relationship.upsert")]
RelationshipUpsert,
#[serde(rename = "relationship.revoke")]
RelationshipRevoke,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipType {
Employee,
Founder,
Contractor,
Advisor,
Investor,
AdminDelegate,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommonEventFields {
pub spec_version: String,
pub event_id: String,
pub event_type: String,
pub issuer: String,
pub issued_at: String,
pub sequence: u64,
pub relationship_id: String,
pub subject: String,
pub visibility: Visibility,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayHints {
pub title: Option<String>,
pub department: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipUpsertEvent {
#[serde(flatten)]
pub common: CommonEventFields,
pub relationship_type: RelationshipType,
pub status: String, // must be "active" in v0.1 validation
pub roles: Vec<String>,
pub valid_from: Option<String>,
pub valid_until: Option<String>,
pub display: Option<DisplayHints>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipRevokeEvent {
#[serde(flatten)]
pub common: CommonEventFields,
pub revokes_relationship_id: String,
pub reason_code: String,
pub effective_at: String,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwsFlattenedEnvelope {
pub protected: String,
pub payload: String,
pub signature: String,
}
Rust Example Reducer State
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum DerivedStatus {
Active,
Revoked,
Expired,
}
#[derive(Debug, Clone)]
pub struct DerivedRelationshipState {
pub issuer: String,
pub relationship_id: String,
pub subject: String,
pub relationship_type: String,
pub roles: Vec<String>,
pub valid_from: Option<String>,
pub valid_until: Option<String>,
pub status: DerivedStatus,
pub revoked_reason_code: Option<String>,
pub revoked_effective_at: Option<String>,
pub last_sequence: u64,
}
#[derive(Debug, Default)]
pub struct FeedState {
pub by_relationship_id: HashMap<String, DerivedRelationshipState>,
pub last_sequence: u64,
}
Rust Example Validation/Replay Pseudocode
fn replay_event(feed: &mut FeedState, event: VerifiedEvent) -> Result<(), ReplayError> {
if event.sequence <= feed.last_sequence {
return Err(ReplayError::OutOfOrderSequence {
got: event.sequence,
last: feed.last_sequence,
});
}
match event.payload {
EventPayload::Upsert(up) => {
validate_upsert(&up)?;
let state = DerivedRelationshipState {
issuer: up.common.issuer.clone(),
relationship_id: up.common.relationship_id.clone(),
subject: up.common.subject.clone(),
relationship_type: format!("{:?}", up.relationship_type),
roles: up.roles.clone(),
valid_from: up.valid_from.clone(),
valid_until: up.valid_until.clone(),
status: DerivedStatus::Active,
revoked_reason_code: None,
revoked_effective_at: None,
last_sequence: up.common.sequence,
};
feed.by_relationship_id
.insert(up.common.relationship_id.clone(), state);
}
EventPayload::Revoke(rv) => {
validate_revoke(&rv)?;
if let Some(existing) = feed.by_relationship_id.get_mut(&rv.revokes_relationship_id) {
existing.status = DerivedStatus::Revoked;
existing.revoked_reason_code = Some(rv.reason_code.clone());
existing.revoked_effective_at = Some(rv.effective_at.clone());
existing.last_sequence = rv.common.sequence;
}
}
EventPayload::Unknown => {
// forward-safe evolution: ignore unknown events after signature verification
}
}
feed.last_sequence = event.sequence;
Ok(())
}
Example SIG Metadata Generation Snippet (Rust, serde_json)
use serde::Serialize;
#[derive(Serialize)]
struct SigMetadata<'a> {
spec_version: &'a str,
issuer: &'a str,
jwks_uri: &'a str,
events_uri: &'a str,
public_only: bool,
algorithms_supported: Vec<&'a str>,
event_serialization: &'a str,
}
fn build_metadata(domain: &str) -> SigMetadata<'_> {
SigMetadata {
spec_version: "sig/0.1",
issuer: "did:web:example.com", // replace with derived domain DID
jwks_uri: &format!("https://{domain}/.well-known/jwks.json"),
events_uri: &format!("https://{domain}/.well-known/sig/events.jsonl"),
public_only: true,
algorithms_supported: vec!["EdDSA"],
event_serialization: "jws-json-flattened+ndjson",
}
}
End-to-End Example
This section shows a complete mini flow with two public events.
Issuer
- Domain:
cowboy.com - DID:
did:web:cowboy.com - Signing key:
orgsign-1(Ed25519)
Event 1 — Alice active employee
Payload (before signing):
{
"spec_version": "sig/0.1",
"event_id": "evt_01A",
"event_type": "relationship.upsert",
"issuer": "did:web:cowboy.com",
"issued_at": "2026-02-26T23:00:00Z",
"sequence": 1,
"relationship_id": "rel_alice_emp",
"subject": "did:key:z6MkAlice",
"visibility": "public",
"relationship_type": "employee",
"status": "active",
"roles": ["engineering"],
"valid_from": "2026-02-01T00:00:00Z",
"valid_until": null,
"display": {"title": "Software Engineer"}
}
Event 2 — Alice employment ended
Payload (before signing):
{
"spec_version": "sig/0.1",
"event_id": "evt_01B",
"event_type": "relationship.revoke",
"issuer": "did:web:cowboy.com",
"issued_at": "2026-08-30T18:20:00Z",
"sequence": 2,
"relationship_id": "rel_alice_emp",
"revokes_relationship_id": "rel_alice_emp",
"subject": "did:key:z6MkAlice",
"visibility": "public",
"reason_code": "employment_ended",
"effective_at": "2026-08-30T18:00:00Z",
"reason": "Offboarded"
}
Consumer Result (Derived)
After replaying sequences 1 and 2:
{
"relationship_id": "rel_alice_emp",
"issuer": "did:web:cowboy.com",
"subject": "did:key:z6MkAlice",
"relationship_type": "employee",
"status": "revoked",
"revoked_reason_code": "employment_ended",
"revoked_effective_at": "2026-08-30T18:00:00Z"
}
Security Considerations
1. Algorithm Downgrade / alg: none
Consumers MUST reject:
alg: none- unsupported algorithms
- unexpected JWS header values
2. Key Rotation
Issuers SHOULD:
- publish new keys in JWKS before using them
- retain retired keys temporarily to preserve historical verification
- maintain clear
kidnaming and lifecycle procedures
3. Sequence Integrity
Duplicate or non-monotonic sequence numbers can corrupt replay determinism. Consumers MUST validate sequence monotonicity.
4. Feed Incompleteness
Sequence gaps may indicate caching issues or tampering. Consumers SHOULD retry and mark feed incomplete if gaps persist.
5. Timestamp Handling
Consumers SHOULD:
- parse timestamps strictly as RFC3339 UTC
- apply bounded clock skew tolerance
- reject obviously invalid future timestamps per policy
6. DID/Domain Binding (did:web)
Consumers SHOULD verify that:
- SIG resources are fetched from the domain implied by
did:web - metadata
issuermatches expected DID host
7. Privacy Leakage
Even public assertions may reveal sensitive organizational information. Issuers should establish publication policy and subject consent processes before exposing relationships.
Future Extensions
The following are intentionally deferred to SIG v0.2+.
Subject Countersignatures / Acceptance
Allow subjects to acknowledge or link the relationship claim to their own identity.
Private Feed Authentication Chain
Standardized authenticated transport for internal-only SIG feeds.
Incremental Sync / Pagination
Range-based endpoints or snapshot + delta feeds for large registries.
Merkle Checkpoints
Efficient inclusion proofs and tamper-evidence for large feeds.
Rich Relationship Namespaces
Standardized subtypes such as:
employment.full_timeemployment.part_timeemployment.interncontractor.vendor
Delegated Signing Policies
Key purpose declarations (e.g., HR keys vs IT keys) and policy-aware verification.
Application and Multi-Issuer Chain (Informative)
This section describes optional application-layer chains built on top of SIG core primitives (identity, signed events, verification, replay, and revocation).
Marketplace and reputation systems are one chain, not the protocol itself.
Chain Example: Marketplace Trust Network
SIG Network Model
- Any organization can run an SIG issuer and publish signed feeds.
- Authkeep MAY run a hosted implementation ("cSIG"), but protocol control remains decentralized.
- Consumers compose trust from multiple issuers according to local policy.
Issuer Classes
Typical issuer roles in this chain:
- Marketplace issuer (example:
did:web:marketplace.authkeep.com)- Publishes signed outcome and reputation claims from completed jobs.
- Human verification issuer (third-party API provider)
- Publishes signed personhood/verification claims.
- Domain issuer (employer/community/platform)
- Publishes domain-specific trust claims (employment, moderation standing, skill attestations).
Principal and Agent Delegation
- A human principal SHOULD have a stable subject identifier (for example DID or account DID).
- Autonomous agents MAY operate as delegates of a principal.
- Delegation SHOULD be explicit, signed, revocable, and time-bounded.
- Reputation rollup policy SHOULD define whether agent outcomes accrue to principal, agent, or both.
Marketplace Outcome Claims
An interoperable marketplace chain SHOULD include verifiable outcomes, such as:
job.postedescrow.fundedjob.submittedjob.verifiedjob.rejecteddispute.openeddispute.resolvedescrow.releasedescrow.refunded
These event names are chain-level conventions, not required SIG v0.1 core events.
Multi-Issuer Trust Policy
Consumers SHOULD evaluate claims through an explicit trust policy:
- trusted issuer allowlist (by DID)
- issuer weights by claim namespace/type
- minimum issuer diversity for high-risk decisions
- claim freshness windows
- revocation precedence
- negative-event handling and decay policy
A relying party MUST keep final decision authority local. SIG provides signed evidence, not global truth.
Sybil Resistance Controls
No single control is sufficient. Effective deployments SHOULD combine:
- economic friction (stake/bond/deposit requirements)
- slashing/penalties for proven bad behavior
- issuer diversity thresholds
- account maturity and rate limits for new identities
- challenge/audit workflows
- decayed scoring (recent verified outcomes weighted higher)
Payment Rails Guidance
For escrow-based marketplaces, a narrow payment rail chain is recommended:
- one chain
- one stablecoin denomination for escrow accounting
- deterministic settlement events linked to job outcomes
Deployment Modes (Informative)
SIG supports multiple operating modes:
Public Hosted Registry
- Multi-tenant service operated by a platform provider.
- Fastest onboarding for early issuers and relying parties.
- Suitable for open communities and public trust signals.
Private Cloud
- Single-tenant managed deployment in customer cloud account.
- Suitable for regulated workflows and enterprise data controls.
On-Premises
- Customer-operated deployment in private infrastructure.
- Suitable where internet exposure, vendor custody, or SaaS is disallowed.
Shared Interoperability Requirements
Across all deployment modes, interoperability depends on:
- stable issuer DID + HTTPS hosting
- published JWKS and SIG metadata
- strict signature and sequence verification
- explicit trust policy configuration in consumers
Local Authn/Authz Attestation Chain (Informative)
This chain describes local-first authentication and authorization where users/bots run SIG clients and sign explicit permission attestations for relying sites.
This chain describes a recommended end-user authn/z experience: install ak, create a principal, review permission challenges, sign attestations, and use them across sites.
Implementation plan for this chain:
docs/core-authn-passkey-plan.mddocs/demo-authn-passkey.md(reference flow)docs/demo-auth-web-ui.md(reference web UI)crates/authkeep-cli/web-ui/(runnable reference app inak)
Chain Terms
- Local SIG Principal: local identity rooted in user-controlled keys (for example under
~/.ak) - Auth Attestation Challenge: relying-party nonce-bound request with audience/scopes/expiry
- Auth Attestation: signed user response approving or denying requested scopes
Entity Model: Multi-Key Principal (Informative)
For browser login and operational resilience, a principal SHOULD be treated as an entity with multiple authenticators and recovery paths, not as a single-device key.
Recommended model:
- Stable principal identifier (for example
did:webor account DID) - Multiple verification methods (device keys, browser passkeys, hardware keys, recovery keys)
- Rotation and recovery policy (for example threshold approvals, delegated recovery issuer, time delays)
Passkey compatibility guidance:
- Browser passkeys (WebAuthn) are authenticator methods that can prove control for a principal
- A passkey credential ID is not the long-term principal identifier
- Relying parties SHOULD bind authenticated passkey sessions to a stable SIG principal identifier
- Compromised or retired authenticators SHOULD be revocable without changing principal identity
Vision
- A principal runs
ak initlocally. - Keys are generated and managed in a local SIG home directory (for example
~/.ak). - A relying site presents a challenge ("Login with SIG protocol").
- The principal reviews requested permissions locally and signs an attestation.
- The site verifies the attestation and establishes a session.
Reference Interaction (Out-of-Band Copy/Paste)
- Relying site generates a challenge payload and displays/copies it.
- Principal runs local CLI command with the challenge string.
- CLI shows:
- requesting site identity
- requested permissions/scopes
- nonce and expiration
- Principal approves/denies; CLI produces signed attestation.
- Principal pastes attestation back to site.
- Site verifies signature, nonce, audience, and expiry; on success, login/session is created.
- Principal and site may both persist attestation for audit/replay defense.
Reference CLI Shape (Illustrative)
The exact command names are implementation-defined, but a practical local UX commonly includes:
- principal bootstrap (for example
ak init) - challenge inspection (for example
ak auth inspect <challenge>) - challenge signing with user approval (for example
ak auth attest <challenge>) - attestation verification (for example
ak auth verify <attestation>)
Challenge Payload (Illustrative)
{
"type": "sig.auth.challenge",
"challenge_id": "chal_01J...",
"audience": "did:web:bot.inc",
"requested_scopes": ["jobs:apply", "identity:read"],
"nonce": "c26f4e4f...",
"issued_at": "2026-02-27T12:00:00Z",
"expires_at": "2026-02-27T12:05:00Z"
}
Attestation Payload (Illustrative)
{
"type": "sig.auth.attestation",
"challenge_id": "chal_01J...",
"subject": "did:key:z6MkAlice",
"audience": "did:web:bot.inc",
"approved_scopes": ["jobs:apply", "identity:read"],
"nonce": "c26f4e4f...",
"issued_at": "2026-02-27T12:01:00Z",
"expires_at": "2026-02-27T12:31:00Z"
}
Verification Requirements
A relying party SHOULD verify:
- signature validity against subject key
- challenge binding (
challenge_id,nonce) - audience binding (site DID/domain)
- expiration windows
- scope minimization and policy compliance
- single-use semantics for nonce/challenge where required
Security Notes
- Challenges MUST be short-lived and nonce-bound.
- Attestations SHOULD be audience-scoped and time-limited.
- CLI UX SHOULD show clear site identity and permissions to reduce phishing risk.
- Sites SHOULD maintain replay-protection state for challenge IDs/nonces.
Reference Web UI Addendum (Informative)
This addendum captures a reference browser UX for "Login with SIG protocol". It is intended to make the protocol immediately understandable and testable for product teams.
Reference source in repository:
docs/demo-auth-web-ui.mdcrates/authkeep-cli/web-ui/crates/authkeep-web/(reference server)
UI Goals
- Make request origin and requested scopes explicit before approval
- Keep protocol objects inspectable (challenge and attestation text)
- Support fallback copy/paste flow even before deep browser wallet integration
Minimum Screens
Login with SIG protocolbutton on relying site- Challenge display/copy view
- Attestation paste/submit view
- Verification result/session confirmation view
Minimum Verification Behavior
- verify signature against principal key material
- verify
challenge_idandnoncebinding - verify
audiencebinding - verify expiry windows
- enforce approved scope policy
Passkey Alignment
- passkeys MAY be used as authenticators in the web flow
- passkeys do not replace stable principal identity
- revoking/rotating authenticators should not require principal ID change
Reference Chains: ID and AUTH (Informative)
This section defines two minimal reference chains that are intentionally simple, CLI-first, and suitable for end-to-end revocation testing.
These chains are not SIG Core normative requirements. They are implementation conventions built on top of SIG Core events.
Chain 1: ID
Purpose:
- publish identity assurance claims for a subject
- support clear allow/deny checks
- support immediate revocation
Chain convention:
relationship_type = "id"roleshold assurance tags (for examplehuman,email_verified,kyc_l1)
CLI flow (illustrative):
ak append-upsert \
--events-path /tmp/sig-demo/.well-known/sig/events.jsonl \
--issuer did:web:test.example \
--event-id evt_id_001 \
--relationship-id rel_id_alice \
--subject did:key:z6MkAlice \
--relationship-type id \
--roles human,email_verified \
--issued-at 2026-02-27T12:00:00Z \
--kid orgsign-1 \
--seed-hex <seed_hex>
ak check \
--sig /tmp/sig-demo/.well-known/sig.json \
--subject did:key:z6MkAlice \
--require relationship=id \
--require role=human
Revocation:
ak append-revoke \
--events-path /tmp/sig-demo/.well-known/sig/events.jsonl \
--issuer did:web:test.example \
--event-id evt_id_002 \
--relationship-id rel_id_alice \
--subject did:key:z6MkAlice \
--reason-code identity_revoked \
--issued-at 2026-02-27T13:00:00Z \
--effective-at 2026-02-27T13:00:00Z \
--kid orgsign-1 \
--seed-hex <seed_hex>
Expected behavior:
- before revoke:
checkallows - after revoke:
checkdenies
Chain 2: AUTH
Purpose:
- publish authorization grants for a relying site or app domain
- model permissions as roles/scopes
- support explicit revocation
Chain convention:
relationship_type = "auth"roleshold permission scope strings (for examplebot.inc:login,jobs:apply)
CLI flow (illustrative):
ak append-upsert \
--events-path /tmp/sig-demo/.well-known/sig/events.jsonl \
--issuer did:web:test.example \
--event-id evt_auth_001 \
--relationship-id rel_auth_alice_botinc \
--subject did:key:z6MkAlice \
--relationship-type auth \
--roles bot.inc:login,jobs:apply \
--issued-at 2026-02-27T12:05:00Z \
--kid orgsign-1 \
--seed-hex <seed_hex>
ak check \
--sig /tmp/sig-demo/.well-known/sig.json \
--subject did:key:z6MkAlice \
--require relationship=auth \
--require role=bot.inc:login
Revocation:
ak append-revoke \
--events-path /tmp/sig-demo/.well-known/sig/events.jsonl \
--issuer did:web:test.example \
--event-id evt_auth_002 \
--relationship-id rel_auth_alice_botinc \
--subject did:key:z6MkAlice \
--reason-code auth_revoked \
--issued-at 2026-02-27T13:05:00Z \
--effective-at 2026-02-27T13:05:00Z \
--kid orgsign-1 \
--seed-hex <seed_hex>
Expected behavior:
- before revoke:
checkallows - after revoke:
checkdenies
Testing note:
- This repository includes CLI integration tests for both chains to ensure issue/revoke/check behavior stays stable.
Appendix A — Minimal Files Checklist
For an SIG v0.1 public issuer deployment, publish:
/.well-known/did.json/.well-known/jwks.json/.well-known/sig.json/.well-known/sig/events.jsonl
And ensure:
- all events are signed with JWS (
EdDSA) kidresolves in JWKSsequenceis strictly monotonic- payload
issuermatches metadataissuer
Appendix B — Suggested Repo Layout (Rust)
sig/
├── crates/
│ ├── authkeep-core/ # models, validation, reducer
│ ├── authkeep-jose/ # JWS/JWKS helpers
│ ├── authkeep-client/ # fetch + verify + replay
│ └── authkeep-server/ # axum server / static artifact generation
├── examples/
│ ├── sample-jwks.json
│ ├── sample-sig.json
│ └── sample-events.jsonl
├── tests/
│ ├── golden_vectors.rs
│ └── replay_semantics.rs
└── SPEC.md
Appendix C — Practical Adoption Notes
- Start with one relationship type (
employee) and revocations. - Keep subject IDs stable (prefer DIDs over app usernames).
- Treat SIG as a signed source of relationship truth, not a social feed.
- Let graph apps/indexers consume SIG instead of manually maintaining inferred edges.
This is the smallest thing that can work — and still be genuinely useful.
Appendix D — Golden Test Vectors (v0.1)
This appendix provides implementation-oriented test vectors for SIG v0.1 consumers and issuers.
These vectors are intended to help with:
- parser correctness
- schema validation
- reducer behavior
- sequence handling
- JWS/JWKS integration wiring
Important: The cryptographic values below are a mix of concrete examples and placeholders unless explicitly marked as “verified test vector.” For production-grade interop tests, generate and publish a canonical signed fixture set from a reference implementation.
D.1 Test Vector Scope
This appendix includes:
- A sample SIG metadata document (
sig.json) - A sample JWKS (
jwks.json) - Two event payloads (upsert + revoke)
- Example NDJSON JWS envelopes
- Expected derived state after replay
- Negative test cases (should fail)
D.2 Fixture 1 — SIG Metadata (sig.json)
{
"spec_version": "sig/0.1",
"issuer": "did:web:test.example",
"jwks_uri": "https://test.example/.well-known/jwks.json",
"events_uri": "https://test.example/.well-known/sig/events.jsonl",
"public_only": true,
"algorithms_supported": ["EdDSA"],
"event_serialization": "jws-json-flattened+ndjson"
}
Validation Expectations
A conforming consumer should:
- accept
spec_version = sig/0.1 - fetch
jwks_uriandevents_uri - require
issuerconsistency with event payloads
D.3 Fixture 2 — JWKS (jwks.json)
This fixture contains one Ed25519 signing key.
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "orgsign-test-1",
"use": "sig",
"alg": "EdDSA",
"x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ"
}
]
}
Validation Expectations
A conforming consumer should:
- resolve
kid = orgsign-test-1 - require
alg = EdDSA - reject events signed with unknown
kid
D.4 Fixture 3 — Unsiged Event Payloads (Canonical Business Payloads)
These payloads represent the semantic SIG events before JOSE wrapping.
D.4.1 Event Payload A — relationship.upsert
{
"spec_version": "sig/0.1",
"event_id": "evt_test_001",
"event_type": "relationship.upsert",
"issuer": "did:web:test.example",
"issued_at": "2026-02-26T23:00:00Z",
"sequence": 1,
"relationship_id": "rel_alice_emp_001",
"subject": "did:key:z6MkAliceTest",
"visibility": "public",
"relationship_type": "employee",
"status": "active",
"roles": ["engineering", "backend"],
"valid_from": "2026-02-01T00:00:00Z",
"valid_until": null,
"display": {
"title": "Software Engineer",
"department": "Engineering"
}
}
Expected Validation Result
- Pass schema validation
- Pass semantic validation (
status == activefor upsert) - Pass reducer application as active relationship
D.4.2 Event Payload B — relationship.revoke
{
"spec_version": "sig/0.1",
"event_id": "evt_test_002",
"event_type": "relationship.revoke",
"issuer": "did:web:test.example",
"issued_at": "2026-08-30T18:20:00Z",
"sequence": 2,
"relationship_id": "rel_alice_emp_001",
"revokes_relationship_id": "rel_alice_emp_001",
"subject": "did:key:z6MkAliceTest",
"visibility": "public",
"reason_code": "employment_ended",
"effective_at": "2026-08-30T18:00:00Z",
"reason": "Offboarded"
}
Expected Validation Result
- Pass schema validation
- Pass semantic validation (
revokes_relationship_idmatches target relationship) - Pass reducer application as revoked relationship
D.5 Fixture 4 — Example JWS Flattened Envelopes (events.jsonl)
This section shows NDJSON lines of JWS flattened envelopes. These are structurally valid examples for parser/transport tests.
For signature-verification tests, replace placeholder
payloadandsignaturevalues with values generated by a reference signing tool using the private key corresponding toorgsign-test-1.
D.5.1 NDJSON Line 1 (Upsert)
{"protected":"eyJhbGciOiJFZERTQSIsImtpZCI6Im9yZ3NpZ24tdGVzdC0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9","payload":"<base64url-of-event-payload-A>","signature":"<base64url-signature-A>"}
D.5.2 NDJSON Line 2 (Revoke)
{"protected":"eyJhbGciOiJFZERTQSIsImtpZCI6Im9yZ3NpZ24tdGVzdC0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9","payload":"<base64url-of-event-payload-B>","signature":"<base64url-signature-B>"}
Combined events.jsonl Example
{"protected":"eyJhbGciOiJFZERTQSIsImtpZCI6Im9yZ3NpZ24tdGVzdC0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9","payload":"<base64url-of-event-payload-A>","signature":"<base64url-signature-A>"}
{"protected":"eyJhbGciOiJFZERTQSIsImtpZCI6Im9yZ3NpZ24tdGVzdC0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9","payload":"<base64url-of-event-payload-B>","signature":"<base64url-signature-B>"}
Parser Expectations
A conforming consumer should:
- parse each line independently as JSON
- require
protected,payload,signaturefields - tolerate trailing newline at EOF
- fail on malformed JSON line (policy-dependent: hard fail vs quarantine)
D.6 Expected Derived State After Replay
Given Event A then Event B (sequences 1 → 2), the consumer should derive:
{
"last_sequence": 2,
"by_relationship_id": {
"rel_alice_emp_001": {
"issuer": "did:web:test.example",
"relationship_id": "rel_alice_emp_001",
"subject": "did:key:z6MkAliceTest",
"relationship_type": "employee",
"roles": ["engineering", "backend"],
"valid_from": "2026-02-01T00:00:00Z",
"valid_until": null,
"status": "revoked",
"revoked_reason_code": "employment_ended",
"revoked_effective_at": "2026-08-30T18:00:00Z",
"last_sequence": 2
}
}
}
Semantics Checklist
- Upsert creates active relationship
- Revoke transitions same
relationship_idto revoked last_sequenceadvances to 2rolesand relationship metadata are preserved from latest upsert
D.7 Negative Test Cases (Should Fail)
These vectors are useful for validator and reducer robustness tests.
D.7.1 Duplicate Sequence Number
Two validly signed events both use sequence = 2.
Expected result:
- Consumer rejects feed as invalid (duplicate sequence)
D.7.2 Sequence Gap
Feed contains sequence 1, then 3 (missing 2).
Expected result:
- Consumer marks feed incomplete and retries (SHOULD)
- Consumer MAY refuse final state publication until gap resolved
D.7.3 Unknown kid
JWS header uses kid = does-not-exist.
Expected result:
- Signature verification fails
- Event must not be replayed
D.7.4 Unsupported Algorithm
JWS header uses alg = HS256 or alg = none.
Expected result:
- Event rejected before replay
D.7.5 Issuer Mismatch
Payload issuer = did:web:evil.example while metadata issuer is did:web:test.example.
Expected result:
- Event rejected (issuer consistency failure)
D.7.6 Invalid Upsert Status
relationship.upsert payload contains status = "revoked".
Expected result:
- Schema/semantic validation failure (v0.1 requires
status = activefor upsert)
D.7.7 Public Feed Contains Private Event
Payload has visibility = "private" but appears in public events.jsonl.
Expected result:
- Consumer rejects event from public feed (strict mode)
- or ignores event and logs policy violation (lenient mode)
D.8 Suggested Test Harness Structure (Rust)
A practical Rust test harness can split tests into three layers:
- Schema tests
- deserialize + validate payload fields
- JOSE tests
- verify JWS envelope using JWKS
- Replay tests
- feed ordered verified events into reducer and compare derived state
Example Rust Test Skeleton
#[test]
fn replay_upsert_then_revoke_yields_revoked_state() {
let metadata = load_test_metadata();
let jwks = load_test_jwks();
let lines = load_test_events_jsonl();
let verified_events = lines
.iter()
.map(|line| verify_and_decode(line, &metadata, &jwks).unwrap())
.collect::<Vec<_>>();
let mut state = FeedState::default();
for ev in verified_events {
replay_event(&mut state, ev).unwrap();
}
let rel = state.by_relationship_id.get("rel_alice_emp_001").unwrap();
assert!(matches!(rel.status, DerivedStatus::Revoked));
assert_eq!(rel.revoked_reason_code.as_deref(), Some("employment_ended"));
assert_eq!(state.last_sequence, 2);
}
D.9 Reference Fixture Generation Steps (Issuer-side)
To publish a canonical fixture set for interop:
- Generate a dedicated Ed25519 test keypair
- Publish corresponding JWK in
jwks.json - Serialize payload A and payload B exactly as bytes to be signed
- Produce JWS flattened envelopes with
alg=EdDSA,kid=orgsign-test-1,typ=sig-event+jws - Store envelopes as
events.jsonl - Publish expected derived state JSON
- Freeze fixtures and hash them for regression testing
Recommended Fixture Artifacts
fixtures/
├── sig.json
├── jwks.json
├── payload-upsert.json
├── payload-revoke.json
├── events.jsonl
├── expected-state.json
└── SHA256SUMS
D.10 Optional Canonical Payload Byte Tests
Because signatures operate on exact payload bytes, implementations may also publish:
- UTF-8 payload bytes (hex)
- base64url encoded payload string
- resulting signature bytes (base64url)
This helps detect subtle serialization differences.
Example artifact fields: