FR EN

Epoch signing and passkey delegation

Two layers, both additive and neutral, strengthen the accountability of epoch roots without ever gating verification.

  1. Epoch signing: each epoch root (access-log and cookie-consent) is signed, at seal time, with your company's registered operational Ed25519 DID key.
  2. Root passkey delegation: once, at workspace setup, the company registers a passkey (WebAuthn) as the root of its DID, then signs a delegation authorizing that operational signing key. The company signs rarely (setup, rotation), never per epoch.

What these layers are, and what they are not

The operational signing key is Agreely-hosted: it is generated server-side and its secret is wrapped under the tenant's data-encryption key, so Agreely can unwrap it and sign on the company's behalf. These layers document that the root carries the company's attestation, and that the company authorized the signing key with its own passkey, committed in its DNS. This is not non-repudiation, it is not "you alone," and it is not tamper-proof. The honest phrasing is: "signed with your company's registered DID key," and "you authorize the signing key with your own passkey, committed in your DNS, auditable and revocable." It strengthens accountability under Law 25 (art. 3.1 and 3.2); it is not a guarantee of compliance.

1. Epoch signing

An epoch is a periodic slice of the access log (per company) or the cookie-consent log (per website), summarized by a Merkle root. The root is anchored on-chain via AuditRootAnchored, so an auditor can confirm the epoch existed and was not rewritten after the fact.

At seal time, the root is also signed with the company's registered operational DID key (#kms-1, the same Ed25519 key as companySig). This decouples "can anchor a root on-chain" (Agreely's relayer) from "can produce the company's attestation over that root" (the company's registered DID key), a real defense-in-depth improvement.

The signed descriptor

The signature is taken over a number-free descriptor (every numeric value travels as a string) so it canonicalizes (JCS, RFC 8785) reproducibly. A third party reconstructs the exact same bytes:

{
  "type": "agreely.auditEpoch.v1",
  "did": "did:web:agreely.ca:c:acme",
  "root": "0x<epoch Merkle root, 32 bytes hex>",
  "seqFrom": "1024",
  "seqTo": "2048",
  "chainId": "84532",
  "registry": "0x<lowercased AgreelyRegistry contract address>",
  "keyId": "0x<sha256 of the raw public key, the on-chain keyId>",
  "didKeyId": "kms-1"
}
Field Meaning
type agreely.auditEpoch.v1 (access log) or agreely.cookieEpoch.v1 (cookie consent).
did The registered company DID that attests (did:web:agreely.ca:c:<slug>).
root The epoch Merkle root, 32 bytes, folded to lowercase 0x-hex.
seqFrom / seqTo The inclusive sequence range the epoch covers. Replays membership without the DB autoincrement id.
chainId The anchoring chain (84532 = Base Sepolia).
registry The AgreelyRegistry contract address (lowercased, 0x-hex). Pins the signature to the right contract, because chainId alone does not bind the contract (we redeploy; two registries can share a chainId).
keyId The on-chain keyId of the signing key = 0x + sha256(raw public key), the same bytes32 registerCompanyDid publishes. It self-describes which key signed and lets the signature join to the delegation.
didKeyId The local signing-key label (kms-1, ...), so the local row and the on-chain key both resolve.

The signature itself is a detached 64-byte Ed25519 signature over the JCS-canonical bytes of this descriptor. A verifier resolves the public key from the company's did.json #kms-1 and checks the signature, without trusting Agreely.

Neutrality: an unsigned epoch stays fully valid

Epoch signing is additive. An epoch with no signature (a company with no provisionable key, or an epoch sealed before this layer) verifies exactly as before: its own integrity (the hash-chain replay plus the on-chain anchor) is judged separately and is untouched. A signature that is absent or invalid degrades to "not company-attested," never to an integrity failure.

2. Root passkey delegation

The operational signing key is Agreely-hosted. So that a company can authorize that key with material it controls itself, it registers a root passkey and signs a delegation.

The root: a WebAuthn passkey

At workspace setup (in the foundations phase, after domain verification), the company registers a dedicated WebAuthn passkey whose userHandle is the company DID. The raw COSE public key is the authoritative root material; it is public. The rpId is frozen to the Agreely host. Only ES256 (-7, P-256) and EdDSA (-8, Ed25519) are accepted.

This root is never reused for authentication or login: it is capability-delegation only (capabilityDelegation). A verifier rejects a root-key assertion presented for any other ceremony.

The delegation: a signed authorization

The company then signs, with its root passkey, a delegation: a number-free authorization stating that one specific operational signing key may seal epochs on its behalf, within a bounded window. The WebAuthn challenge is the keccak256 of the canonicalized delegation, so the meaning of the signature lives entirely in the signed body:

challenge = keccak256( JCS(delegation body) )
{
  "type": "agreely.keyDelegation.v1",
  "companyDid": "did:web:agreely.ca:c:acme",
  "delegatee": { "keyId": "0x<sha256 of the raw operational key>" },
  "rootKeyId": "0x<keccak of the root COSE key: which root signed>",
  "registry": "0x<lowercased contract address>",
  "chainId": "84532",
  "rpId": "agreely.ca",
  "nonce": "0x<32 random bytes, single-use>",
  "validFrom": "2026-07-20T15:00:00Z",
  "validUntil": "2027-07-20T15:00:00Z"
}

One delegation covers the access log and cookie consent for all the company's sites. The company signs rarely (setup, rotation), never per epoch; Agreely operates the delegated operational key day to day. The [validFrom, validUntil] window is bounded (12 months by default).

The root is bound to the company by TWO anchors

A delegation that Agreely served inside a did.json Agreely pins would prove nothing on its own (root-substitution risk). The root is therefore bound to the company by two independent anchors:

  1. DNS-TXT root= (the linchpin anchor). The company extends its durable _agreely.<domain> record with a root= commitment equal to the keccak thumbprint of the root COSE key:

    Host:  _agreely.<company-domain>
    Value: agreely-did=did:web:agreely.ca:c:<slug>; key=<sha256 fingerprint of the operational key>; root=<0x + keccak of the root COSE key>
    

    The root is as strong as the company's control of its DNS zone, the system's declared source of truth. Without this commitment, the root would route only through a did.json an Agreely relayer writes.

  2. On-chain did.json CID. The company's did.json (which publishes the root under #root-1) is pinned to IPFS and its CID is registered on-chain via registerCompanyDid. A verifier confirms that the did.json it reads hashes to the on-chain CID.

The root is verifiably bound only when both anchors confirm it: #root-1 must equal the DNS root=, and the did.json must be the one the on-chain CID commits.

Revocable

The authorization is revocable. Revocation is realized as an on-chain operational-key rotation (rotateCompanyDid): the revoked key exits the on-chain key window, so epochs it signs after the rotation block no longer verify as authorized.

Honest caveat on revocation

It is Agreely's relayer that submits the rotation transaction. Revocation is therefore not guaranteed visible independently of the relayer; what is true is that the absence of the expected rotation is detectable by the company, which controls its own DNS zone and can read the chain directly.

Recovery, and the honest limit of custody

The company can register multiple root passkeys and one optional Ed25519 recovery key (authorized only to rotate the root, never to sign a consent). Adding a second root or a recovery key must be signed by an existing active root (a passkey assertion), never merely session-authenticated. A domain-based recovery must commit the new root thumbprint into the DNS root=.

Agreely cannot restore a lost root

Because the root is material the company controls, Agreely genuinely cannot restore it if it is lost and no backup root or recovery key exists. Register a second root or a recovery key at setup. Note also the ~AAL2 caveat for a synced passkey (a platform-provider-synced credential is not hardware-isolated the way a device-bound one is).

3. The trust model and offline verification

The full chain a verifier checks, bottom-up:

epoch signature (operational key #kms-1)
  -> the delegation authorizes that operational keyId (passkey-signed, in-window,
     not revoked)
  -> the delegation is signed by the company's ROOT passkey (WebAuthn assertion,
     #root-1)
  -> the root is bound to the company by the DNS `root=` AND the on-chain did.json CID
  -> domain binding (DNS-TXT `_agreely.<domain>`)

An auditor verifies without trusting Agreely

The shipped offline verifier (verify-core.js / epoch-chain.js / webauthn.js, and the SDK offline verifier) replays the whole chain from a downloaded VerificationBundle, reading Base and the company's DNS directly:

  1. Epoch signature: reconstruct the descriptor, verify the Ed25519 signature against the did.json #kms-1.
  2. Delegation assertion: re-derive the challenge from the body (keccak(JCS(body)), never a supplied challenge), then verify the WebAuthn assertion against #root-1. Hardened checks: origin = rpId, authData.rpIdHash = sha256(rpId), UV flag set, type = webauthn.get, and low-s for ES256.
  3. Coverage: does the delegation authorize this operational keyId, within the [validFrom, validUntil] window resolved by the anchor's (block number, log index) point, and bound to the right registry / chainId / did?
  4. Root binding: does #root-1 equal the root= read live from the company's DNS zone, and does the did.json hash to the on-chain CID?
  5. On-chain key window: is the operational key inside its on-chain key window at anchor time (a rotated-out key resolves "not authorized," which is the revocation)?

The "company-attested" verdict (the green) requires the whole chain: the epoch signature, the delegation signature, coverage, the window actually resolved against the anchor time, root binding, and the key not rotated out.

Neutrality: the undelegated stays green

An epoch with no delegation, or with a delegation present but invalid, degrades to "not company-attested" (attested: false), never to an integrity failure. Unsigned and pre-delegation epochs still verify as before. Signing and delegation are additive: they never gate verification.

Next

  • On-chain registry: AuditRootAnchored, registerCompanyDid / rotateCompanyDid, and the on-chain did.json CID.
  • DID documents: the #root-1 (capabilityDelegation) method and the delegation block in the company did.json.
  • Honest boundaries: why this is accountability, not non-repudiation.