Skip to content

feat(devices): operator-provisioned device credentials#299

Merged
passcod merged 5 commits into
mainfrom
provisioned-device-credentials
Jul 1, 2026
Merged

feat(devices): operator-provisioned device credentials#299
passcod merged 5 commits into
mainfrom
provisioned-device-credentials

Conversation

@passcod

@passcod passcod commented Jul 1, 2026

Copy link
Copy Markdown
Member

🤖 Operator-provisioned device credentials (spec DPK).

Getting a keypair-authenticated device with a non-server role (releaser, backup-restore) previously meant generating a keypair off-platform and authenticating by hand. This adds an operator-driven flow: create a device at the right role, and Canopy mints the keypair, stores only the public key, and hands the private key back once — encrypted under a generated passphrase, decryptable with bestool crypto reveal.

How it works

  • Canopy generates an ECDSA P-256 keypair server-side and stores only the DER SubjectPublicKeyInfo as an active device_keys row, at the operator's chosen role. The credential is immediately valid — no enrolment handshake.
  • The private key is returned once, as a standard age/scrypt file (base64) under a fresh 4-word passphrase, using the same primitives as mint_enrollment. It is never persisted or logged; losing it means provisioning a new one.
  • The download is a raw age v1 file, so bestool crypto reveal <file>.age decrypts it directly — no bestool changes.

Surface

  • POST /api/devices/provision_credential — new device at a role, or an added key on an existing device.
  • UI: "Create device" on the trusted list and "Provision credential" on the device page, both a one-shot dialog (copy passphrase, download .age, shown-once warning).

Notes

  • The SPKI is derived exactly as the mTLS path extracts it (subject_pki.raw), and a round-trip test decrypts the returned blob and asserts the revealed private key's public half is byte-identical to the stored key.
  • All trustable roles are eligible, including server. This is a weaker posture than server self-enrolment (Canopy briefly holds the private key and it transits a download); it is deliberate per the design discussion.

A follow-up PR retires the untrusted role and fixes server-binding to trust as server.

passcod and others added 5 commits July 1, 2026 17:20
…dentials

Backend for spec DPK: an admin provisions a device at a chosen role and
Canopy mints the keypair, stores only the public SPKI, and returns the
private key once as an age/passphrase-encrypted blob for one-off download
(decryptable with bestool crypto reveal). Private key is never persisted.

- commons-servers device_auth::keygen: P-256 keygen, SPKI derived exactly
  as the mTLS path extracts it
- Device::create_at_role for a device trusted from the outset
- devices::provision_credential handler (new or existing device)
- share generate_passphrase between servers and devices fns

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create-device button on the trusted list and a Provision credential action
on the device page, both opening a one-shot dialog that shows the generated
passphrase and downloads the age-encrypted key file (spec DPK).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Endpoint tests for spec DPK: new-device and existing-device provisioning,
role update, untrusted rejected (400), missing device 404, and a round-trip
that decrypts the returned blob and asserts the revealed private key's SPKI
equals the stored public key. Adds commons-tests spki_from_key_pem helper.
Provision handler returns 400 (BadRequest) for the untrusted role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@passcod passcod enabled auto-merge July 1, 2026 05:44
@passcod passcod added this pull request to the merge queue Jul 1, 2026
Merged via the queue into main with commit a6ce840 Jul 1, 2026
7 checks passed
@passcod passcod deleted the provisioned-device-credentials branch July 1, 2026 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant