> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clawb.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Attestation (prove key ownership)

Attestation is how an **agent proves it controls its private key**.

If you’re not security-focused, here’s the mental model:

* You register a **public key** (safe to share).
* Clawb gives you a one-time **challenge** (random bytes).
* Your agent signs that challenge with its **private key** (secret).
* Clawb verifies the signature using the stored public key.

Once attested, the agent can be marked **active**.

***

## What you need (inputs)

From the register step you will have:

* `agent_id`
* `challenge_id`
* `challenge` (a **base64** string)

And you also have:

* the agent’s **private key** (keep it secret)

***

## Step-by-step: how to attest

### 1) Register the agent (get a challenge)

Call `POST /v1/agents/register` with your agent’s public key.

Clawb returns:

<CodeGroup>
  ```bash curl theme={null}
  curl -sS -X POST "https://api.clawb.ai/api/v1/agents/register" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "my-agent",
      "public_key": "<base64-ed25519-public-key>",
      "key_type": "ed25519",
      "metadata": {"env": "dev"}
    }'
  ```

  ```python Python SDK theme={null}
  from clawb_agent_sdk import ClawbClient

  client = ClawbClient(base_url="https://api.clawb.ai/api")
  register_out = client.register(
      name="my-agent",
      public_key_b64="<base64-ed25519-public-key>",
  )
  print(register_out)
  ```
</CodeGroup>

Response:

<CodeGroup>
  ```json JSON theme={null}
  {
    "agent_id": "agt_...",
    "challenge_id": "ch_...",
    "challenge": "<base64-challenge>"
  }
  ```
</CodeGroup>

### 2) Decode the challenge (base64 → bytes)

The challenge is shown as **base64** because JSON can’t safely carry raw bytes.

**Important:** you must sign the **decoded bytes**, not the base64 text.

Conceptually:

<CodeGroup>
  ```python Python SDK theme={null}
  challenge_bytes = base64_decode(challenge)
  ```
</CodeGroup>

### 3) Sign the decoded bytes with Ed25519

<CodeGroup>
  ```python Python SDK theme={null}
  signature_bytes = ed25519_sign(private_key, challenge_bytes)
  signature = base64_encode(signature_bytes)
  ```
</CodeGroup>

Ed25519 signatures are always **64 bytes**.

### 4) Submit the signature

Call `POST /v1/agents/attest`:

<CodeGroup>
  ```bash curl theme={null}
  curl -sS -X POST "https://api.clawb.ai/api/v1/agents/attest" \
    -H "Content-Type: application/json" \
    -d '{
      "agent_id": "agt_...",
      "challenge_id": "ch_...",
      "signature": "<base64-signature>"
    }'
  ```

  ```python Python SDK theme={null}
  from clawb_agent_sdk import ClawbClient

  agent_client = ClawbClient(
      base_url="https://api.clawb.ai/api",
      agent_id="agt_...",
      private_key_b64="<private_key_b64>",
  )

  agent_client.attest(
      challenge_id="ch_...",
      challenge_b64="<challenge_base64>",
  )
  ```
</CodeGroup>

If everything is correct, Clawb responds with:

<CodeGroup>
  ```json JSON theme={null}
  { "agent_id": "agt_...", "status": "active" }
  ```
</CodeGroup>

***

## What does “sign the decoded challenge bytes” mean?

You will see something like:

* `challenge = "MjAy..."` (base64 text)

Clawb will verify a signature over:

* `base64_decode(challenge)` (the underlying bytes)

So:

* ✅ correct: `sign(base64_decode(challenge))`
* ❌ wrong: `sign("MjAy..." as text)`

***

## Common errors (and what they usually mean)

* **`410 challenge expired`**: you waited too long (challenge has a short lifetime). Re-register to get a new challenge.
* **`409 challenge already used`**: a challenge is one-time use. Re-register.
* **`401 invalid signature`**: usually one of:
  * you signed the base64 *string* instead of decoded bytes
  * you used the wrong private key
  * you accidentally encoded/decoded with the wrong base64 variant

***

## Attestation examples (Node, Python, Go)

### Key format note

If you copied the private key from the dashboard and it says something like **“PKCS8 base64”**, that usually means:

* your private key is **PKCS#8 DER**, represented as a base64 string

Each example below assumes:

* `challenge_b64` = the `challenge` you got from register
* `priv_pkcs8_b64` = your base64 PKCS#8 private key

### Node.js (built-in `crypto`)

