Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Asfaload Documentation

Welcome to the Asfaload documentation. Asfaload is a multig sign-off solution that is:

  • open source
  • auditable as it is using a public git repo as storage
  • self-hostable
  • accountless, the keypair is the identity

Alsfaload provides tools for signing and verifying software release artifacts. The Client CLI documentation explain how to use our command line utility to interact with the backend. The REST API details the endpoints of the backend server.

Sections

Client CLI — How-to Guides

Step-by-step recipes for common operations: generating keys, creating signers files, registering releases, signing, and verification.

Client CLI — Manual

Reference for all client commands: options, environment variables, output formats, and exit codes.

REST API — Manual

Reference for the Asfaload REST API: endpoints, request/response formats, and error codes.

How-to guides

Step-by-step recipes for common operations with the client CLI. Follow them in order for a first-time setup, or jump to the one you need.

Setup

  1. Generate a key pair
  2. Create a signers file

Project registration

  1. Register a repository
  2. Activate a signers file

Releases

  1. Register a release
  2. Sign a release

Maintenance

  1. Update a signers file
  2. Revoke a signed release

Verification

  1. Download a file with signature verification

Generate a key pair

Every signer needs their own key pair. This guide walks you through creating one.

Prerequisites

  • The client binary is installed and in your PATH.

Steps

1. Choose a directory

Pick a directory to store your keys. A common convention is ~/.asfaload/:

mkdir -p ~/.asfaload

2. Generate the key pair

client new-keys --name mykey --output-dir ~/.asfaload

You’ll be prompted for a password to protect the secret key. Pick a strong one — this password is required every time you sign.

This creates two files:

FilePurpose
~/.asfaload/mykeySecret key (keep this safe)
~/.asfaload/mykey.pubPublic key (share with your team)

3. Verify the output

ls ~/.asfaload/mykey*

You should see both mykey and mykey.pub.

Choosing an algorithm

The default algorithm is minisign. If your project requires Ed25519 keys:

client new-keys --name mykey --output-dir ~/.asfaload --algorithm ed25519

All signers in the same signers file must use the same algorithm.

Non-interactive usage

For CI or scripting, pass the password directly:

client new-keys --name ci-key --output-dir ./keys --password "$KEY_PASSWORD"

Or via environment variable:

export ASFALOAD_NEW_KEYS_PASSWORD="$KEY_PASSWORD"
client new-keys --name ci-key --output-dir ./keys

Next step

Share your .pub file with whoever maintains the signers file. They’ll include it when creating the signers file.

Reference

Create a signers file

A signers file defines who can sign artifacts for your project, and how many signatures are needed (the threshold). You create it once, commit it to your repository, then register it with the backend.

Prerequisites

  • Public key files (.pub) for every signer. See Generate a key pair.
  • A target repository where the signers file will live.

Steps

1. Collect the public keys

Gather .pub files from all signers. For this example, three artifact signers with a threshold of 2 (any two out of three must sign):

alice.pub
bob.pub
carol.pub

2. Create the signers file

client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    --artifact-signer-file carol.pub \
    --artifact-threshold 2 \
    --output-file signers.json

The command prints a summary:

Signers file created successfully at: "signers.json"
Artifact signers: 3 (threshold: 2)
Admin keys: 0 (threshold: none)
Master keys: 0 (threshold: none)

3. Commit and push

Place the signers file in your repository and push it. We advise to commit the file in your main branch (eg under a directory .asfaload.signers) or in a dedicated branch of the repo. The best is to not update existing signers files but add a new version alongside it. The backend needs to fetch it by URL during repository registration.

A common location is at the root of your repo:

cp signers.json my-project/asfaload.signers/index.json
cd my-project
git add asfaload.signers/index.json
git commit -m "Add asfaload signers file"
git push

Adding optional key groups

Beyond artifact signers, you can define admin, master, and revocation key groups. Each group has its own keys and threshold.

With revocation keys

Revocation keys can revoke signed releases. Useful to have a separate set of keys for emergency access:

client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    --artifact-signer-file carol.pub \
    --artifact-threshold 2 \
    --revocation-key-file revoke1.pub \
    --revocation-key-file revoke2.pub \
    --revocation-key-file revoke3.pub \
    --revocation-threshold 2 \
    --output-file signers.json

With admin keys

Admin keys can propose signers file updates:

client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    --artifact-threshold 2 \
    --admin-key-file admin.pub \
    --admin-threshold 1 \
    --output-file signers.json

Mixing base64 strings and files

You can pass public keys as base64 strings instead of files. This is handy when keys come from a secrets manager:

client new-signers-file \
    --artifact-signer "minisign:RWQwtmTQyX/sEi37..." \
    --artifact-signer-file bob.pub \
    --artifact-threshold 1 \
    --output-file signers.json

Next step

Register the repository with the backend so it knows where to find your signers file.

Reference

Register a repository

Registering a repository tells the backend where your signers file lives. Once registered, the backend can enforce signature requirements for your project.

Prerequisites

Steps

1. Get the signers file URL

You need the public URL that points to your signers file on the forge. For GitHub:

https://github.com/acme/tool/blob/main/asfaload.signers/index.json

2. Register the repository

client register-repo \
    --secret-key ~/.asfaload/mykey \
    https://github.com/acme/tool/blob/main/asfaload.signers/index.json

On success:

Repository registered successfully!
Project ID: abc123
Required signers (3): alice, bob, carol
Next step: signers must submit signatures to activate the project.

The backend fetches the signers file, creates a pending signers entry, and waits for all listed signers to sign before the project becomes active.

3. Point to a non-default backend

If your backend is not at http://127.0.0.1:3000:

client register-repo \
    --secret-key ~/.asfaload/mykey \
    -u https://asfaload.example.com \
    https://github.com/acme/tool/blob/main/asfaload.signers/index.json

What happens next

After registration, the signers file is in a pending state. Every signer listed in it must sign before the project is activated. See Activate a signers file.

Reference

Activate a signers file

After a repository is registered (or a signers file is updated), the signers file sits in a pending state. Every signer listed in it must sign before the project becomes active. This guide covers the signing round.

Prerequisites

Steps

Each signer performs steps 1–2 independently.

1. Check for pending work

client list-pending --secret-key ~/.asfaload/mykey

If the signers file is waiting for your signature, you’ll see:

Files requiring your signature:
  - https/github.com/443/acme/tool/asfaload.signers.pending/index.json

If nothing is pending for you, the output says No pending signatures found.

2. Sign the pending signers file

Copy the path from the output above and pass it to sign-pending:

client sign-pending --secret-key ~/.asfaload/mykey \
    https/github.com/443/acme/tool/asfaload.signers.pending/index.json

If more signatures are needed:

Success! Signature submitted

When your signature completes the required count (every signer must sign for an initial signers file):

Success! Signature submitted (complete)

3. Verify activation

Once every signer has signed, the signers file moves from pending to active. There is no separate activation step — the last signature triggers it automatically.

Coordinating signers

Signers don’t need to sign in any particular order. The workflow looks like:

alice: client list-pending --secret-key alice.key    → sees pending signers
alice: client sign-pending --secret-key alice.key ...  → "submitted"

bob:   client list-pending --secret-key bob.key      → sees pending signers
bob:   client sign-pending --secret-key bob.key ...    → "submitted"

carol: client list-pending --secret-key carol.key    → sees pending signers
carol: client sign-pending --secret-key carol.key ...  → "submitted (complete)"

Scripting the sign step

For CI, supply the password non-interactively:

client sign-pending \
    --secret-key ~/.asfaload/mykey \
    --password "$KEY_PASSWORD" \
    https/github.com/443/acme/tool/asfaload.signers.pending/index.json

Troubleshooting

“Already completed” error when signing — someone else already provided the final signature. The signers file is active; no action needed.

list-pending returns empty — either your key is not listed in the signers file, or the file has already been fully signed.

Next step

Once the signers file is active, you can register a release for signing.

Reference

Register a release

Once your project’s signers file is active, you can register releases for signing. The backend will index the release assets and start a signature collection round.

Prerequisites

  • The repository is registered and the signers file is activated.
  • The release is published on the forge (e.g., a GitHub release page exists).
  • Your secret key file. See Generate a key pair.

Register a GitHub release

Pass the release page URL:

client register-assets \
    --secret-key ~/.asfaload/mykey \
    --github-release-url https://github.com/acme/tool/releases/tag/v1.0

On success:

Assets registered successfully! Remember you still need to sign it yourself!
Index file path: https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

The backend fetches the release, creates an index of the assets, and waits for signatures.

Register checksum files

If your release uses checksum files instead of a GitHub release:

client register-assets \
    --secret-key ~/.asfaload/mykey \
    --csum-file https://example.com/releases/v1.0/SHA256SUMS \
    --csum-file https://example.com/releases/v1.0/SHA512SUMS

All checksum file URLs must share a common parent path. --csum-file is repeatable; --github-release-url and --csum-file are mutually exclusive.

Re-registration

Registering the same release twice fails — the backend rejects duplicates. Existing signatures are preserved; there’s no risk of losing progress.

What happens next

You registered the release, but you haven’t signed it yet. The backend is now waiting for enough signatures to meet the threshold. See Sign a release.

Reference

Sign a release

After a release is registered, artifact signers must provide enough signatures to meet the threshold defined in the signers file. This is the same list-pending / sign-pending flow used for activating a signers file, but applied to a release index.

Prerequisites

  • A release has been registered.
  • Your key is listed as an artifact signer in the active signers file.

Steps

1. List pending files

client list-pending --secret-key ~/.asfaload/mykey
Files requiring your signature:
  - https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

2. Sign the release index

client sign-pending --secret-key ~/.asfaload/mykey \
    https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

The command fetches all files associated with the release, hashes each one, signs the hashes, and submits everything in a single request.

If more signatures are needed:

Success! Signature submitted

When the threshold is met:

Success! Signature submitted (complete)

3. Check progress

At any point, you can check whether the threshold has been reached:

client signature-status --secret-key ~/.asfaload/mykey \
    https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json
https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json: pending

or

https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json: complete

Example: two-of-three threshold

With three artifact signers and a threshold of 2, only two need to sign:

alice: client sign-pending --secret-key alice.key ...  → "submitted"
bob:   client sign-pending --secret-key bob.key ...    → "submitted (complete)"
# carol doesn't need to sign — threshold already met

Next step

Once the release is fully signed, users can download it with verification.

Reference

Update a signers file

Need to add a signer, remove one, or change the threshold? This guide covers proposing and activating a signers file update.

Prerequisites

  • The repository is already registered and the current signers file is active.
  • A new signers file has been created (see Create a signers file), committed, and pushed to the forge.
  • Your secret key file. See Generate a key pair.

Steps

1. Create and push the new signers file

Generate a new signers file with the updated set of keys and thresholds:

client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    --artifact-signer-file carol.pub \
    --artifact-signer-file dave.pub \
    --artifact-threshold 3 \
    --revocation-key-file revoke1.pub \
    --revocation-key-file revoke2.pub \
    --revocation-key-file revoke3.pub \
    --revocation-threshold 2 \
    --output-file signers_v2.json

Commit and push it to your repository so the backend can fetch it by URL. We advise to commit the file either in your main branch, or in a dedicated branch in which you save all signers files updates.

2. Propose the update

client update-signers \
    --secret-key ~/.asfaload/mykey \
    https://github.com/acme/tool/blob/main/asfaload.signers/index.json

On success:

Signers update proposed successfully!
Project ID: abc123
Required signers (4): alice, bob, carol, dave
Next step: signers must submit signatures to activate the update.

3. All signers must sign

Just like initial activation, every signer listed in the new signers file must sign before the update takes effect. This includes both existing and newly added signers.

Each signer runs:

client list-pending --secret-key ~/.asfaload/mykey
client sign-pending --secret-key ~/.asfaload/mykey \
    https/github.com/443/acme/tool/asfaload.signers.pending/index.json

See Activate a signers file for the full signing flow.

What about existing releases?

Releases signed under the previous signers file remain valid. The backend keeps a signers chain history, so older releases can still be verified against the signers file that was active when they were signed.

Reference

Revoke a signed release

If a release needs to be recalled — a vulnerability was found, or the wrong artifacts were published — you can revoke it. Revoked files can no longer be downloaded with verification.

Prerequisites

  • The release is fully signed and active on the backend.
  • Your key is listed as a revocation key in the active signers file. Artifact signers without revocation privileges cannot revoke.
  • The signers file has a revocation group with a threshold. See Create a signers file.

Steps

1. Initiate the revocation

client revoke \
    --secret-key ~/.asfaload/revoke-key \
    https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

On success:

Success! File revoked: https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

If the revocation threshold is 1, the file is revoked immediately. If the threshold is higher, the revocation enters a pending state and more revocation signers must co-sign.

2. Co-sign the revocation (if threshold > 1)

When the revocation threshold requires multiple signatures, additional revocation signers use list-pending and sign-pending to add their signatures:

# Another revocation signer checks for pending work
client list-pending --secret-key ~/.asfaload/revoke-key-2

The pending revocation shows up as a path ending in .revocation.json.pending:

Files requiring your signature:
  - https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json.revocation.json.pending

Sign it:

client sign-pending --secret-key ~/.asfaload/revoke-key-2 \
    https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json.revocation.json.pending

Once the threshold is met, the revocation is finalized.

3. Verify the revocation

Attempting to download a revoked file fails:

client download https://github.com/acme/tool/releases/download/v1.0/artifact.bin
This file has been revoked.
  Revoked at: 2025-03-15T10:30:00Z
  Revoked by: minisign:RWQwtmTQyX/sEi37...

Important notes

  • Only revocation keys can revoke. Artifact signers listed in the artifact_signers group cannot initiate or co-sign a revocation.
  • Revocation is irreversible. Once a file is revoked, it stays revoked.
  • Other releases are unaffected. Revoking v1.0 does not impact v2.0.
  • Re-initiating a pending revocation fails. If a revocation is already pending, starting another one for the same file is rejected.

Reference

Download a file with signature verification

The download command fetches a file and verifies its signatures before saving it to disk. If the signatures don’t check out, or the file has been revoked, the download is aborted.

Prerequisites

  • The file has been signed on the backend (signatures meet the threshold).
  • The backend is running and reachable.

Steps

1. Download a release artifact

Pass the original download URL — the same URL you’d use to download from GitHub or your forge:

client download \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

The command prints each verification step:

Starting download: https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz
✓ Downloaded signers file (1234 bytes)
✓ Downloaded index file (567 bytes)
✓ Downloaded signatures file (890 bytes)
✓ Signatures verified successfully (2 valid)
Downloading tool-linux-amd64.tar.gz
  Size: 12.50 MB
Progress: 100.0% (12.50 MB / 12.50 MB)
✓ Download complete (12.50 MB)
✓ File hash verified (SHA-256)
✓ File saved to: ./tool-linux-amd64.tar.gz
✓ All done! Verified 2 signature(s)

2. Choose where to save

By default, the file is saved in the current directory using the filename from the URL. Use -o to specify a different path:

client download -o /tmp/tool.tar.gz \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

Full signers chain verification

By default, only the current signers file is verified. For stronger assurance — especially if the signers file has been updated since the release was signed — use --full-check:

client download --full-check \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

This walks the full signers chain history and verifies each entry against the forge, catching tampering in historical signers files. You’ll see an additional verification line:

✓ Signers chain history verified (3 entries)

Overriding forge detection

The CLI auto-detects the forge type from the URL. If detection fails or you’re using a generic file server:

client download --type fileserver \
    https://files.example.com/tool/v1.0/tool.tar.gz

Available types: github, gitlab, fileserver.

Pointing to a non-default backend

client download -u https://asfaload.example.com \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

What happens with revoked files

If the file has been revoked, the download fails:

This file has been revoked.
  Revoked at: 2025-03-15T10:30:00Z
  Revoked by: minisign:RWQwtmTQyX/sEi37...

Reference

client-cli manual

Reference for all client commands.

Keys

  • new-keys — generate a new signing key pair

Signers

Registration

  • register-repo — register a repository with the backend
  • register-assets — register assets (GitHub release or checksum files) for signing

Signing

Revocation

  • revoke — revoke a previously signed file

Verification

  • download — download a file with signature verification

client new-keys

Generate a new signing key pair. The command creates both a secret key and a public key in the specified directory.

Options

-n --name <NAME>

Base name for the key files. Produces <NAME> (secret key) and <NAME>.pub (public key) in the output directory.

-o --output-dir <DIR>

Directory to write the key files into. Created automatically if it doesn’t exist.

-a --algorithm <ALGORITHM>

Signing algorithm to use. Defaults to minisign.

ValueDescription
minisignMinisign format (default)
ed25519Raw Ed25519

-p --password <PASSWORD>

Password to protect the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

--accept-weak-password

Bypass password strength validation. Insecure — only use for testing.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_NEW_KEYS_PASSWORD — alternative to --password.
  • ASFALOAD_NEW_KEYS_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Generating Minisign keypair with name 'mykey' in directory "/home/user/.asfaload"
Public key saved at /home/user/.asfaload/mykey.pub and secret key at /home/user/.asfaload/mykey

JSON (with --json):

{"public_key_path":"/home/user/.asfaload/mykey.pub","secret_key_path":"/home/user/.asfaload/mykey"}

Examples

# generate a minisign key pair
client new-keys -n mykey -o ~/.asfaload

# generate an ed25519 key pair
client new-keys -n mykey -o ~/.asfaload -a ed25519

# non-interactive usage in CI
client new-keys -n ci-key -o ./keys -p "$KEY_PASSWORD"

Exit codes

  • 0 — key pair created successfully.
  • non-zero — error (invalid directory, password mismatch, etc.).

client new-signers-file

Create a new signers file that defines who can sign artifacts, administer, and manage the project. At minimum, one artifact signer and its threshold are required.

Options

Artifact signers

Every signers file needs at least one artifact signer.

-a --artifact-signer <BASE64_KEY>

Public key as a base64 string. Repeatable.

--artifact-signer-file <PATH>

Path to a .pub key file. Repeatable. Combines with --artifact-signer.

-A --artifact-threshold <N>

Number of artifact signers required to complete a signature. Must be between 1 and the total number of artifact signers.

Admin keys (optional)

-d --admin-key <BASE64_KEY>

Admin public key as a base64 string. Repeatable.

--admin-key-file <PATH>

Path to an admin .pub key file. Repeatable.

-D --admin-threshold <N>

Required when admin keys are provided. Number of admins required to approve changes.

Master keys (optional)

-m --master-key <BASE64_KEY>

Master public key as a base64 string. Repeatable.

--master-key-file <PATH>

Path to a master .pub key file. Repeatable.

-M --master-threshold <N>

Required when master keys are provided.

Revocation keys (optional)

-r --revocation-key <BASE64_KEY>

Revocation public key as a base64 string. Repeatable.

--revocation-key-file <PATH>

Path to a revocation .pub key file. Repeatable.

-R --revocation-threshold <N>

Required when revocation keys are provided.

General

-o --output-file <PATH>

Path for the new signers file. The file must not already exist — the command refuses to overwrite. Parent directories are created automatically.

--json

Emit output as JSON instead of human-readable text.

Output

Human-readable (default):

Signers file created successfully at: "signers.json"
Artifact signers: 3 (threshold: 2)
Admin keys: 1 (threshold: 1)
Master keys: 0 (threshold: none)
Revocation keys: 0 (threshold: none)

JSON (with --json):

{"output_file":"signers.json","artifact_signers_count":3,"artifact_threshold":2,"admin_keys_count":1,"admin_threshold":1,"master_keys_count":0,"master_threshold":null,"revocation_keys_count":0,"revocation_threshold":null}

Examples

# minimal: two artifact signers, threshold of 2
client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    -A 2 \
    -o signers.json

# with admin and revocation groups
client new-signers-file \
    --artifact-signer-file alice.pub \
    --artifact-signer-file bob.pub \
    -A 2 \
    --admin-key-file admin.pub \
    -D 1 \
    --revocation-key-file revoke.pub \
    -R 1 \
    -o signers.json

# mix base64 strings and files
client new-signers-file \
    -a "minisign:RWQwtmTQyX/sEi37..." \
    --artifact-signer-file bob.pub \
    -A 1 \
    -o signers.json

Exit codes

  • 0 — signers file created.
  • non-zero — error (output file exists, invalid threshold, missing keys, etc.).

client update-signers

Propose an update to an existing project’s signers file. The backend fetches the new file from the forge and starts a signature collection round — signers from the current configuration must approve the change before it takes effect.

Arguments

<SIGNERS_FILE_URL>

Public URL to the new signers file on the forge. For example:

https://raw.githubusercontent.com/owner/repo/main/asfaload.signers/index.json

Options

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_UPDATE_SIGNERS_PASSWORD — alternative to --password.
  • ASFALOAD_UPDATE_SIGNERS_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Signers update proposed successfully!
Project ID: abc123
Required signers (2): alice, bob
Next step: signers must submit signatures to activate the update.

JSON (with --json):

{"success":true,"project_id":"abc123","required_signers":["alice","bob"],"message":""}

Examples

# propose a signers update
client update-signers -K ~/.asfaload/key.minisign \
    https://raw.githubusercontent.com/acme/tool/main/asfaload.signers/index.json

Exit codes

  • 0 — update proposed.
  • non-zero — error (authentication failure, invalid signers file, network error).

client register-repo

Register a new repository with the backend. Points the backend at your signers file so it knows which keys are authorized to sign artifacts for this project.

After registration, all signers listed in the file must submit their signatures to activate the project.

Arguments

<SIGNERS_FILE_URL>

Public URL to the signers file on the forge. For example:

https://raw.githubusercontent.com/owner/repo/main/asfaload.signers/index.json

Options

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_REGISTER_REPO_PASSWORD — alternative to --password.
  • ASFALOAD_REGISTER_REPO_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Repository registered successfully!
Project ID: abc123
Required signers (2): alice, bob
Next step: signers must submit signatures to activate the project.

JSON (with --json):

{"success":true,"project_id":"abc123","required_signers":["alice","bob"],"message":""}

Examples

# register a GitHub-hosted signers file
client register-repo -K ~/.asfaload/key.minisign \
    https://raw.githubusercontent.com/acme/tool/main/asfaload.signers/index.json

# with explicit backend
client register-repo -K ~/.asfaload/key.minisign \
    -u https://asfaload.example.com \
    https://raw.githubusercontent.com/acme/tool/main/asfaload.signers/index.json

Exit codes

  • 0 — repository registered.
  • non-zero — error (authentication failure, invalid signers file URL, network error).

client register-assets

Register assets for signing. Tell the backend about a new set of files — either a GitHub release or one or more checksum files — so that the signature collection process can begin.

After registration you still need to sign the assets yourself with sign-pending.

Options

--github-release-url <URL>

URL of a GitHub release page. The backend fetches the release assets automatically. Mutually exclusive with --csum-file.

--csum-file <URL>

URL of a checksums file. Repeatable — pass once per file. All URLs must share a common parent path. Mutually exclusive with --github-release-url.

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_REGISTER_ASSETS_PASSWORD — alternative to --password.
  • ASFALOAD_REGISTER_ASSETS_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Assets registered successfully! Remember you still need to sign it yourself!
Index file path: https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json

JSON (with --json):

{"success":true,"message":"","index_file_path":"https/github.com/443/acme/tool/releases/tag/v1.0/asfaload.index.json"}

Examples

# register a GitHub release
client register-assets -K ~/.asfaload/key.minisign \
    --github-release-url https://github.com/acme/tool/releases/tag/v1.0

# register checksum files
client register-assets -K ~/.asfaload/key.minisign \
    --csum-file https://example.com/releases/v1.0/SHA256SUMS \
    --csum-file https://example.com/releases/v1.0/SHA512SUMS

