Signing & Verification
How events are signed with JWS and verified by consumers.
JWS JSON Flattened Serialization
Each line in events.jsonl is a JWS using JSON Flattened Serialization (RFC 7515 Section 7.2.2). The envelope is a JSON object with three base64url-encoded fields:
| Field | Contents |
|---|---|
protected | Base64url-encoded JSON header |
payload | Base64url-encoded JSON event body |
signature | Base64url-encoded Ed25519 signature |
{
"protected": "eyJhbGciOiJFZERTQSIsImtpZCI6Im9yZ3NpZ24tMSIsInR5cCI6Im9yZS1ldmVudCtqd3MifQ",
"payload": "eyJldmVudF9pZCI6ImV2dF8wMDEiLCJldmVudF90eXBlIjoicmVsYXRpb25zaGlwLnVwc2VydCIsInNlcXVlbmNlIjoxLCJpc3N1ZXIiOiJkaWQ6d2ViOmFjbWUuZXhhbXBsZSIsImlzc3VlZF9hdCI6IjIwMjYtMDEtMTVUMDk6MDA6MDBaIiwic3ViamVjdCI6ImRpZDprZXk6ejZNa0FsaWNlIiwicmVsYXRpb25zaGlwX2lkIjoicmVsX2FsaWNlXzAwMSIsInJlbGF0aW9uc2hpcF90eXBlIjoiZW1wbG95ZWUiLCJyb2xlcyI6WyJlbmdpbmVlcmluZyJdLCJ2aXNpYmlsaXR5IjoicHVibGljIn0",
"signature": "3Kkz...base64url-encoded-signature..."
}
Each line in the event feed is exactly one of these JWS envelopes. No additional wrapping or framing is applied.
Protected Header
The protected field decodes to a JSON header with three required fields:
{
"alg": "EdDSA",
"kid": "orgsign-1",
"typ": "sig-event+jws"
}
| Field | Requirement |
|---|---|
alg | Must be "EdDSA". No other algorithms are permitted. |
kid | Must match a key ID present in the issuer’s JWKS. |
typ | Must be "sig-event+jws". Identifies the payload as an SIG event. |
Signing
The signing input is the concatenation of the base64url-encoded protected header, a literal ASCII period (.), and the base64url-encoded payload:
base64url(protected) + "." + base64url(payload)
This input is signed using the Ed25519 private key identified by kid. The resulting signature is base64url-encoded and placed in the signature field.
The signing key must be an OKP (Octet Key Pair) key with curve Ed25519. The corresponding public key is published in the issuer’s JWKS so that consumers can verify signatures.
Verification Procedure
Consumers process each line of the event feed through a 9-step verification procedure. Every step must pass for the event to be accepted.
Step 1: Parse JWS
Parse the JSON line as a JWS JSON Flattened Serialization object. The object must contain protected, payload, and signature fields, all of which are strings.
Step 2: Decode Protected Header
Base64url-decode the protected field and parse it as JSON. The result must be a valid JSON object.
Step 3: Decode Payload
Base64url-decode the payload field. Do not parse it as JSON yet — the raw bytes are needed for signature verification.
Step 4: Check Algorithm
The alg field in the protected header must be "EdDSA". Consumers must reject "none" and any unsupported algorithm values. This prevents algorithm downgrade attacks.
Step 5: Resolve Key
Look up the kid from the protected header in the issuer’s JWKS. The key must be present, must have kty: "OKP" and crv: "Ed25519", and must include a public key (x field).
Step 6: Verify Signature
Reconstruct the signing input (base64url(protected) + "." + base64url(payload)) and verify the signature against it using the resolved Ed25519 public key. Reject the event if verification fails.
Step 7: Parse Event JSON
Parse the decoded payload bytes as JSON. The result must be a valid SIG event object.
Step 8: Validate Schema
Validate the event against the expected schema for its event_type. Required fields must be present and correctly typed.
Step 9: Validate Sequence and Apply
Validate that the event’s sequence number is correct relative to previously processed events (no duplicates, no gaps). Apply the event to the state reducer.
Algorithm Restrictions
The SIG protocol exclusively uses Ed25519 for signing. Consumers must enforce this strictly:
- The only accepted
algvalue is"EdDSA"with key typeOKPand curveEd25519. alg: "none"must always be rejected.- Any algorithm not explicitly supported (RS256, ES256, HS256, etc.) must be rejected.
- Implementations must not fall back to weaker algorithms if
EdDSAverification fails.