<CodeGroup>
  ```js JavaScript theme={null}
  import crypto from "crypto";

  const agent_id = "agt_...";
  const challenge_id = "ch_...";

  const challenge_b64 = "<challenge_base64>";
  const priv_pkcs8_b64 = "<private_key_pkcs8_base64>";

  // 1) Decode challenge bytes
  const challengeBytes = Buffer.from(challenge_b64, "base64");

  // 2) Load PKCS8 DER private key
  const privateKeyDer = Buffer.from(priv_pkcs8_b64, "base64");
  const privateKey = crypto.createPrivateKey({
    key: privateKeyDer,
    format: "der",
    type: "pkcs8",
  });

  // 3) Sign (Ed25519: pass null digest)
  const sig = crypto.sign(null, challengeBytes, privateKey);

  // 4) Signature base64
  const signature_b64 = sig.toString("base64");

  console.log({ agent_id, challenge_id, signature: signature_b64 });

  // 5) POST signature_b64 to /v1/agents/attest
  ```

  ```py Python SDK theme={null}

  ### Python (`cryptography`)
  # Install: pip install cryptography

  import base64
  from cryptography.hazmat.primitives import serialization

  agent_id = "agt_..."
  challenge_id = "ch_..."

  challenge_b64 = "<challenge_base64>"
  priv_pkcs8_b64 = "<private_key_pkcs8_base64>"

  # 1) Decode challenge bytes
  challenge_bytes = base64.b64decode(challenge_b64)

  # 2) Load PKCS8 DER private key
  priv_der = base64.b64decode(priv_pkcs8_b64)
  private_key = serialization.load_der_private_key(priv_der, password=None)

  # 3) Sign
  signature_bytes = private_key.sign(challenge_bytes)

  # 4) Signature base64
  signature_b64 = base64.b64encode(signature_bytes).decode("utf-8")

  print({
      "agent_id": agent_id,
      "challenge_id": challenge_id,
      "signature": signature_b64,
  })

  # 5) POST signature_b64 to /v1/agents/attest
  ```

  ```go Go theme={null}
  ### Go (standard library)

  package main

  import (
  	"crypto/ed25519"
  	"crypto/x509"
  	"encoding/base64"
  	"fmt"
  	"log"
  )

  func main() {
  	agentID := "agt_..."
  	challengeID := "ch_..."

  	challengeB64 := "<challenge_base64>"
  	privPKCS8B64 := "<private_key_pkcs8_base64>"

  	challengeBytes, err := base64.StdEncoding.DecodeString(challengeB64)
  	if err != nil {
  		log.Fatal(err)
  	}

  	privDer, err := base64.StdEncoding.DecodeString(privPKCS8B64)
  	if err != nil {
  		log.Fatal(err)
  	}

  	keyAny, err := x509.ParsePKCS8PrivateKey(privDer)
  	if err != nil {
  		log.Fatal(err)
  	}

  	priv, ok := keyAny.(ed25519.PrivateKey)
  	if !ok {
  		log.Fatal("not an ed25519 private key")
  	}

  	sig := ed25519.Sign(priv, challengeBytes)
  	signatureB64 := base64.StdEncoding.EncodeToString(sig)

  	fmt.Printf("agent_id=%s\nchallenge_id=%s\nsignature=%s\n", agentID, challengeID, signatureB64)
  }
  ```
</CodeGroup>

***

## After attestation: signing requests (step-by-step)

Once your agent is active, you’ll typically sign requests that matter (for example when calling your service, or when calling Clawb signed endpoints).

### Required headers

* `X-Clawb-Agent-Id`: your `agent_id`
* `X-Clawb-Timestamp`: unix epoch milliseconds (example: `1739140000000`)
* `X-Clawb-Nonce`: random string (UUID is fine)
* `X-Clawb-Signature`: base64 Ed25519 signature

Timestamp must be `milliseconds`; using `seconds` causes signature validation failure.

### Canonical string (exact format)

Build this string (newlines matter):

<CodeGroup>
  ```text Text theme={null}
  METHOD\nPATH\nTIMESTAMP\nNONCE\nSHA256(body)
  ```
</CodeGroup>

Where:

* `METHOD` is uppercase (`POST`, `GET`, …)
* `PATH` is only the path (example: `/v1/check`) — not the full URL
* `TIMESTAMP` is unix epoch milliseconds from `X-Clawb-Timestamp`
* `SHA256(body)` is lowercase hex of the SHA-256 of the raw request body bytes

Then sign the **UTF-8 bytes** of that canonical string with Ed25519, and base64-encode the signature bytes into `X-Clawb-Signature`.

### Why timestamp + nonce?

They help prevent replay attacks (someone capturing a request and re-sending it later).

***

## UI note: “Attest now”

The dashboard may offer an **“Attest now”** button.

That button is a convenience for development:

* if the browser generated the keypair, the browser already has the private key
* so it can sign locally and call `/v1/agents/attest`

In production, the **agent runtime** should do the signing.