Exit codes

  • 0 — assets registered.
  • non-zero — error (authentication failure, invalid URL, mutually exclusive flags, network error).

client list-pending

List all files on the backend that still need your signature. The command authenticates with your secret key and returns only the files where your public key is among the expected signers.

Options

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit the backend response as JSON instead of human-readable text.

Environment

  • ASFALOAD_LIST_PENDING_PASSWORD — alternative to --password.
  • ASFALOAD_LIST_PENDING_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default), when files are pending:

Files requiring your signature:
  - https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json
  - https/github.com/443/acme/repo/releases/tag/v2.0/asfaload.index.json

When nothing is pending:

No pending signatures found.

JSON (with --json):

{"file_paths":["https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json"]}

Examples

# list pending files
client list-pending -K ~/.asfaload/key.minisign

# non-interactive, piped into sign-pending
client list-pending --json -K ~/.asfaload/key.minisign -p "$PASSWORD" \
    | jq -r '.file_paths[]'

Exit codes

  • 0 — query succeeded (even if no files are pending).
  • non-zero — error (authentication failure, network error).

client sign-pending

Sign a pending file. The command fetches all files associated with the given path from the backend, computes a SHA-512 hash of each, signs them with your secret key, and submits the signatures in a single request.

Use list-pending to discover which files need signing.

Arguments

<FILE_PATH>

Mirror-relative path to the file to sign, as returned by list-pending. For example https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json.

Options

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_SIGN_PENDING_PASSWORD — alternative to --password.
  • ASFALOAD_SIGN_PENDING_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Success! Signature submitted

When the submission completes the required threshold:

Success! Signature submitted (complete)

JSON (with --json):

{"is_complete":true}

Examples

# sign a pending release index
client sign-pending -K ~/.asfaload/key.minisign \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

# sign with explicit password (CI usage)
client sign-pending -K ~/.asfaload/key.minisign -p "$PASSWORD" \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

Exit codes

  • 0 — signature submitted successfully.
  • non-zero — error (authentication failure, file not found, network error).

client signature-status

Query the backend for the signature collection status of a file. The caller must be an authorized signer in the file’s current signers file; see the linked endpoint for the exact authorization rule.

Arguments

<FILE_PATH>

Mirror-relative path to the file to query, for example https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json.

Options

-K --secret-key <PATH>

Path to the caller’s secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit the backend response as JSON instead of human-readable text.

Environment

  • ASFALOAD_SIGNATURE_STATUS_PASSWORD — alternative to --password.
  • ASFALOAD_SIGNATURE_STATUS_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

<file_path>: pending

or

<file_path>: complete

JSON (with --json):

{"file_path":"<file_path>","is_complete":false}

Examples

# check a pending release index
client signature-status -K ~/.asfaload/key.minisign \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

# use a non-default backend
client signature-status -K ~/.asfaload/key.minisign -u https://asfaload.example.com \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

# machine-readable output
client signature-status --json -K ~/.asfaload/key.minisign \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

Exit codes

  • 0 — query succeeded.
  • non-zero — error (authentication failure, not authorized, file not found, network error).

client revoke

Revoke a previously signed file on the mirror. The command fetches the file from the backend, builds a revocation document (timestamped, with the initiator’s public key and the file’s SHA-512 digest), signs it, and submits it.

Once revoked, clients that download the file will see a revocation warning.

Arguments

<FILE_PATH>

Mirror-relative path to the signed file, for example https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json.

Options

-K --secret-key <PATH>

Path to your secret key file. Required.

-p --password <PASSWORD>

Password for the secret key. Conflicts with --password-file. Prompted interactively if neither is set.

-P --password-file <PATH>

File containing the password. Conflicts with --password.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--json

Emit output as JSON instead of human-readable text.

Environment

  • ASFALOAD_REVOKE_PASSWORD — alternative to --password.
  • ASFALOAD_REVOKE_PASSWORD_FILE — alternative to --password-file.

Output

Human-readable (default):

Success! File revoked: https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

JSON (with --json):

{"success":true,"message":""}

Examples

# revoke a release index
client revoke -K ~/.asfaload/key.minisign \
    https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json

Exit codes

  • 0 — file revoked.
  • non-zero — error (authentication failure, file not found, not authorized, network error).

client download

Download a file and verify its signatures before saving. The command fetches the signers file, index, and signatures from the backend, checks everything is valid, then downloads the actual file and verifies its hash.

If the file has been revoked, a warning is printed to stderr and the download is aborted.

Arguments

<FILE_URL>

Public URL of the file to download. For example:

https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

Options

-o --output <PATH>

Output file path. Defaults to the filename extracted from the URL.

-u --backend-url <URL>

Backend API URL. Defaults to http://127.0.0.1:3000.

--type <FORGE_TYPE>

Override automatic forge type detection.

ValueDescription
githubGitHub release
gitlabGitLab release
fileserverGeneric file server

--full-check

Verify the full signers chain history during download. Without this flag only the current signers file is checked.

Output

The command prints progress to stdout as each verification step completes:

Starting download: https://github.com/acme/tool/releases/download/v1.0/tool.tar.gz
✓ Downloaded signers file (1234 bytes)
✓ Downloaded index file (567 bytes)
✓ Downloaded signatures file (890 bytes)
✓ Signatures verified successfully (2 valid)
✓ Signers chain history verified (3 entries)
Downloading tool.tar.gz
  Size: 12.50 MB
Progress: 100.0% (12.50 MB / 12.50 MB)
✓ Download complete (12.50 MB)
✓ File hash verified (SHA-256)
✓ File saved to: ./tool.tar.gz
✓ All done! Verified 2 signature(s)

If the file has been revoked:

This file has been revoked.
  Revoked at: 2025-03-15T10:30:00Z
  Revoked by: minisign:RWQwtmTQyX/sEi37...

Examples

# download and verify a release artifact
client download \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

# save to a specific path
client download -o /tmp/tool.tar.gz \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

# verify the full signers chain history
client download --full-check \
    https://github.com/acme/tool/releases/download/v1.0/tool-linux-amd64.tar.gz

