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

  1. Overview
  2. Scope Boundary
  3. Goals and Non-Goals
  4. Terminology
  5. Protocol Model
  6. Discovery and Endpoints
  7. Data Model
  8. Event Types
  9. Signing and Verification
  10. Consumer State Derivation
  11. HTTP and Sync Semantics
  12. Privacy Model
  13. Conformance
  14. Phased Rollout Plan
  15. Implementation Steps
  16. Schemas and Example Code
  17. End-to-End Example
  18. Security Considerations
  19. Future Extensions
  20. Application and Multi-Issuer Chain (Informative)
  21. Deployment Modes (Informative)
  22. Local Authn/Authz Attestation Chain (Informative)
  23. Reference Web UI Addendum (Informative)
  24. 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.example
  • acct:[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 DID
  • jwks_uri (string, REQUIRED) — HTTPS URL for public keys
  • events_uri (string, REQUIRED) — HTTPS URL for public event feed
  • public_only (boolean, REQUIRED) — whether the feed is only public assertions
  • algorithms_supported (array[string], REQUIRED) — allowed signing algorithms
  • event_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.1 for this spec version.
  • 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.upsert and relationship.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.

Event Types

relationship.upsert (REQUIRED)

Creates or updates a relationship record and sets it active.

Additional Required Fields

  • relationship_type (string)
  • status (string, MUST be active in v0.1 upsert events)
  • roles (array[string])
  • valid_from (RFC3339 string or null)
  • valid_until (RFC3339 string or null)

Optional Fields

  • display (object) — public hints (title, department, label)
  • reason (string)
  • metadata (object) — issuer-defined, non-normative extras

Allowed relationship_type Values (v0.1)

  • employee
  • founder
  • contractor
  • advisor
  • investor
  • admin_delegate
  • other

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_ended
  • contract_ended
  • permission_revoked
  • superseded
  • admin_action
  • error_correction
  • other

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:

  • protected
  • payload
  • signature

Event Envelope Example (JWS Flattened)

{
  "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6Im9yc2lnbi0xIiwidHlwIjoib3JyLWV2ZW50K2p3cyJ9",
  "payload": "eyJzcGVjX3ZlcnNpb24iOiJvcnIvMC4xIiwiZXZlbnRfaWQiOiJldnRfMDFKWFlaMSIsLi4ufQ",
  "signature": "L6g6...snip..."
}

Protected Header Requirements

The protected header MUST include:

  • alg — v0.1 requires EdDSA
  • kid — key identifier matching a JWK in the issuer JWKS
  • typ — MUST be sig-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:

  1. Parse JWS flattened envelope.
  2. Decode protected header and payload (base64url).
  3. Check alg is allowed (EdDSA).
  4. Resolve kid in issuer JWKS.
  5. Verify JWS signature.
  6. Parse payload JSON.
  7. Validate schema and issuer consistency.
  8. Validate sequence ordering.
  9. Apply reducer/state transition.

Issuer Consistency Rules

  • Payload issuer MUST equal SIG metadata issuer.
  • 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: sequence ascending
  • Duplicate sequence values: 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:

  • active
  • revoked
  • expired (derived if valid_until has 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 optional reason

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 subject and required predicates, should access be allowed?"

Recommended evaluation flow:

  1. Verify metadata, JWKS, and all feed signatures.
  2. Replay feed and derive current relationship state.
  3. Filter relationships where subject matches exactly.
  4. Only consider relationships with derived status active.
  5. Evaluate required predicates (for example relationship=employee, role=engineering).
  6. Return allow if any active relationship satisfies all predicates; otherwise deny.

A CLI surface for this helper can be:

  • ak check <sig_metadata> --subject <did> --require relationship=employee --require role=engineering
  • ak check ... --explain for human-readable debugging output

Recommended CLI exit behavior:

  • 0 = allow
  • 1 = deny
  • 2 = verification/input/runtime failure

HTTP and Sync Semantics

Content Types

Issuers SHOULD use:

  • application/json for sig.json
  • application/jwk-set+json or application/json for jwks.json
  • application/x-ndjson for events.jsonl

Caching

Issuers SHOULD support:

  • ETag
  • Last-Modified
  • Cache-Control

Consumers SHOULD use conditional requests (If-None-Match, If-Modified-Since) for efficient syncing.

Recommended Consumer Sync Loop

  1. Fetch SIG metadata (sig.json)
  2. Fetch JWKS (jwks.json)
  3. Fetch event feed (events.jsonl)
  4. Verify and replay all events
  5. Persist derived state + latest sequence
  6. 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:

  • public
  • private

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.yaml
  • scripts/conformance-check.sh
  • docs/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:web issuer identifier
  • /.well-known/sig.json
  • /.well-known/jwks.json
  • /.well-known/sig/events.jsonl
  • Ed25519 signatures in JWS (EdDSA)
  • relationship.upsert
  • relationship.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:web issuer
  • .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:

  • EventType
  • RelationshipType
  • Visibility
  • CommonFields
  • UpsertEvent
  • RevokeEvent
  • SignedEnvelope

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.json
  • ak dump-state https://cowboy.com/.well-known/sig.json
  • ak check https://cowboy.com/.well-known/sig.json --subject did:key:z6MkAlice --require relationship=employee --require role=engineering
  • ak 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_id detection
  • 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 kid naming 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 issuer matches 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_time
  • employment.part_time
  • employment.intern
  • contractor.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.posted
  • escrow.funded
  • job.submitted
  • job.verified
  • job.rejected
  • dispute.opened
  • dispute.resolved
  • escrow.released
  • escrow.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.md
  • docs/demo-authn-passkey.md (reference flow)
  • docs/demo-auth-web-ui.md (reference web UI)
  • crates/authkeep-cli/web-ui/ (runnable reference app in ak)

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:web or 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 init locally.
  • 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)

  1. Relying site generates a challenge payload and displays/copies it.
  2. Principal runs local CLI command with the challenge string.
  3. CLI shows:
    • requesting site identity
    • requested permissions/scopes
    • nonce and expiration
  4. Principal approves/denies; CLI produces signed attestation.
  5. Principal pastes attestation back to site.
  6. Site verifies signature, nonce, audience, and expiry; on success, login/session is created.
  7. 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.md
  • crates/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

  1. Login with SIG protocol button on relying site
  2. Challenge display/copy view
  3. Attestation paste/submit view
  4. Verification result/session confirmation view

Minimum Verification Behavior

  • verify signature against principal key material
  • verify challenge_id and nonce binding
  • verify audience binding
  • 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"
  • roles hold assurance tags (for example human, 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: check allows
  • after revoke: check denies

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"
  • roles hold permission scope strings (for example bot.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: check allows
  • after revoke: check denies

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)
  • kid resolves in JWKS
  • sequence is strictly monotonic
  • payload issuer matches metadata issuer

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:

  1. A sample SIG metadata document (sig.json)
  2. A sample JWKS (jwks.json)
  3. Two event payloads (upsert + revoke)
  4. Example NDJSON JWS envelopes
  5. Expected derived state after replay
  6. 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_uri and events_uri
  • require issuer consistency 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 == active for 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_id matches 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 payload and signature values with values generated by a reference signing tool using the private key corresponding to orgsign-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, signature fields
  • 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_id to revoked
  • last_sequence advances to 2
  • roles and 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 = active for 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:

  1. Schema tests
    • deserialize + validate payload fields
  2. JOSE tests
    • verify JWS envelope using JWKS
  3. 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:

  1. Generate a dedicated Ed25519 test keypair
  2. Publish corresponding JWK in jwks.json
  3. Serialize payload A and payload B exactly as bytes to be signed
  4. Produce JWS flattened envelopes with alg=EdDSA, kid=orgsign-test-1, typ=sig-event+jws
  5. Store envelopes as events.jsonl
  6. Publish expected derived state JSON
  7. 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: