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

# Signing keys

> Generate, sign, trust, and revoke Ed25519 publisher keys.

Every plugin the agent installs by default must be signed by an Ed25519
key the agent trusts. This page covers the publisher-side lifecycle:
generation, signing, the on-device trust list, and revocation.

## Why Ed25519

Small keys (32 bytes), small signatures (64 bytes), constant-time
verification, no parameter choices to misconfigure. The same algorithm
and verifier sign OTA payloads, so the agent's verification code already
exists and is well-tested.

## Generate a key pair

The agent CLI mints a keypair in one step:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
ados plugin keygen acme-2026-A --output-dir keys
```

This writes two files under `keys/`:

* `acme-2026-A.pem` is the public key (mode 0644). This is what operators
  install at `/etc/ados/plugin-keys/<signer-id>.pem`.
* `acme-2026-A.priv.pem` is the private key (mode 0600). The signing rig
  keeps this. Never check it in, never copy it across hosts without an
  encrypted transport.

The first positional argument is the signer id. Pass `--force` to
overwrite existing files at the target paths. `keygen` prints a SHA-256
fingerprint of the public key so an operator can cross-check the
installed key against what you generated.

<Warning>
  `keygen` is a developer aid. Production publisher keys deserve a
  hardware-token or air-gapped workflow, not a one-liner. See
  [Key storage](#key-storage).
</Warning>

The reference repo template at `altnautica/ADOSExtensions` carries a
`.gitignore` that covers `keys/*.pem`.

### Alternative: openssl

`openssl` produces an equivalent PEM keypair:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir -p keys
openssl genpkey -algorithm ed25519 -out keys/acme-2026-A.priv.pem
openssl pkey -in keys/acme-2026-A.priv.pem -pubout -out keys/acme-2026-A.pem
```

The public PEM (`acme-2026-A.pem`) is what operators add to their trust
list; its basename is the signer id. Keep the private key out of git.

## Pick a signer key id

The signer key id is a short, stable label. It is the first line of the
archive's `SIGNATURE` file and the basename of the public key in the
trust list. Operators see it on the install dialog. Use a namespace
prefix and a generation suffix, for example `acme-2026-A`.

The id is not a secret. It is a stable identifier for trust management.

## Pack and sign

`ados plugin sign` packs a plugin directory and signs it in one command:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
ados plugin sign ./my-extension \
  --key keys/acme-2026-A.priv.pem \
  --signer-id acme-2026-A \
  --output dist/com.example.foo-1.0.0.adosplug
```

All four are required: the plugin directory (it must contain a
`manifest.yaml`), `--key` (the Ed25519 private key in PEM), `--signer-id`
(written to the `SIGNATURE` file, and it must match the public-key
filename on the agent), and `--output` (the path for the signed archive).

The command computes the canonical payload hash (SHA-256 over the sorted
`<path>\n<sha256-hex>\n` lines of every entry except `SIGNATURE`), signs
that 32-byte digest with the Ed25519 private key, and writes a
`SIGNATURE` file (the signer id on the first line, then the base64
signature) into the archive. Because the hash covers every entry, the
signature protects the manifest and every asset at once.

### Alternative: pack.sh and sign.sh (monorepo CI)

The `altnautica/ADOSExtensions` repo ships two scripts for its tagged
release pipeline. `pack.sh` builds the GCS bundle and zips the archive;
`sign.sh` adds the signature.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
# 1. build the unsigned archive -> dist/<id>-<version>.adosplug
scripts/pack.sh my-extension

# 2. sign it -> dist/<id>-<version>.signed.adosplug
ADOS_SIGNING_KEY=keys/acme-2026-A.priv.pem \
ADOS_SIGNING_KEY_ID=acme-2026-A \
  scripts/sign.sh dist/com.example.foo-1.0.0.adosplug
```

`sign.sh` writes a `*.signed.adosplug` archive next to the input.
`ADOS_SIGNING_KEY` is a path to the private key (PEM or raw 32-byte). The
signer id defaults to `altnautica-2026-A` when `ADOS_SIGNING_KEY_ID` is
unset. The payload hash and `SIGNATURE` format are identical to the CLI,
so an archive from either path verifies the same way on the agent.

## Trust list on the agent

The agent trusts a directory of PEM public keys:

```
/etc/ados/plugin-keys/
  altnautica-2026-A.pem      # first-party Altnautica signer
  acme-2026-A.pem            # your key, for self-signed deployments
```

Each file is a PEM public key whose basename is the signer id. The agent
verifies an archive's signature against the key whose id matches the
`SIGNATURE` file's first line. A signature from a signer id with no
matching file is rejected. After adding or removing a key, restart the
supervisor:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
sudo systemctl restart ados-supervisor
```

A short hardcoded allowlist of first-party signer ids
(`altnautica-2026-A`, `altnautica-2026-B`) is maintained in agent code,
not by filename, so a planted key file cannot impersonate first-party
status. First-party status is what unlocks the in-process agent
isolation and inline GCS isolation modes; third-party plugins always run
subprocess and iframe.

## Revocation

Revoked signer ids live in a JSON file on the agent:

```
/etc/ados/plugin-revocations.json
```

The file is a flat JSON array of signer ids:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
["example-2025-X", "acme-2026-A"]
```

A plugin signed by a revoked id refuses to load. Revocation is the
response when a key is compromised: add the id to the list, re-sign your
current releases with a fresh key, and notify operators. For the full
incident flow, see
[Revocation and incidents](/developers/revocation-and-incidents).

## CI signing

A tagged release can sign in CI with the key held in a repository
secret. Pass the base64-encoded private key inline:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
env:
  ADOS_SIGNING_KEY: ${{ secrets.ADOS_SIGNING_KEY }}
  ADOS_SIGNING_KEY_INLINE: "1"
  ADOS_SIGNING_KEY_ID: ${{ secrets.ADOS_SIGNING_KEY_ID }}
```

`ADOS_SIGNING_KEY_INLINE=1` tells `sign.sh` to treat `ADOS_SIGNING_KEY`
as a base64 key body rather than a file path; it writes it to a tempfile,
signs, and wipes it. Hold the secret in a protected environment with
required reviewers if the key is high-value.

## Key storage

For developer iteration, a file on disk is fine. Pin tight permissions
and never commit it:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
chmod 0400 keys/acme-2026-A.priv.pem
```

For production keys, keep the private key off general-purpose disks. A
hardware security module, a smartcard with an Ed25519 applet, or a
TPM-sealed key all reduce the blast radius of a host compromise. The
signing input is just the 32-byte payload digest, so any tool that can
produce an Ed25519 signature over that digest can replace `sign.sh` in a
hardened pipeline.

## Self-signed for closed deployments

A fleet that never pulls from a public registry signs and trusts end to
end without Altnautica involvement:

1. Generate a key pair locally.
2. Place the public key at `/etc/ados/plugin-keys/<your-id>.pem` on every
   agent and restart the supervisor.
3. Sign your plugins with the matching private key.
4. Distribute via internal HTTPS, USB, or your existing fleet channel,
   then `ados plugin install <path>` against the local archive file.

No outbound calls, no external trust dependency. The trust chain is
operator-end-to-end.

## See also

* [Manifest reference](/developers/manifest)
* [Revocation and incidents](/developers/revocation-and-incidents)
* [Local file install](/developers/distribution-local-install)
