# Machine Users and Agents (Ed25519)

Machine users and AI agents authenticate with an API key plus an Ed25519 signature over a SHA-256 transcript of the request. This is the authentication method for any automated system, trading bot, or agent that calls the vault without human interaction. See the [Users API reference](/api-reference#tag/users) for the `POST /v1/users` endpoint used to create machine users, and [Whoami](/api-reference#tag/whoami) to verify your identity.

## Prerequisites

1. A machine user exists in the vault with an Ed25519 public key registered against it.
2. You have the API key UUID returned at creation time (or after the most recent rotation).
3. You have the Ed25519 private key whose corresponding public key is registered.
4. The machine user's group has the necessary permissions granted in the Administrative Policy.

## Transcript construction

The vault verifies the `x-request-assertion` header by reconstructing the same transcript you signed and checking it against the public key on file.

The transcript is a binary structure:

```
0x01                                           (1 byte: version)
[4-byte LE uint32 length] HTTP method bytes    (e.g. POST)
[4-byte LE uint32 length] URI path bytes       (no host, no scheme, no normalization)
[4-byte LE uint32 length] URI query bytes      (empty if no query string)
[4-byte LE uint32 length] request body bytes   (empty if no body)
[4-byte LE uint32 length] Authorization value  (scheme prefix and trailing whitespace stripped)
8-byte LE uint64: x-request-timestamp value    (milliseconds since Unix epoch)
8 raw bytes: x-request-nonce value             (the decoded bytes, not the hex string)
```

SHA-256 of the above (37 fixed bytes plus the variable-length field contents) produces the 32-byte transcript challenge. Your Ed25519 private key signs that 32-byte digest. The resulting 64-byte signature is base64url-encoded (RFC 4648 section 5, no padding) and placed in `x-request-assertion`.

:::tip
The `Authorization` field in the transcript is the raw UUID only: strip the `ApiKey ` prefix and any surrounding whitespace before computing the length and appending the bytes.
:::

:::warning\[Clock synchronization]
The vault rejects any request whose `x-request-timestamp` is more than 5 minutes from server time. Keep your host clock synchronized with NTP. Generate the timestamp immediately before building the transcript, not minutes in advance.
:::

## Step-by-step in Node.js

The example below is a complete, runnable implementation. It generates the nonce and timestamp, builds the transcript, signs it, then calls the [sign endpoint](/api-reference#tag/keys). You can adapt it to any other endpoint by changing `method`, `path`, `query`, and `body`.

```js
const crypto = require('crypto');
const fs = require('fs');

const CV_BASE = 'https://<your-vault-host>';
const API_KEY_UUID = '<your-api-key-uuid>';
const PRIVATE_KEY_PEM = fs.readFileSync('./private.pem', 'utf8');

const privateKey = crypto.createPrivateKey(PRIVATE_KEY_PEM);

// Base64url encode without padding.
const b64url = (buf) =>
  buf.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');

/**
 * Build the transcript challenge and sign it.
 *
 * @param {object} params
 * @param {string}  params.method       - HTTP method in uppercase (e.g. "POST")
 * @param {string}  params.path         - URI path with leading slash (e.g. "/v1/keys/my-key/sign")
 * @param {string}  params.query        - URI query string, empty string if absent
 * @param {Buffer}  params.body         - Request body bytes, empty Buffer if absent
 * @param {string}  params.apiKeyUuid   - The raw UUID (no "ApiKey " prefix)
 * @param {number}  params.timestampMs  - Current Unix time in milliseconds
 * @param {Buffer}  params.nonce        - 8 random bytes
 * @returns {Buffer} 32-byte transcript challenge (SHA-256 digest)
 */
function buildTranscriptChallenge({ method, path, query, body, apiKeyUuid, timestampMs, nonce }) {
  const fields = [
    Buffer.from(method, 'utf8'),
    Buffer.from(path, 'utf8'),
    Buffer.from(query, 'utf8'),
    body,
    Buffer.from(apiKeyUuid, 'utf8'),
  ];

  const parts = [Buffer.from([0x01])];

  for (const field of fields) {
    const lenBuf = Buffer.alloc(4);
    lenBuf.writeUInt32LE(field.length, 0);
    parts.push(lenBuf, field);
  }

  const tsBuf = Buffer.alloc(8);
  tsBuf.writeBigUInt64LE(BigInt(timestampMs), 0);
  parts.push(tsBuf, nonce);

  return crypto.createHash('sha256').update(Buffer.concat(parts)).digest();
}

/**
 * Build the four authentication headers for one request.
 */
function buildAuthHeaders({ method, path, query = '', body = Buffer.alloc(0) }) {
  const timestampMs = Date.now();
  const nonce = crypto.randomBytes(8);

  const challenge = buildTranscriptChallenge({
    method,
    path,
    query,
    body,
    apiKeyUuid: API_KEY_UUID,
    timestampMs,
    nonce,
  });

  // Ed25519: sign the 32-byte SHA-256 digest directly.
  const assertion = crypto.sign(null, challenge, privateKey);

  const tsBuf = Buffer.alloc(8);
  tsBuf.writeBigUInt64LE(BigInt(timestampMs), 0);

  return {
    authorization: `ApiKey ${API_KEY_UUID}`,
    'x-request-timestamp': tsBuf.toString('hex'),
    'x-request-nonce': nonce.toString('hex'),
    'x-request-assertion': b64url(assertion),
    'content-type': 'application/json',
  };
}

// Example: sign a message with key "my-key".
async function signMessage(keyId, message) {
  const path = `/v1/keys/${keyId}/sign`;
  const method = 'POST';
  // `message` is the HEX-encoded bytes to sign. A non-hex value returns 400 invalid hex.
  const bodyObj = { message: Buffer.from(message, 'utf8').toString('hex') };
  const body = Buffer.from(JSON.stringify(bodyObj), 'utf8');

  const headers = buildAuthHeaders({ method, path, body });

  const res = await fetch(`${CV_BASE}${path}`, { method, headers, body });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Vault returned ${res.status}: ${text}`);
  }
  const data = await res.json();
  return data.signature;
}
```

## Key generation (create the Ed25519 keypair)

Generate a keypair once. Store the private key securely (for example, in a secrets manager or HSM). The public key goes into the vault when you create the machine user.

```bash
# Generate an Ed25519 key in PEM format (requires OpenSSL 1.1.1+).
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
```

The contents of `public.pem` are what you supply as `public_key` when [creating a user](/api-reference#tag/users) via `POST /v1/users`.

## Common mistakes

**Wrong encoding for `x-request-nonce`:** The header value is the hex encoding of the raw nonce bytes. In the transcript, use the **raw bytes** (the 8 bytes you generated), not the hex string. If you put the hex string into the transcript, the signature will not match.

**Wrong encoding for `x-request-timestamp`:** The header value is a 16-character hex string of the timestamp encoded as a u64 little-endian integer. Use little-endian, not big-endian, not a decimal string.

**Including the `ApiKey ` prefix in the transcript:** The transcript uses the raw UUID only. Strip the scheme prefix before computing the length and appending to the buffer.

**Signing the wrong thing:** Sign the 32-byte SHA-256 digest of the transcript, not the transcript itself. The `crypto.sign(null, challenge, privateKey)` call in Node.js with an Ed25519 key signs the buffer directly without additional hashing, which is correct because the input is already the SHA-256 digest.

**Clock skew:** If you see `401 unauthorized` with a timestamp-related message, verify your host clock is accurate and that you are generating `timestampMs` with `Date.now()` immediately before the request, not caching it across multiple calls.
