#351

GitHub: Actions secrets public key endpoints (feature request)

Openwaynesun09opened 6d ago

Problem

Clients create or update an Actions secret in two steps. They fetch the repository or organization public key, encrypt the value with a libsodium sealed box, and PUT the secret with encrypted_value and key_id. The emulator accepts the PUT request, but it lacks the public key endpoints. The first step fails with 404, so clients never write the secret. Checked on main at b75beb7 with npx emulate@0.13.0.

Reproduction

Seed octocat/hello-world, an organization my-org with octocat as an admin member, and a repro_token for octocat. Then start the GitHub service:

npx emulate@0.13.0 start --service github --port 4001 --seed emulate.config.yaml
curl -s -i http://localhost:4001/repos/octocat/hello-world/actions/secrets/public-key -H 'Authorization: Bearer repro_token'
curl -s -i http://localhost:4001/orgs/my-org/actions/secrets/public-key -H 'Authorization: Bearer repro_token'

Both return:

HTTP/1.1 404 Not Found
{"message":"Not Found","documentation_url":"https://emulate.dev/github"}

Writing the secret already works when a client skips the key lookup:

curl -s -i -X PUT http://localhost:4001/repos/octocat/hello-world/actions/secrets/MY_SECRET \
  -H 'Authorization: Bearer repro_token' -H 'Content-Type: application/json' \
  -d '{"encrypted_value":"c2VhbGVk","key_id":"012345678912345678"}'
# HTTP/1.1 201 Created

curl -s http://localhost:4001/repos/octocat/hello-world/actions/secrets/MY_SECRET -H 'Authorization: Bearer repro_token'
# {"name":"MY_SECRET","visibility":"all","created_at":"2026-11-06T14:31:46.021Z","updated_at":"2026-11-06T14:31:46.021Z"}

GitHub behavior

  • GET /repos/{owner}/{repo}/actions/secrets/public-key and GET /orgs/{org}/actions/secrets/public-key return 200 with { "key_id": "<id>", "key": "<base64 public key>" }. The key is a base64-encoded 32-byte Curve25519 public key, and key_id identifies it.
  • Clients encrypt the secret value with a libsodium sealed box (crypto_box_seal) using that key, then send encrypted_value (base64) and the matching key_id to PUT /repos/{owner}/{repo}/actions/secrets/{secret_name} or PUT /orgs/{org}/actions/secrets/{secret_name}.
  • The PUT request returns 201 on creation and 204 on update. For organization secrets, visibility is required, and selected_repository_ids applies when visibility is selected.
  • Access: Anyone with read access to the repository can read its public key. Reading the organization public key and writing secrets require collaborator access, with the repo or admin:org scope for personal access tokens (classic).

Docs: https://docs.github.com/en/rest/actions/secrets#get-a-repository-public-key and https://docs.github.com/en/rest/actions/secrets#create-or-update-a-repository-secret

Proposed scope

  • GET /repos/{owner}/{repo}/actions/secrets/public-key and GET /orgs/{org}/actions/secrets/public-key, each returning a stable key_id and a valid base64-encoded Curve25519 public key per repository or organization so libsodium clients can encrypt against it.
  • Optional: Reject PUT requests when key_id does not match the current key. GitHub docs do not specify the HTTP status code returned for this case, so the emulator can continue accepting requests until confirmed.
  • Optional: Retain the matching private key so the emulator can decrypt encrypted_value and expose the plaintext through an inspection endpoint, similar to GET /_emulate/installation-tokens. This allows tests to assert the value a client wrote.
  • Out of scope for the initial release: environment, Dependabot, and Codespaces public keys.

Proposed implementation

The secret routes already exist and accept writes:

https://github.com/vercel-labs/emulate/blob/b75beb7ce7d78676d471fff47432b77be6ff6528/packages/@emulators/github/src/routes/actions.ts#L658-L791

The emulator can generate a key pair for each repository and organization using Node's built-in crypto.generateKeyPairSync("x25519"), which produces Curve25519 keys compatible with libsodium sealed boxes. Store the key pair alongside the secrets collection. The public key is the raw 32 bytes of the key, base64-encoded. Generated keys can follow the pattern used for generated GitHub App keys: keep them stable across reset() and list them in generatedSecrets.

Activity

0

No activity yet.