YadaCoin KEL Agent Auth Protocol — Specification v1.1

1. Overview

The YadaCoin KEL Agent Auth protocol lets any third-party service authenticate AI agents (or any automated caller) without the operator’s private key ever leaving the operator’s device. Authority is delegated by committing a one-time agent key on the YadaCoin blockchain, optionally with a structured scope document that binds exactly what the agent is allowed to do. The service then authenticates the agent via a stateless challenge-response and enforces the on-chain scope.

Design goals

Goal How it is achieved
Private key never transmitted Challenge is signed client-side; only the public key + signature reach the server
Forward secrecy Every agent credential is one-time-use; once spent it appears as a revoked entry in the KEL
Scope binding The scope document is committed in a blockchain transaction before the request; it cannot be altered in transit
Stateless service The HMAC challenge is derived deterministically; the service keeps no session state
Third-party friendly The only YadaCoin dependency is a KEL lookup — either via the SDK or the public REST API

2. Roles

Role Description
Operator Human who owns the root key and approves agent actions
Agent Automated process that holds a provisioned one-time key
Service Third-party API endpoint that accepts KEL-authenticated requests

3. Key Event Log (KEL)

A KEL is a chain of version-7 transactions on the YadaCoin network. Each entry:

Field Description
public_key Compressed secp256k1 public key (hex) of the current signer
public_key_hash P2PKH address of public_key — once this appears in the KEL the key is revoked
prerotated_key_hash P2PKH address of the next authorised signer
twice_prerotated_key_hash P2PKH address of the signer after next
relationship Base64-encoded scope document (optional)
id / transaction_signature Transaction identifier

The chain guarantees that:

  • Only the holder of the key pre-committed by kel[-1].prerotated_key_hash can act next.
  • Any key that has signed a rotation transaction (public_key_hash appears in the KEL) is revoked.

4. Agent Provisioning

Before calling a service the operator must provision the agent key on-chain. This is a pair of rotation transactions broadcast to the YadaCoin network.

┌─────────────────────────────────────────────────────────────┐
│  UNCONFIRMED transaction  (signed by K_{n})                 │
│                                                             │
│  public_key                = K_{n}                         │
│  public_key_hash           = addr(K_{n})                   │
│  prerotated_key_hash       = addr(K_{n+1})  ← agent key    │
│  twice_prerotated_key_hash = addr(K_{n+2})  ← scope anchor │
│  relationship              = base64(W3C_VC_JSON)            │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│  CONFIRMING transaction  (signed by K_{n+1})                │
│                                                             │
│  public_key                = K_{n+1}                       │
│  public_key_hash           = addr(K_{n+1})                 │
│  prerotated_key_hash       = addr(K_{n+2})                 │
│  twice_prerotated_key_hash = addr(K_{n+3})  ← agent key    │
│  relationship              = ""                             │
└─────────────────────────────────────────────────────────────┘

The agent credential is K_{n+3} — the twice_prerotated_key_hash of the CONFIRMING tx. The scope is carried on the UNCONFIRMED tx, whose twice_prerotated_key_hash equals addr(K_{n+2}) which in turn equals the CONFIRMING tx’s prerotated_key_hash.

After both transactions are broadcast (mempool is sufficient — services check mempool + chain), K_{n+3} is the agent credential.

Scope location rule: To retrieve the scope for an agent key K_agent, walk the KEL and find the entry E where E.twice_prerotated_key_hash == addr(K_agent). The scope is in E.relationship.


5. Scope Document (W3C Verifiable Credential 2.0)

The relationship field of the UNCONFIRMED provisioning transaction MUST be a base64-encoded UTF-8 JSON object conforming to the W3C Verifiable Credentials Data Model v2.0.

Required fields

JSON path Type Description
@context string[] Must include "https://www.w3.org/ns/credentials/v2"
type string[] Must include "VerifiableCredential"
issuer string did:yadacoin:<operator_public_key_hex>
validFrom string ISO 8601 timestamp
credentialStatus.type string MUST be "YadaKELStatus" (see §5.1)
credentialStatus.mode string "rotation" or "temporal" (see §5.1)
credentialSubject.id string did:yadacoin:<agent_public_key_hex> (K_{n+3})
credentialSubject.agentAuthorization.type string Service-defined, e.g. "TravelBookingAuthorization"

Services define their own agentAuthorization fields; unrecognised keys MUST be ignored.

5.1 YadaKELStatus — Credential Status Type

credentialStatus.type: "YadaKELStatus" declares that revocation and presentation validity are governed by the holder’s Key Event Log on the YadaCoin blockchain. The mode field controls how the verifier interprets a key rotation in the holder’s KEL:

mode Revocation behaviour Typical use case
rotation One-time-use. If the holder’s key appears as public_key_hash in any KEL entry the credential is revoked. Short-lived delegated agent credentials
temporal Persists across rotations. Revocation check is skipped; the VP MUST be signed with the holder’s current active key per the KEL. Long-lived professional credentials, licenses

Verifiers MUST read credentialStatus.mode from the on-chain VC before applying the revocation check. If the field is absent the verifier MUST default to "rotation" behaviour.

The YadaKELStatus type is defined in the https://yadacoin.io/contexts/agent-auth/v1 JSON-LD context.

Example — Travel Booking

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://yadacoin.io/contexts/agent-auth/v1"
  ],
  "type": ["VerifiableCredential", "AgentAuthorizationCredential"],
  "issuer": "did:yadacoin:02a1b2c3...",
  "validFrom": "2026-05-01T00:00:00.000Z",
  "credentialStatus": {
    "type": "YadaKELStatus",
    "mode": "rotation"
  },
  "credentialSubject": {
    "id": "did:yadacoin:03d4e5f6...",
    "agentAuthorization": {
      "type": "TravelBookingAuthorization",
      "destination": "New York City",
      "checkin": "May 10",
      "checkout": "May 15",
      "services": ["hotel", "flight"]
    }
  }
}

Legacy flat format (deprecated)

Pre-v1.1 implementations used a flat JSON object:

{
  "task": "travel_booking",
  "dest": "New York City",
  "checkin": "2026-05-10",
  "checkout": "2026-05-15",
  "services": ["hotel", "flight"]
}

Services SHOULD accept both formats for backwards compatibility. The Python and JavaScript SDKs provide _extract_scope() / parseScope() helpers that normalise both into a common {dest, checkin, checkout, services} dict.


6. Challenge-Response Protocol

6.1 Request a Challenge

GET /your-endpoint/challenge?public_key=<hex>

Response

{
  "challenge": "a3f9b2...",
  "expires_in": 27
}

The challenge is a stateless HMAC-SHA256 hex string computed as:

challenge = HMAC-SHA256(key=SECRET, msg="{public_key}:{window}")
window    = floor(unix_timestamp / 30)

Challenges are valid for the current window and the previous one (up to ~60 s of clock skew tolerance). No server-side session state is required.

6.2 Sign the Challenge (client-side)

message_hash = SHA-256( challenge.encode("utf-8") )   // 32 raw bytes
signature    = secp256k1_sign( message_hash, agent_private_key )
                 // DER-encoded ECDSA, base64-encoded

The message passed to the signing function is the raw 32-byte hash — do not hash it again inside the signing library.

With @noble/secp256k1 v2:

import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";

const msgHash = sha256(new TextEncoder().encode(challenge));
const sigObj = secp256k1.sign(msgHash, privateKeyBytes, { lowS: true });
const derBytes = sigObj.toDERRawBytes();
const signature = btoa(String.fromCharCode(...derBytes));

6.3 Submit the Authenticated Request

POST /your-endpoint/action
Content-Type: application/json

{
  "public_key": "<66-char hex compressed secp256k1 key>",
  "challenge":  "<64-char hex HMAC-SHA256>",
  "signature":  "<base64-encoded DER signature>",
  ...service-specific fields...
}

7. Server Validation (Required Steps)

A conforming service MUST perform these checks in order, returning the specified HTTP status on failure:

Step Check Failure status
1 challenge matches HMAC-SHA256 for current or previous 30-second window 401
2 secp256k1_verify(b64decode(signature), sha256(challenge), public_key) 401
3 KEL exists for public_key 403
4 Find KEL entry E where E.twice_prerotated_key_hash == addr(public_key); read scope and credentialStatus from E.relationship 403
5 Read credentialStatus.mode (default "rotation" if absent)
6 If mode == "rotation": addr(public_key) does NOT appear as public_key_hash in any KEL entry 403
7 kel[-1].prerotated_key_hash == addr(public_key) (confirms key is the current active key per the KEL, regardless of mode) 403
8 Request is within the scope read in step 4 (if present) 403

Step 4 detail: kel[-1] is the CONFIRMING tx and its relationship is always "". The scope is on the UNCONFIRMED tx one level back, identified by its twice_prerotated_key_hash == addr(agent_key). Always iterate the KEL to find this entry rather than reading kel[-1].relationship.

Step 6 note: In temporal mode the revocation check is skipped because the credential is intended to survive key rotations. Step 7 still enforces that the VP is signed with the holder’s current active key; an old key that has already rotated will fail step 7 rather than step 6.

Step 8 is service-defined; the SDK provides helpers but ultimate enforcement is the service’s responsibility.


8. HTTP Status Semantics

Services SHOULD use these codes to communicate booking / action outcomes:

Code Meaning
200 All requested actions completed successfully
206 Partial success: some actions completed, others failed
400 Malformed request (missing fields, bad encoding)
401 Challenge expired/invalid or signature verification failed
403 Revoked key, KEL mismatch, or scope violation
422 Authentication passed but nothing could be fulfilled (e.g. no inventory)

9. Signature Format

Signatures MUST be:

  • Algorithm: ECDSA over secp256k1
  • Message: raw 32-byte SHA-256 of the challenge string (hasher=None / pre-hashed)
  • Encoding: DER-encoded, then base64 (standard, with = padding)
  • Canonicalisation: low-S form RECOMMENDED (prevents signature malleability)

Example value: MEUCIQCGhWcXJnjbbOvb...=


10. KEL Lookup

Services need to resolve the KEL for a given public_key. Two options:

Option A — Bundled YadaCoin node

from yadacoin.core.keyeventlog import KeyEventLog
kel = await KeyEventLog.build_from_public_key(public_key_hex)

Option B — REST API (no local node required)

GET https://yadacoin.io/key-event-log?public_key=<hex>

Response: JSON array of KEL entries (same schema as on-chain transactions).

The Python SDK YadaCoinRestKelProvider implements option B out of the box.


11. Security Considerations

Challenge secret
The HMAC secret must be kept server-side. Use an environment variable: YADACOIN_AGENT_SECRET. Rotating the secret immediately invalidates all outstanding challenges (at most 60 s disruption).

One-time use
Each provisioned key SHOULD be used for a single request then discarded. The revocation check (step 4) enforces this: once the key signs a rotation it appears as public_key_hash in the KEL and is rejected.

Scope binding
The scope is committed in a blockchain transaction before the request. An attacker who intercepts the request cannot widen the scope.

Replay protection
The 30-second challenge window provides replay protection. Services that require stronger guarantees (e.g. financial transactions) SHOULD track used challenges in a short-lived cache keyed by (public_key, challenge).

Clock skew
The ±30-second window (two windows accepted) tolerates typical NTP drift. Services MAY tighten this by accepting only the current window.

DER malleability
Require lowS: true from clients and verify the s-value constraint on the server to prevent signature malleability.


12. Minimal Endpoint Template

from yadacoin_agent_auth import AgentAuthValidator, AuthError
import os

validator = AgentAuthValidator(
    challenge_secret=os.environ["YADACOIN_AGENT_SECRET"].encode()
)

# GET /challenge?public_key=<hex>
async def challenge_handler(request):
    info = validator.make_challenge(request.args["public_key"])
    return json_response(info)          # {challenge, expires_in}

# POST /action
async def action_handler(request):
    body = await request.json()
    try:
        auth = await validator.validate(
            public_key=body["public_key"],
            challenge=body["challenge"],
            signature=body["signature"],
        )
    except AuthError as exc:
        return json_response({"error": str(exc)}, status=exc.http_status)

    scope = auth.scope          # dict from on-chain relationship field
    # ... implement your service logic here ...
    return json_response({"status": True})


13. Versioning

This document describes protocol version 1.2.

Version Changes
1.0 Initial specification
1.1 Scope format upgraded to W3C Verifiable Credentials 2.0; clarified scope location (UNCONFIRMED tx); validation table updated to 7 steps; CONFIRMING/UNCONFIRMED tx pair documented
1.2 Added credentialStatus.type: "YadaKELStatus" with mode field (rotation / temporal); server validation table restructured — scope read before revocation check so mode gates check; updated VC examples and VP auth sections

Breaking changes will increment the major version. The public_key field in requests MAY carry a protocol_version hint in future versions.


This section maps each architectural component of this protocol to the most relevant existing standards. Implementors MAY replace or augment components with the alternatives listed below.

14.1 Key Event Log / Key Rotation

The YadaCoin KEL uses version-7 blockchain transactions chained by hash pre-commitment. This model is a blockchain-anchored subset of KERI (Key Event Receipt Infrastructure — IETF draft), which defines a formal grammar for key events (icp inception, rot rotation, ixn interaction), pre-rotation with next key hash commitments, and duplicity detection. Implementations that require off-chain operation, witness infrastructure, or multi-threshold receipts SHOULD adopt full KERI event types. Alignment with KERI terminology improves interoperability with existing KERI tooling and verifiers.


14.2 Agent Identity / DID Method

Agent and operator identities are expressed as did:yadacoin:<pubkey_hex>. This convention is compatible with the W3C Decentralized Identifiers specification. A formal did:yadacoin DID method registration at the W3C CCG would allow standard DID resolvers to treat KEL entries as a DID Document with verificationMethod and authentication sections, enabling interoperability with any W3C VC issuer or verifier. Alternative DID methods are compared below:

Method Trade-off
did:key Self-contained, no chain needed; no revocation
did:web DNS-anchored; centralised trust
did:ion Bitcoin-anchored; well-specified; closest structural match to YadaCoin’s model
did:peer P2P, no blockchain; no global resolution

14.3 Challenge-Response Authentication

This protocol uses a stateless HMAC-SHA256 challenge with 30-second windows. The following standards provide alternative or complementary authentication mechanisms:

Standard Notes
HTTP Message Signatures (RFC 9421) Signs request headers + body; replay-safe with nonce; no pre-shared secret needed; eliminates the challenge round-trip
DPoP (RFC 9449) Proof-of-possession JWT bound to a specific HTTP request; compatible with OAuth 2.0
FIDO2/WebAuthn Applicable to the operator approval step; the second-factor confirmation flow matches the WebAuthn ceremony model
GNAP (RFC 9635) Full grant negotiation protocol; supports key-bound tokens and delegated agent flows

RFC 9421 (HTTP Message Signatures) eliminates the challenge round-trip by binding the signature to the full request payload including headers and body.


14.4 Scope / Authorization Document

This protocol uses W3C Verifiable Credentials 2.0 committed in the relationship field. The following standards provide alternative or complementary authorization mechanisms:

Standard Notes
Verifiable Presentations (W3C) Agent presents a VP containing the VC directly to the service rather than the service resolving it from the KEL; improves privacy
Rich Authorization Requests (RFC 9396) Structured authorization_details JSON in OAuth token requests; service-defined type objects map directly to agentAuthorization
UMA 2.0 Operator controls resource server grants via an authorisation server; well-suited to multi-party agent delegation
XACML 3.0 Full policy language; useful when scope enforcement requires complex policy composition

RFC 9396 authorization_details objects are structurally equivalent to agentAuthorization and are supported by the majority of OAuth 2.0 authorisation server implementations.


14.5 Signature Scheme

This protocol uses secp256k1 ECDSA with DER encoding and base64 transport. The following alternatives offer improved size, performance, or ecosystem interoperability:

Alternative Benefit
Ed25519 / EdDSA (RFC 8032) Faster; 64-byte flat encoding vs ~72-byte DER; no malleability; deterministic
JWS ES256K (RFC 7515 + RFC 8812) Standard JSON encoding for the existing secp256k1 key material; drop-in for interoperability
BLS12-381 Supports signature aggregation (multiple agents, one combined proof); useful at scale

JWS with ES256K (secp256k1) or EdDSA (Ed25519) provides a standardised encoding layer over the existing key material with broad library support.


14.6 Key Derivation

This protocol uses 4× BIP32 hardened child derivation. The reference implementation uses HMAC-SHA256 for the derivation function; conforming implementations SHOULD use HMAC-SHA512 per the full BIP32 specification. Alternative KDFs:

Standard Notes
BIP32 (full spec) Intended target for this protocol; HMAC-SHA512 MUST be used in production
SLIP-0010 BIP32 generalised to Ed25519 and other curves
HKDF (RFC 5869) Simpler extract-expand KDF; not HD-wallet compatible but more auditable for non-Bitcoin contexts

14.7 Interoperability Roadmap

The following integrations would maximise third-party adoption without breaking changes to the core protocol:

  1. did:yadacoin DID method registration (W3C CCG) — enables standard DID resolvers to resolve credentialSubject.id values to live DID Documents with verificationMethod entries.
  2. RFC 9421 (HTTP Message Signatures) — replaces the stateless HMAC challenge with request-bound signatures that cover the full body, eliminating a round-trip.
  3. RFC 9396 (Rich Authorization Requests) — maps agentAuthorization objects to a standard OAuth 2.0 authorization_details vocabulary understood by existing authorisation servers.
  4. KERI event type alignment — if the KEL is extended to support multi-witness operation, the UNCONFIRMED/CONFIRMING tx pair maps directly to KERI rot and ixn event types.

YadaCoin Open Source License (YOSL) v1.1 — Copyright © 2017-2025 Matthew Vogel, Reynold Vogel, Inc.