# override forge detection
client download --type gitlab \
    https://gitlab.com/acme/tool/-/releases/v1.0/downloads/tool.tar.gz

Exit codes

  • 0 — download and verification succeeded.
  • non-zero — error (verification failure, revoked file, network error).

rest-api manual

Reference for the Asfaload REST API.

Registration

Signatures

Files

Signers

Revocation

Assets

  • POST /v1/assets — register assets from a GitHub release or checksums files

POST /v1/register_repo

Register a new project with the signing server. The server fetches the signers file from the forge URL, validates it, creates the directory structure, records the first signature, and commits the result to the backing Git repository.

A project can only be registered once. Calling this endpoint again for an already-registered project returns an error.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Request body

JSON object:

{
  "signers_file_url": "https://github.com/acme/repo/blob/main/asfaload.signers.json",
  "public_key": "<base64-public-key>"
}

Fields:

  • signers_file_url — URL pointing to the signers file on the forge (GitHub, GitLab, or file server).
  • public_key — base64-encoded Ed25519 public key of the submitter. Must match one of the keys in the signers file.

Response

200 OK

{
  "success": true,
  "project_id": "https/github.com/443/acme/repo",
  "message": "Project registered successfully. Collect signatures to activate.",
  "required_signers": ["<base64-public-key-1>", "<base64-public-key-2>"],
  "signature_submission_url": "/v1/signatures"
}

Fields:

  • success — always true on success.
  • project_id — normalised identifier for the registered project.
  • message — human-readable status message.
  • required_signers — list of base64-encoded public keys that still need to sign.
  • signature_submission_url — path to use for submitting signatures.

Errors

  • 400 Bad Request — invalid or unparseable forge URL, or invalid public key.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 409 Conflict — project is already registered or registration is in progress.
  • 500 Internal Server Error — forge validation, signers initialisation, or Git commit failed.

Examples

Successful registration

curl -sS -X POST 'http://127.0.0.1:3000/v1/register_repo' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "signers_file_url": "https://github.com/acme/repo/blob/main/asfaload.signers.json",
    "public_key": "<base64-public-key>"
  }'

{"success":true,"project_id":"https/github.com/443/acme/repo","message":"Project registered successfully. Collect signatures to activate.","required_signers":["<base64-pk-1>","<base64-pk-2>"],"signature_submission_url":"/v1/signatures"}

Project already registered

HTTP/1.1 409 Conflict

{"error":"Project 'https/github.com/443/acme/repo' is already registered or registration is in progress."}

POST /v1/update_signers

Propose an update to a project’s signers file. The server fetches the new signers file from the forge, validates it, writes it as a pending proposal, and commits the result. The project must already be registered and have an active signers file.

Like initial registration, the update requires all new signers to submit their signatures before it takes effect.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Request body

JSON object (same shape as register_repo):

{
  "signers_file_url": "https://github.com/acme/repo/blob/main/asfaload.signers.json",
  "public_key": "<base64-public-key>"
}

Fields:

  • signers_file_url — URL pointing to the updated signers file on the forge.
  • public_key — base64-encoded Ed25519 public key of the submitter.

Response

200 OK

{
  "success": true,
  "project_id": "https/github.com/443/acme/repo",
  "message": "Signers update proposed successfully. Collect signatures to activate.",
  "required_signers": ["<base64-public-key-1>", "<base64-public-key-2>"],
  "signature_submission_url": "/v1/signatures"
}

Fields:

  • success — always true on success.
  • project_id — normalised project identifier.
  • message — human-readable status message.
  • required_signers — list of base64-encoded public keys that need to sign the update.
  • signature_submission_url — path to use for submitting signatures.

Errors

  • 400 Bad Request — project not registered, no active signers file, invalid forge URL, or invalid public key.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 500 Internal Server Error — forge validation, proposal creation, or Git commit failed.

Examples

Successful update proposal

curl -sS -X POST 'http://127.0.0.1:3000/v1/update_signers' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "signers_file_url": "https://github.com/acme/repo/blob/main/asfaload.signers.json",
    "public_key": "<base64-public-key>"
  }'

{"success":true,"project_id":"https/github.com/443/acme/repo","message":"Signers update proposed successfully. Collect signatures to activate.","required_signers":["<base64-pk-1>","<base64-pk-2>"],"signature_submission_url":"/v1/signatures"}

Project not registered

HTTP/1.1 400 Bad Request

{"error":"Project 'https/github.com/443/acme/repo' is not registered. Register the repo first."}

POST /v1/signatures

Submit one or more signatures for a file. The server validates each signature against the signer’s public key and the file content, adds it to the pending collection, and commits the result to Git. Once all required signatures are collected, the aggregate signature is marked complete.

For signers files, the request must include signatures for both the signers file itself and its metadata file.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Request body

JSON object:

{
  "file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
  "public_key": "<base64-public-key>",
  "signatures": {
    "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json": "<base64-signature>"
  }
}

Fields:

  • file_path — mirror-relative path to the primary file being signed.
  • public_key — base64-encoded Ed25519 public key of the signer.
  • signatures — map of file paths to their base64-encoded Ed25519 signatures. Must include at least the primary file_path. For signers files, include the metadata file path as well.

Response

200 OK

{
  "is_complete": false
}

Fields:

  • is_completetrue when all required signatures have been collected; false while signatures are still pending.

Errors

  • 400 Bad Request — empty file path, file not found, invalid public key or signature format, or no signature provided for the primary file.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 409 Conflict — signature already collected for this key, or signature already added.
  • 500 Internal Server Error — signature collection or Git commit failed.

Examples

Successful submission (collection not yet complete)

curl -sS -X POST 'http://127.0.0.1:3000/v1/signatures' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
    "public_key": "<base64-public-key>",
    "signatures": {
      "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json": "<base64-signature>"
    }
  }'

{"is_complete":false}

File not found

HTTP/1.1 400 Bad Request

{"error":"File not found: https/github.com/443/acme/repo/releases/tag/v1.0/missing.json"}

GET /v1/signatures/{file_path}

