npm package: @hathbanger/tap-core
Description: RFC 9421 HTTP Message Signatures for AI agents — crypto primitives for Visa Trusted Agent Protocol (TAP)
Latest version: 1.0.0

# @hathbanger/tap-core

RFC 9421 HTTP Message Signatures for AI agents — the crypto primitives behind [Visa's Trusted Agent Protocol (TAP)](https://developer.visa.com/pages/tap).

Generate Ed25519 keypairs, sign HTTP requests with cryptographically verifiable identity, and verify those signatures on the server side. Works in Node.js and any runtime with the Web Crypto API.

```ts
import { generateKeyPair, signRequest, verifyAgentSignature } from "@hathbanger/tap-core"

const keyPair = await generateKeyPair()

const headers = await signRequest(keyPair, [
  ["@method", "POST"],
  ["@authority", "api.example.com"],
  ["@path", "/v1/checkout"],
], "agent-payer-auth")

// headers["signature-input"] → sig1=("@method" "@authority" "@path");created=...;keyid="...";...
// headers["signature"]       → sig1=:base64url:
```

---

## Install

```bash
npm install @hathbanger/tap-core
# or
pnpm add @hathbanger/tap-core
```

Requires Node.js 18+. No native dependencies.

---

## Core concepts

**Keypair** — an Ed25519 keypair with a derived key ID (JWK SHA-256 thumbprint). The public half is published to a JWKS endpoint; the private half signs requests.

**Signature-Input** — an RFC 9421 header that describes _what_ was signed: the ordered list of covered components, a timestamp window, nonce, key ID, algorithm, and an optional application tag.

**Signature** — the Ed25519 signature over the canonical signature base, base64url-encoded.

**Verification** — the recipient re-derives the signature base from the live request values and verifies the signature against the key fetched from JWKS.

---

## Usage

### 1. Generate a keypair

```ts
import { generateKeyPair, exportKeyPairToJwk } from "@hathbanger/tap-core"

const keyPair = await generateKeyPair()
// keyPair.keyId     → "abc123..." (JWK thumbprint, stable key identifier)
// keyPair.privateKey → CryptoKey (sign)
// keyPair.publicKey  → CryptoKey (verify)
// keyPair.jwk        → TapJwk    (publish to /.well-known/jwks)

// Export to JWK for storage / transport
const { privateJwk, publicJwk } = await exportKeyPairToJwk(keyPair)
```

### 2. Sign a request

Pass an ordered list of `[componentName, componentValue]` tuples. You control exactly which parts of the request are covered.

```ts
import { signRequest } from "@hathbanger/tap-core"

// Minimal — authority + path only
const headers = await signRequest(keyPair, [
  ["@authority", "api.example.com"],
  ["@path", "/v1/browse"],
], "agent-browser-auth")

// Full — method + authority + path + body integrity
const headers = await signRequest(keyPair, [
  ["@method", "POST"],
  ["@authority", "api.example.com"],
  ["@path", "/v1/checkout"],
  ["content-type", "application/json"],
  ["content-digest", `sha-256=:${bodyDigestBase64}:`],
], "agent-payer-auth")

// Attach to your fetch call
await fetch("https://api.example.com/v1/checkout", {
  method: "POST",
  headers: { ...headers, "content-type": "application/json" },
  body: JSON.stringify(payload),
})
```

**Covered component names** follow RFC 9421 conventions:
- Derived: `@method`, `@authority`, `@path`, `@target-uri`, `@scheme`, `@query`
- HTTP fields: any lowercase header name — `content-type`, `content-digest`, `authorization`, etc.

**Tags** are application-defined labels. TAP uses:
| Tag | When to use |
|-----|-------------|
| `agent-browser-auth` | Agent browsing / reading data |
| `agent-payer-auth` | Agent initiating a payment as payer |
| `agent-payee-auth` | Agent accepting a payment as payee |

### 3. Verify a signature

The verifier reads the covered component list out of `Signature-Input` and reconstructs the base from the values you supply. Pass a map of all components that could appear.

```ts
import { verifyAgentSignature, fetchPublicKeyFromJwks } from "@hathbanger/tap-core"

const result = await verifyAgentSignature(
  {
    "@method":    req.method,
    "@authority": req.headers.host,
    "@path":      new URL(req.url, "http://x").pathname,
    "content-type": req.headers["content-type"] ?? "",
  },
  req.headers["signature-input"],
  req.headers["signature"],
  (kid) => fetchPublicKeyFromJwks("https://mcp.visa.com/.well-known/jwks", kid)
)

if (!result.valid) {
  return res.status(401).json({ error: result.failureReason })
}

// result.agentKeyId → verified key ID
// result.tag        → "agent-payer-auth" etc.
```

### 4. Load a keypair from stored JWK

```ts
import { loadKeyPairFromJwk } from "@hathbanger/tap-core"

const privateJwk = JSON.parse(fs.readFileSync("agent.private.jwk.json", "utf-8"))
const keyPair = await loadKeyPairFromJwk(privateJwk)
```

### 5. Serve a JWKS endpoint

```ts
import { buildJwksResponse } from "@hathbanger/tap-core"

// In your route handler:
const body = buildJwksResponse([keyPair.jwk])
res.json(body)
// → { keys: [{ kty: "OKP", crv: "Ed25519", x: "...", kid: "...", use: "sig" }] }
```

---

## API Reference

### Keys

| Function | Description |
|----------|-------------|
| `generateKeyPair()` | Generate an Ed25519 `TapKeyPair` with derived key ID |
| `exportKeyPairToJwk(pair)` | Export keypair to `{ privateJwk, publicJwk }` |
| `loadKeyPairFromJwk(privateJwk)` | Reconstruct a `TapKeyPair` from a stored private JWK |
| `importPrivateKey(jwk)` | Import a raw JWK → `CryptoKey` for signing |
| `importPublicKey(jwk)` | Import a raw JWK → `CryptoKey` for verification |

### Signing

| Function | Description |
|----------|-------------|
| `signRequest(keyPair, components, tag)` | Sign an HTTP request — returns `{ "signature-input", "signature" }` |
| `signPayload(keyPair, payload)` | Sign an arbitrary JSON payload — returns base64url signature |
| `buildSignatureBase(components, params)` | Low-level: construct the RFC 9421 signature base string |
| `buildSignatureInputParams(componentNames, params)` | Low-level: serialize the Inner List + params string |

### Verification

| Function | Description |
|----------|-------------|
| `verifyAgentSignature(componentValues, sigInput, sig, fetchKey)` | Verify an RFC 9421 agent signature |
| `parseSignatureInput(header)` | Parse `Signature-Input` → `{ params, components, raw }` |
| `parseSignatureHeader(header)` | Extract raw base64url signature from `Signature` header |
| `verifyConsumerRecognition(obj, nonce, fetchKey)` | Verify a TAP Consumer Recognition Object |
| `verifyPaymentContainer(obj, nonce, fetchKey)` | Verify a TAP Payment Container |
| `verifyAgentPayeeObject(payment, fetchKey)` | Verify a TAP A2A payment credential |

### JWKS

| Function | Description |
|----------|-------------|
| `fetchPublicKeyFromJwks(url, keyId)` | Fetch a key by ID from a JWKS endpoint (5-min cache) |
| `buildJwksResponse(publicJwks)` | Build a `{ keys: [...] }` JWKS response body |

### Nonce / Timestamps

| Function | Description |
|----------|-------------|
| `generateNonce()` | Generate a cryptographically random base64url nonce |
| `makeTimestamps()` | Return `{ created, expires }` with an 8-minute window |
| `validateTimestamps(created, expires)` | Validate timestamp window against current time |
| `nowSeconds()` | Current Unix timestamp in seconds |

### TAP Tokens (Consumer Identity)

| Function | Description |
|----------|-------------|
| `createIdToken(keyPair, claims)` | Create an EdDSA JWT for a Consumer Recognition Object |
| `verifyIdToken(token, publicJwk, audience)` | Verify and decode a TAP ID token |
| `hashPii(value)` | SHA-256 hash a PII value (email / phone) |
| `maskEmail(email)` | Mask email to `a***@domain.com` |
| `maskPhone(phone)` | Mask phone to `***-***-1234` |

---

## Types

```ts
import type {
  TapKeyPair,          // { keyId, privateKey, publicKey, jwk }
  TapJwk,              // JWK with optional kid, use, alg
  TapSignedHeaders,    // { "signature-input": string; signature: string }
  SignatureInputParams,// { tag, keyId, alg, created, expires, nonce }
  SignatureTag,        // "agent-browser-auth" | "agent-payer-auth" | "agent-payee-auth"
  VerificationResult,  // { valid, agentKeyId?, tag?, failureReason? }
} from "@hathbanger/tap-core"
```

---

## Express middleware example

```ts
import express from "express"
import { verifyAgentSignature, fetchPublicKeyFromJwks } from "@hathbanger/tap-core"

const JWKS_URL = process.env.TAP_JWKS_URL ?? "http://localhost:4456/.well-known/jwks"

async function tapAuth(req, res, next) {
  const sigInput = req.headers["signature-input"]
  const sig      = req.headers["signature"]

  if (!sigInput || !sig) return next() // not a TAP request

  const result = await verifyAgentSignature(
    {
      "@method":    req.method,
      "@authority": req.hostname,
      "@path":      req.path,
    },
    sigInput,
    sig,
    (kid) => fetchPublicKeyFromJwks(JWKS_URL, kid)
  )

  if (!result.valid) {
    return res.status(401).json({ error: result.failureReason })
  }

  req.tapAgent = { keyId: result.agentKeyId, tag: result.tag }
  next()
}
```

---

## Spec compliance

This library implements the signature and verification mechanics from [RFC 9421 — HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421). Key points:

- **Signature base** is constructed as specified in §2.5, with each covered component on its own line followed by `"@signature-params"`.
- **Inner List** serialization (§4.1) covers any ordered set of derived components (`@method`, `@authority`, `@path`, etc.) and HTTP field names.
- **Algorithm**: Ed25519 (`alg="ed25519"`) using Web Crypto `subtle.sign/verify`.
- **Key IDs**: derived as a JWK SHA-256 thumbprint (RFC 7638).
- **Timestamp window**: 8 minutes (`created` → `expires`), enforced on both sign and verify.

---

## License

MIT