# Verify a signature yourself

Every image Tarsana builds comes with a signed statement, an attestation, that names the image, its software bill of materials (SBOM) and its package set by their SHA-256 digests. In this quickstart you check that signature and those digests with `openssl`, `jq` and `sha256sum`, without trusting Tarsana's own check.

Before you start, finish [Build your first image](/quickstarts/first-image). You need `TENANT`, `ACCESS_TOKEN` and the `SUBMISSION` of a build that has succeeded. You also need OpenSSL 3.0 or later (`openssl version`).

## 1. Get the attestation

[`attestation`](/api/attestation) returns the signed statement and the public key in one answer:

```sh
curl "https://api.tarsana.io/v1/tenants/$TENANT/submissions/$SUBMISSION/attestation" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
```

Save the answer as `attestation.json`, for example by adding `> attestation.json` to the command. It has this shape:

```json
{
  "attestation": {
    "schema": "tarsana.submission-attestation/v1",
    "key": {
      "keyid": "52f4abfb...",
      "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
      "registered": true,
      "claim": "A sentence that says what a signature by this key covers."
    },
    "signature": {
      "algorithm": "ed25519",
      "format": "in-toto-dsse",
      "keyid": "52f4abfb...",
      "envelope": {
        "payloadType": "application/vnd.in-toto+json",
        "payload": "eyJfdHlwZSI6...",
        "signatures": [
          {
            "algorithm": "ed25519",
            "keyid": "52f4abfb...",
            "sig": "MEUCIQ..."
          }
        ]
      }
    },
    "subject": {
      "image": {
        "digest": "sha256:0f1e2d3c...",
        "attested": true
      },
      "release": {
        "plain_sha256": "9487214a...",
        "plain_size": 2147483648,
        "version": "20261005-030112"
      },
      "sbom": {
        "digest": "sha256:fb917071...",
        "format": "CycloneDX"
      }
    },
    "verification": {
      "verdict": "verified",
      "checks": [
        {
          "id": "signature_verifies",
          "result": "pass",
          "detail": "A sentence about this check."
        }
      ]
    },
    "verify_it_yourself": {
      "why": "A sentence.",
      "steps": []
    }
  }
}
```

`attestation.verification` is Tarsana's own check. The steps below repeat it with your own tools, so you do not have to trust it.

## 2. Check the key

Write the public key to a file:

```sh
jq -r '.attestation.key.public_key' attestation.json > tarsana-signing.pub
```

A key ID is the SHA-256 of the public key in DER form. Compute it yourself and compare it with the key ID the signature names:

```sh
openssl pkey -pubin -in tarsana-signing.pub -outform DER | sha256sum
jq -r '.attestation.signature.envelope.signatures[0].keyid' attestation.json
```

The two must be the same. A key you got from the same answer as the signature only proves that the answer is consistent. Keep a copy of the key, compare it with the copy you get next time, and treat a new key ID as something to look into. `attestation.key.claim` says what a signature by this key covers.

## 3. Check the signature

The signature is a [DSSE](https://github.com/secure-systems-lab/dsse) signature: it covers the statement together with its type. Rebuild the exact bytes that were signed, then check them with the key:

```sh
jq -r '.attestation.signature.envelope.payload' attestation.json | base64 -d > statement.json
jq -r '.attestation.signature.envelope.signatures[0].sig' attestation.json | base64 -d > signature.bin
TYPE=$(jq -r '.attestation.signature.envelope.payloadType' attestation.json)
printf 'DSSEv1 %d %s %d ' "${#TYPE}" "$TYPE" "$(wc -c < statement.json)" > signed.bin
cat statement.json >> signed.bin
openssl pkeyutl -verify -pubin -inkey tarsana-signing.pub -rawin -in signed.bin -sigfile signature.bin
```

If it worked, you see:

```text
Signature Verified Successfully
```

If a single byte of the statement or the signature was changed, you see `Signature Verification Failure` instead.

## 4. Check what the statement covers

The statement lists its subjects: the disk image (`node.raw`), the SBOM and the package set, each with its SHA-256:

```sh
jq -r '.subject[] | "\(.digest.sha256)  \(.name)"' statement.json
```

To check the image itself, download it, decompress it and compare. [`fetch`](/api/fetch) with `artifact` set to `image` gives you a download link that expires:

```sh
curl "https://api.tarsana.io/v1/tenants/$TENANT/submissions/$SUBMISSION/image" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
```

```sh
curl -fL -o node.raw.zst "$(jq -r '.content.image.url' image.json)"
zstd -d node.raw.zst -o node.raw
sha256sum node.raw
```

Here `image.json` is the saved answer of the `fetch` call. The digest of `node.raw` must match the `node.raw` subject of the statement. The signature covers the decompressed image, so compare `node.raw`, not the `.zst` file you downloaded.

## What a pass means

A pass means: this statement was signed by the key you checked, and it names exactly these digests. It does not tell you that the software in the image is free of faults, or that you should trust the key. Read the SBOM with [`fetch`](/api/fetch) (`artifact` set to `sbom`) to see what is in the image.

## Next steps

- [Concepts: attestation](/concepts#attestation).
- [Audit trail](/api/audit): every build, signature and check recorded for your tenant.