Return the current signature collection status of a file. Authorization is checked against the current global signers file, not the copy frozen at the file’s registration time; this keeps revocation semantics uniform across all authenticated endpoints.

Path parameters

file_path

Mirror-relative path to the file. Slashes are preserved (the route uses a catch-all parameter).

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Response

200 OK

{
  "file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
  "is_complete": false
}

Fields:

  • file_path — normalized mirror-relative path to the file.
  • is_completetrue once the aggregate signature threshold has been met; false while signatures are still being collected.

Errors

  • 400 Bad Request — path is empty.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 403 Forbidden — caller’s public key is not in the file’s current signers file.
  • 404 Not Found — no file exists at the given path, or no signers file could be located for it.
  • 500 Internal Server Error — backend failure (read, parse, or actor error).

Examples

Successful query

curl -sS 'http://127.0.0.1:3000/v1/signatures/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>'

{"file_path":"https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json","is_complete":false}

Caller not an authorized signer

curl -sS -i 'http://127.0.0.1:3000/v1/signatures/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-non-signer-public-key>'

HTTP/1.1 403 Forbidden

GET /v1/pending_signatures

  • Auth: required
  • Source: src/handlers.rsget_pending_signatures_handler

List all files that still need the caller’s signature. The server walks the repository, finds files with pending aggregate signatures, and filters to those where the caller is an authorized signer who has not yet signed.

The returned paths point to the artifact files themselves, not to the .signatures.json.pending files used internally.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Response

200 OK

{
  "file_paths": [
    "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
    "https/github.com/443/acme/repo/asfaload.signers.pending/asfaload.signers.json"
  ]
}

Fields:

  • file_paths — list of mirror-relative paths to files awaiting the caller’s signature. Empty array if nothing is pending.

Errors

  • 401 Unauthorized — missing or invalid authentication headers.
  • 500 Internal Server Error — failed to scan repository or check signer authorization.

Examples

Files pending signature

curl -sS 'http://127.0.0.1:3000/v1/pending_signatures' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>'

{"file_paths":["https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json"]}

Nothing pending

{"file_paths":[]}

GET /v1/files/{file_path}

Fetch the raw content of a file from the repository. Returns the file as an application/octet-stream byte stream. This is a public endpoint — no authentication is required.

The server validates the path against directory traversal attempts before reading.

Path parameters

file_path

Mirror-relative path to the file. Slashes are preserved (the route uses a catch-all parameter).

Response

200 OK

Raw file bytes with headers:

  • Content-Type: application/octet-stream
  • Content-Length: <size-in-bytes>

Errors

  • 400 Bad Request — invalid file path or path traversal detected, or path points to a directory.
  • 404 Not Found — file does not exist.
  • 500 Internal Server Error — failed to read the file.

Examples

Fetch a file

curl -sS 'http://127.0.0.1:3000/v1/files/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json' \
  -o index.json

File not found

curl -sS -i 'http://127.0.0.1:3000/v1/files/https/github.com/443/acme/repo/releases/tag/v1.0/missing.json'

HTTP/1.1 404 Not Found

{"error":"File not found: https/github.com/443/acme/repo/releases/tag/v1.0/missing.json"}

GET /v1/files-to-sign/{file_path}

Fetch the contents of all files that need to be signed for a given artifact. For regular files, the response contains only the primary file. For signers files, it includes both the signers file and its metadata file. All contents are base64-encoded.

This endpoint is used by the client CLI during the sign-pending workflow to retrieve file contents before computing signatures locally.

Path parameters

file_path

Mirror-relative path to the file. Slashes are preserved (the route uses a catch-all parameter).

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Response

200 OK

For a regular artifact:

{
  "files": {
    "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json": "<base64-content>"
  }
}

For a signers file (includes metadata):

{
  "files": {
    "https/github.com/443/acme/repo/asfaload.signers.pending/asfaload.signers.json": "<base64-content>",
    "https/github.com/443/acme/repo/asfaload.signers.pending/asfaload.signers.json.metadata.json": "<base64-content>"
  }
}

Fields:

  • files — map of mirror-relative file paths to their base64-encoded contents.

Errors

  • 400 Bad Request — invalid file path.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 404 Not Found — file does not exist.
  • 500 Internal Server Error — failed to determine file type, read file, or locate metadata file for a signers file.

Examples

Fetch files for a release artifact

curl -sS 'http://127.0.0.1:3000/v1/files-to-sign/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>'

{"files":{"https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json":"<base64-content>"}}

File not found

HTTP/1.1 404 Not Found

{"error":"File not found: https/github.com/443/acme/repo/releases/tag/v1.0/missing.json"}

GET /v1/get_signers/{file_path}

Fetch the signers configuration that applies to a given path. The server walks parent directories from the given path upward until it finds an active signers file, then returns its raw JSON content. This is a public endpoint — no authentication is required.

Path parameters

file_path

Mirror-relative path to any file or directory in the repository. The server locates the nearest signers file by traversing parent directories. Slashes are preserved (the route uses a catch-all parameter).

Response

200 OK

Raw signers file JSON with headers:

  • Content-Type: application/json
  • Content-Length: <size-in-bytes>

The body is the signers configuration file as stored on disk — a SignersConfig JSON object.

Errors

  • 400 Bad Request — invalid file path.
  • 404 Not Found — no signers file found in any parent directory of the given path.
  • 500 Internal Server Error — failed to read the signers file.

Examples

Fetch signers for a release artifact

curl -sS 'http://127.0.0.1:3000/v1/get_signers/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json'

Returns the content of the nearest asfaload.signers/asfaload.signers.json ancestor.

No signers file found

curl -sS -i 'http://127.0.0.1:3000/v1/get_signers/https/github.com/443/unknown/repo/file.txt'

HTTP/1.1 404 Not Found

{"error":"No signers file found for: https/github.com/443/unknown/repo/file.txt. Error: No signers file found in parent directories"}

GET /v1/get_signers_chain/{artifact_path}

Fetch the signers history chain for a signed artifact. The server traces the artifact’s local signers copy back to its source commit, reads the history file and all associated signers/metadata/signature files at that point in time, and returns the chain of signers configurations that were active up to and including the one used to sign the artifact.

