Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Signatures

This chapter specifies how operations are signed and verified, and how nodes authenticate to one another over HTTP. It is the specification of the JWS envelope used by the signature field on every operation, the canonical signing form referenced from Wire Format, and the bearer-token issuance used by Sync.

1. Algorithm

All signatures defined by v0.1 of this specification are Ed25519 (RFC 8032). Signature size is fixed at 64 bytes; verification keys are 32 bytes.

A future revision may add additional algorithms. v0.1 conformant implementations MUST support Ed25519 and MAY accept any other algorithm if and only if a future minor version explicitly introduces it.

2. Per-node keys

Each node holds exactly one Ed25519 signing key for the duration of its lifetime. Key rotation in v0.1 is performed by issuing a new node identity (a new NodeId and key pair) and delegating authority to it via DelegateUcan; the previous identity may then be revoked.

The mapping NodeId -> Ed25519 public key is established by the node’s first DelegateUcan op observed on the log. This op MUST embed a UCAN whose iss field is the issuer’s DID and whose sub (or equivalent) names the NodeId and carries the public key. After this binding op is observed, every subsequent op authored by that NodeId MUST be verified against the bound key.

3. Detached JWS envelope for op signatures

The wire-level value of an operation’s signature field is a detached JWS as defined by RFC 7515 §A.5: the header and signature segments are present, the payload segment is empty.

The detached JWS is encoded as the UTF-8 bytes of the string:

BASE64URL(header) "." "." BASE64URL(signature)

Where:

  • header is the JSON Object Serialization of:
    {"alg":"EdDSA","kid":"node-<node_id>"}
    
    with field order as written, no insignificant whitespace, ASCII encoding. The kid value is the literal prefix "node-" followed by the node’s NodeId rendered as decimal digits (since NodeId is a 64-bit integer in v0.1; see Wire Format).
  • signature is the 64-byte Ed25519 signature over the canonical signing form defined in Section 4.

The bytes carried in the operation’s signature field are the UTF-8 byte sequence of the above string. Implementations MUST NOT include line breaks or trailing whitespace.

4. Canonical signing form

To sign or verify an operation:

  1. Construct the operation per the operation envelope.
  2. Clear the signature field (encode it as absent).
  3. Encode the operation per Wire Format. Call the result op_bytes.
  4. Sign or verify op_bytes using the Ed25519 key bound to the operation’s node_id.

When signing, the resulting 64-byte signature is wrapped per Section 3 and stored in the signature field before transmission.

When verifying, the receiver unwraps the detached JWS, recovers the 64-byte raw signature, and verifies it against op_bytes constructed from the received op (with its signature field cleared) using the public key bound to the op’s node_id.

A receiver MUST reject any operation whose verification fails, unless the op is a sanitized op admitted by Section 6.

5. Implementation note: round-tripping the signature field

The most common implementation error in this area is mishandling the round-trip: implementations sign an op with the signature field absent, transmit it with the field populated by the JWS, and then attempt to verify by re-signing the received op as-is — yielding a signature over different bytes. Implementations MUST explicitly clear the signature field before computing the canonical encoding for verification, and MUST treat that step as the canonical procedure regardless of how the op is held in memory.

A reference test vector for cross-implementation interop is expected to ship with v0.1.1, alongside the public release of the reference implementation. Until both exist, implementers cannot fully validate signature canonicalization against an authoritative source; the procedure in this section and the field-ordering rules in Wire Format are what to follow in the meantime. See Implementations for status.

6. Sanitized operations

When an outbound op crosses a delegation that requires sanitization (per the sanitize caveat described in UCAN and Caveats), the sanitizer modifies the op’s payload by stripping or redacting the affected fields. Because the resulting op no longer matches the bytes the original signature was computed over, the signature would no longer verify. Therefore the sanitizer MUST clear the signature field on the sanitized op (encode it as absent) and record the sanitization in a marker field (specified in UCAN and Caveats).

The receiver MUST NOT attempt signature verification on a sanitized op. It MUST verify that the sanitization marker is consistent with a delegation that authorized the sender to apply it; this is the receive-side procedure specified in UCAN and Caveats.

An op that arrives without a signature and without a sanitization marker MUST be rejected. The two conditions are the only legitimate paths to an unsigned op on the wire (and even the bootstrap path described in Section 7 produces a signed op).

7. Bootstrap: the first op a node authors

A node that has not yet been seen on the log presents a chicken-and-egg problem: the receiver does not know the node’s public key, so cannot verify the op that establishes the binding.

The protocol resolves this by requiring that a node’s first authored op be a DelegateUcan carrying a UCAN that:

  1. Is signed by the issuer’s DID key (not the node’s).
  2. Embeds the node’s public key in the UCAN’s subject claim.
  3. Is itself well-formed and verifiable against the issuer’s DID.

The receiver:

  1. Recognizes that the authoring NodeId is unknown.
  2. Decodes the embedded UCAN.
  3. Verifies the UCAN’s signature against the issuer’s DID.
  4. If valid, extracts the embedded public key and binds it to the authoring NodeId.
  5. Verifies the op’s own signature using the freshly-bound key.

If any step fails, the op is rejected. After this op is applied, the node’s identity is known and subsequent ops authored by it follow the standard signing rules.

8. Bearer tokens for HTTP authentication

The Authorization: Bearer header on GET /ops and POST /ops identifies the requesting node to the server. A bearer token is a short-lived signed assertion of node identity, structured as follows:

BASE64URL(header) "." BASE64URL(payload) "." BASE64URL(signature)

(The standard JWS Compact Serialization, this time not detached.)

Header:

{"alg":"EdDSA","kid":"node-<node_id>"}

Payload (JSON):

{
  "iss": "<NodeId>",
  "aud": "<recipient NodeId or origin>",
  "iat": <unix-seconds>,
  "exp": <unix-seconds>,
  "nonce": "<random>"
}

Tokens MUST have an expiry (exp) no more than one hour in the future. Recipients MUST reject tokens that are expired, that present an iss not bound to a valid public key on the log, or that reuse a nonce already seen for the same iss within the validity window.

Token issuance is per-request: a node generates a fresh token for each peer, signs it, and presents it. There is no central issuer. A future revision may add a refresh-token mechanism; v0.1 implementations issue one-shot tokens.

9. Verifying authority

A signature establishes that the authoring NodeId produced the op. It does not establish that the NodeId was authorized to produce it. Authorization is a separate check performed against the UCAN delegation graph — see UCAN and Caveats and Capabilities. Both checks are required; either failure rejects the op.