> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/hinkal-api/description/attestation.md).

# Attestation

The enclave exposes a `GET /attestation` endpoint that lets anyone verify what code is actually running inside the Trusted Execution Environment.

## What is attestation?

The enclave runs inside a **GCP Confidential VM** (AMD SEV-SNP), which means its memory is encrypted at the hardware level and inaccessible to the host, Google, or Hinkal's own infrastructure. GCP's Confidential Space service can issue a signed JWT — called an **attestation token** — that proves:

* The exact Docker image digest that is currently running
* That it is running inside a genuine AMD SEV-SNP hardware enclave
* That secure boot is enabled and debugger access is disabled

This JWT is signed by Google's Confidential Space attestation service and verifiable against Google's public OIDC keys.

## The Hinkal-API-Enclave repository

Every time Hinkal deploys a new version of the enclave-api, the following files are automatically published to [github.com/Hinkal-Protocol/Hinkal-API-Enclave](https://github.com/Hinkal-Protocol/Hinkal-API-Enclave):

| File                 | Contents                                                                                                 |
| -------------------- | -------------------------------------------------------------------------------------------------------- |
| `digest.txt`         | SHA256 digest of the deployed Docker image + the git commit SHA it was built from (both signed together) |
| `bundle.json`        | Sigstore/cosign bundle: signature, Fulcio certificate, and Rekor transparency log inclusion proof        |
| `enclave-api-src/`   | Full TypeScript source of the enclave-api at that commit                                                 |
| `enclave-api-dist/`  | Compiled JavaScript bundle (enclave-api + all shared libraries)                                          |
| `enclaveApiGcp.yaml` | The GitHub Actions workflow used to build, sign, and deploy                                              |

## How to verify

{% stepper %}
{% step %}

## Verify the cosign bundle

Install [cosign](https://docs.sigstore.dev/cosign/system_config/installation/), download `digest.txt` and `bundle.json` from the Hinkal-API-Enclave repository, then run:

```bash
cosign verify-blob \
  --bundle bundle.json \
  --certificate-identity-regexp "https://github.com/Hinkal-Protocol/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  digest.txt
```

This proves that `digest.txt` (containing both the image digest and the commit SHA) was signed by a GitHub Actions workflow in the `Hinkal-Protocol` org, and that the signing event is recorded in Rekor's public transparency log.
{% endstep %}

{% step %}

## Verify the running enclave matches the digest

Call the attestation endpoint with a UUID nonce you generate yourself:

```bash
curl "https://api.hinkal.io/attestation?nonce=$(uuidgen)"
```

Response:

```json
{
  "jwt": "eyJhbGciOiJSUzI1NiIs...",
  "imageDigest": "sha256:01c6cb76481dd3601c5cdbd899d95c95a75e5874998360187219819b511767c4",
  "verificationPublicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----\n",
  "nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

`imageDigest` is extracted from `submods.container.image_digest` in the decoded JWT. Compare it to the first line of `digest.txt` from the Hinkal-API-Enclave repository. If they match, the running enclave is the image whose provenance you verified in Step 1.

`verificationPublicKey` is an EC P-256 public key generated by the enclave at startup. It is embedded as the `aud` claim in the JWT — because the JWT is signed by Google's attestation service, this proves the key was generated inside the TEE and belongs to this specific enclave instance.
{% endstep %}

{% step %}

## Verify the JWT signature

The JWT is signed by Google's Confidential Space attestation service using RS256. Decode the payload to inspect it:

```bash
node -e "const p='<jwt>'.split('.')[1]; console.log(JSON.stringify(JSON.parse(Buffer.from(p,'base64url').toString()),null,2))"
```

Key fields to inspect:

| Field                            | Description                                                                         |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| `submods.container.image_digest` | Digest of the running image — matches `digest.txt` line 1                           |
| `eat_nonce`                      | Your nonce — proves this token was issued for your specific request                 |
| `aud`                            | The enclave's `verificationPublicKey` — proves the key was generated inside the TEE |
| `hwmodel`                        | `GCP_AMD_SEV_SNP` — confirms AMD SEV-SNP hardware                                   |
| `secboot`                        | `true` — secure boot enabled                                                        |
| `dbgstat`                        | `disabled-since-boot` — debugger access disabled                                    |
| `iss`                            | `https://confidentialcomputing.googleapis.com` — Google issued this token           |

To verify the JWT signature cryptographically, fetch Google's public keys and check the RS256 signature over the JWT header and payload. The signing keys are published at:

```
https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com
```

This endpoint is documented in [Google's Confidential Space attestation token validation reference](https://cloud.google.com/confidential-computing/confidential-space/docs/reference/token-validation-endpoint-fields).

Find the full Node.js example in the [Hinkal-API-Enclave repository README](https://github.com/Hinkal-Protocol/Hinkal-API-Enclave#step-3--verify-the-jwt-signature-optional).
{% endstep %}
{% endstepper %}

## Using the public key to verify Hinkal API responses

Every Hinkal API endpoint returns an `x-hinkal-response-signature` response header — a base64-encoded ECDSA SHA-256 signature over the raw JSON response body, signed with the EC P-256 private key corresponding to `verificationPublicKey`.

Every authenticated request includes a per-request `nonce` (UUID) in the body or query string.\
The enclave echoes this nonce back in the response body. The echoed nonce, covered by\
`x-hinkal-response-signature`, binds the response to the specific request and proves the enclave processed\
that exact request — not a cached or replayed response.

**Important:** you cannot simply fetch `verificationPublicKey` from `/attestation` and use it directly — that would only prove you talked to a server that returned a key, not that the key came from the TEE. Before trusting the key you must:

1. Verify the JWT signature (Google signed it) — see Step 3 above.
2. Confirm the JWT's `aud` claim equals the returned `verificationPublicKey` — this is what proves the key was generated inside the TEE.
3. Store the verified key and use it for all subsequent response verification.

Only after these checks does a valid `x-hinkal-response-signature` constitute proof that the response was produced by the TEE.

```js
import { createVerify, createPublicKey } from 'crypto';

// 1. fetch attestation
const nonce = randomUUID();
const { jwt, verificationPublicKey } = await fetch(`https://api.hinkal.io/attestation?nonce=${nonce}`).then(r => r.json());

// 2. verify JWT signature and confirm aud matches verificationPublicKey
// (see Step 3 above for the full example — find signing key by kid, verify RS256 signature)
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());
if (payload.eat_nonce !== nonce) throw new Error('nonce mismatch');
if (payload.aud !== verificationPublicKey) throw new Error('key not attested by JWT');

// 3. store the verified key, then use it to verify a response
const response = await fetch('https://api.hinkal.io/deposit', { method: 'POST', body: ... });
const rawBody = await response.text();
const signature = response.headers.get('x-hinkal-response-signature');

const verify = createVerify('SHA256');
verify.update(rawBody);
const valid = verify.verify({ key: verificationPublicKey, dsaEncoding: 'ieee-p1363' }, signature, 'base64');

// 4. confirm the nonce in the response matches what you sent
const responseJson = JSON.parse(rawBody);
if (responseJson.nonce !== yourRequestNonce) throw new Error('nonce mismatch — response does not correspond to this request');
```

> The signature uses IEEE P1363 encoding. `dsaEncoding: 'ieee-p1363'` is required — Node.js `createVerify` defaults to DER.

## The full trust chain

```
enclave-api-src/ at commit SHA (digest.txt line 2)
    ↓ built by Hinkal-Protocol GitHub Actions (proven by bundle.json)
digest.txt line 1: sha256:01c6cb76...
    ↓ matches running enclave (proven by GCP JWT)
GET /attestation?nonce=<your-uuid> → imageDigest + verificationPublicKey
    ↓ nonce in JWT eat_nonce proves token is fresh and bound to your request
    ↓ verificationPublicKey in JWT aud proves it was generated inside the TEE
JWT signed by Google Confidential Space
    ↓ verificationPublicKey verifies x-hinkal-response-signature on every response
    ↓ response body includes request nonce, binding the response to the specific request
```

## Key rotation

You do not need to call `/attestation` before every request. Fetch `verificationPublicKey` once at startup (or on first use), verify the JWT, and store the key. Reuse it for all subsequent response verification — there is no need to re-attest on each call.

`verificationPublicKey` does change whenever the server restarts, so the stored key can become outdated over time. If response signature verification fails, treat it as a potential key rotation and:

1. Re-fetch `/attestation` with a new nonce.
2. Verify the new JWT signature and confirm its `aud` matches the new `verificationPublicKey`.
3. Retry verification with the updated key.

Only treat the response as invalid if verification still fails after re-attesting.