This endpoint is useful for verifying the full provenance of an artifact’s signing authority.

Path parameters

artifact_path

Mirror-relative path to the signed artifact. Slashes are preserved (the route uses a catch-all parameter).

Response

200 OK

{
  "history": {
    "entries": [
      {
        "signers_config": "...",
        "signatures": "...",
        "metadata": "...",
        "metadata_signatures": "...",
        "timestamp": "2024-04-11T12:00:00Z"
      }
    ]
  }
}

Fields:

  • history — a HistoryFile object containing the chain of signers configurations. Each entry includes the signers config, its signatures, metadata, metadata signatures, and the timestamp when it became active. The chain is filtered to entries relevant to the artifact’s signing time.

Errors

  • 400 Bad Request — invalid artifact path or cannot derive signers path.
  • 500 Internal Server Error — failed to trace signers source, read files from Git history, or build the chain.

Examples

Fetch the signers chain

curl -sS 'http://127.0.0.1:3000/v1/get_signers_chain/https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json'

{"history":{"entries":[...]}}

Invalid artifact path

curl -sS -i 'http://127.0.0.1:3000/v1/get_signers_chain/invalid'

HTTP/1.1 400 Bad Request

{"error":"Invalid artifact path: ..."}

POST /v1/revoke

Revoke a previously signed file. The caller provides a revocation document (as a JSON string), a signature over its SHA-512 digest, and their public key. The server validates that the file has a complete aggregate signature, verifies the revocation authorization, and records the revocation.

Only files with a complete aggregate signature can be revoked. Files that are still collecting signatures or already revoked are rejected.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Request body

JSON object:

{
  "file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
  "revocation_json": "{\"reason\":\"compromised\",\"timestamp\":1712860800}",
  "signature": "<base64-signature>",
  "public_key": "<base64-public-key>"
}

Fields:

  • file_path — mirror-relative path to the signed file being revoked.
  • revocation_json — JSON string of the revocation document (RevocationInfo).
  • signature — base64-encoded Ed25519 signature of the SHA-512 digest of revocation_json.
  • public_key — base64-encoded Ed25519 public key of the revoker.

Response

200 OK

{
  "success": true,
  "message": "File revoked successfully"
}

Fields:

  • success — always true on success.
  • message — human-readable confirmation.

Errors

  • 400 Bad Request — empty file path, invalid public key or signature format, digest mismatch, file already revoked, or revocation authorization failed.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 404 Not Found — file does not exist.
  • 409 Conflict — file has not been fully signed yet.
  • 500 Internal Server Error — revocation processing or Git commit failed.

Examples

Successful revocation

curl -sS -X POST 'http://127.0.0.1:3000/v1/revoke' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json",
    "revocation_json": "{\"reason\":\"compromised\",\"timestamp\":1712860800}",
    "signature": "<base64-revocation-signature>",
    "public_key": "<base64-public-key>"
  }'

{"success":true,"message":"File revoked successfully"}

File not fully signed

HTTP/1.1 409 Conflict

{"error":"File has not been fully signed: https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json"}

POST /v1/assets

Register assets for signing. Accepts either a GitHub release URL or a list of checksums file URLs — exactly one must be provided.

In GitHub mode, the server fetches the release metadata, downloads all assets, builds an index file, and commits everything to the repository. In checksums mode, it downloads the referenced checksums files, builds the index, and commits.

Request headers

Standard Asfaload authentication headers, signed by the caller’s secret key:

  • X-asfld-timestamp — Unix timestamp, seconds.
  • X-asfld-nonce — random nonce.
  • X-asfld-sig — Ed25519 signature over the canonical request string.
  • X-asfld-pk — caller’s public key.

Request body

JSON object with exactly one of the two fields set:

GitHub release mode

{
  "github_release_url": "https://github.com/acme/repo/releases/tag/v1.0"
}

Checksums mode

{
  "csum_files": [
    "https://example.com/releases/v1.0/SHA256SUMS",
    "https://example.com/releases/v1.0/SHA512SUMS"
  ]
}

Fields:

  • github_release_url — full URL to a GitHub release page. Must point to a known GitHub host. Mutually exclusive with csum_files.
  • csum_files — list of URLs to checksums files. All URLs must share the same origin and parent directory. Mutually exclusive with github_release_url.

Response

200 OK

{
  "success": true,
  "message": "Release registered successfully",
  "index_file_path": "https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json"
}

Fields:

  • success — always true on success.
  • message — human-readable status. Either “Release registered successfully” (GitHub mode) or “Assets registered successfully” (checksums mode).
  • index_file_path — mirror-relative path to the generated index file.

Errors

  • 400 Bad Request — both or neither fields provided, invalid URL format, non-GitHub host for github_release_url, or invalid checksums URLs.
  • 401 Unauthorized — missing or invalid authentication headers.
  • 409 Conflict — release already registered.
  • 500 Internal Server Error — release processing, checksums download, or Git commit failed.

Examples

Register a GitHub release

curl -sS -X POST 'http://127.0.0.1:3000/v1/assets' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "github_release_url": "https://github.com/acme/repo/releases/tag/v1.0"
  }'

{"success":true,"message":"Release registered successfully","index_file_path":"https/github.com/443/acme/repo/releases/tag/v1.0/asfaload.index.json"}

Register checksums files

curl -sS -X POST 'http://127.0.0.1:3000/v1/assets' \
  -H 'Content-Type: application/json' \
  -H 'X-asfld-timestamp: 1712860800' \
  -H 'X-asfld-nonce: <random-nonce>' \
  -H 'X-asfld-sig: <base64-signature>' \
  -H 'X-asfld-pk: <base64-public-key>' \
  -d '{
    "csum_files": ["https://example.com/releases/v1.0/SHA256SUMS"]
  }'

{"success":true,"message":"Assets registered successfully","index_file_path":"https/example.com/443/releases/v1.0/asfaload.index.json"}

Mutually exclusive fields

HTTP/1.1 400 Bad Request

{"error":"github_release_url and csum_files are mutually exclusive"}