TrustGraph

Browse api referenceAPI referenceAttestationsBind a deviceContent certificatesDevice authorityEdgesEpoch commitmentsExternal signing keysGit signer membershipGraph keysPublic handlesHealth and readinessNodes and account deletionReachabilityRelationship tags

External signing keys

Attach and resolve an existing OpenPGP signing key.

Auth: Own-device signed read, operation list-external-keys.

Request and response

Angle-bracket values describe placeholders, including nested objects and numeric types; these are wire shapes, not captured service data. Replace them before sending.

POST /v1/external-keys/list HTTP/1.1
Host: trust-staging.rebellion.systems
Content-Type: application/json

{
  "graphNodeId": "<own-node-id>",
  "authorization": {
    "graphKeyId": "<device-id>",
    "nonce": "<fresh-random-nonce>",
    "signedAt": "<current-ISO-timestamp>",
    "signature": "<base64-DER-signature>"
  }
}

Response shape:

{
  "externalKeys": [
    {
      "externalKeyId": "<40-hex-fingerprint>",
      "kind": "openpgp",
      "graphNodeId": "<own-node-id>",
      "primaryFingerprint": "<40-hex-primary-fingerprint>",
      "attachedAt": "<timestamp>",
      "revokedAt": "<timestamp-or-null>"
    }
  ]
}

An ordinary graph device can attach one selected OpenPGP primary or signing subkey to its node using both a device signature and OpenPGP proof of possession. All routes require a usable, approved graph-purpose device. Bearer credentials and content-signing devices cannot authorize these requests.

Attach

POST /v1/external-keys/attach takes:

{
  "action": {
    "action": "attach-external-key",
    "graphKeyId": "device-id",
    "externalKeyId": "40-character-lowercase-fingerprint",
    "materialDigest": "64-character-lowercase-sha256",
    "proofDigest": "64-character-lowercase-sha256",
    "nonce": "fresh-random-base64url-nonce",
    "signedAt": "2026-11-04T12:00:00.000Z"
  },
  "signature": "base64-device-action-signature",
  "externalKey": {
    "kind": "openpgp",
    "publicKey": "ASCII-armored public key block"
  },
  "proof": "ASCII-armored clearsigned proof"
}

The proof signs exactly this single ASCII line, without a trailing newline:

trustgraph-external-key-v1 node=<graphNodeId> key=<externalKeyId> digest=<materialDigest> nonce=<nonce>

materialDigest is SHA-256 of sanitized protocol-framed public key material, not the raw armor. proofDigest is SHA-256 of the sanitized canonical framed proof signature packet. Use the integration's reviewed OpenPGP preparation implementation; do not substitute a hash of the submitted strings. Freeze material and proof before signing the device action.

The clearsigned proof contains exactly one type 0x01 signature from the selected eligible key. Its hashed creation time must be within one hour of signedAt. The action itself follows the normal five-minute freshness window and durable nonce rules.

Success is 201 {externalKey}. Attach, revoke and list use this public record:

export type ExternalKeyRecord = {
  externalKeyId: string;
  kind: 'openpgp';
  graphNodeId: string;
  primaryFingerprint: string;
  attachedAt: string;
  revokedAt: string | null;
};

Revoke or list

POST /v1/external-keys/:externalKeyId/revoke accepts {action, signature}. Sign revoke-external-key with graphKeyId, externalKeyId, nonce and signedAt; omit the attach digests. The signed fingerprint must exactly match the URL or the request returns 403. Success is 200 {externalKey}.

POST /v1/external-keys/list takes {graphNodeId, authorization}, read operation list-external-keys. It is own-node only and returns {externalKeys}, including revoked records. The per-node limit is 32 total records, counting revoked keys.

Resolve a signer

POST /v1/external-keys/resolve-signer takes either {graphNodeId, signingFingerprint, authorization} or {graphNodeId, signingKeyId, authorization}. Sign operation resolve-external-signer over the exact query. The node ID is your own; supply exactly one locator, never null or both.

Fingerprints are 40 lowercase hex characters; key IDs are their last 16. Key-ID resolution requires a unique reserved fingerprint before lifecycle filtering. Missing, ambiguous, revoked or inactive membership returns the same 404 EXTERNAL_KEY_NOT_FOUND.

Success returns {certificate, status}. The certificate includes v: "trust-graph-external-key-certificate-v1", issuer, certificate ID, attestation key ID, proven fingerprint, kind, parent fingerprint, base64 signingKeyPacket, node ID, attachment time, issuance, expiry and signature. The packet is the public-key packet body, not an armored block. Lifetime is 24 hours.

Status uses v: "trust-graph-external-key-status-v1" and includes the complete certificate digest, authorization nonce, evaluation time, valid / revoked / unavailable, nullable revocation time and issuer signature. There is no separate external-key status endpoint.

Verify both issuer signatures and pins; recompute the v4 fingerprint from the packet and match the requested locator. Require attachment no later than issuance, issuance no more than 60 seconds ahead, a positive certificate lifetime at most 24 hours, unexpired evidence, and status evaluation within 60 seconds of your clock. Verify the document signature locally. If a path check takes more than 30 seconds after status evaluation, resolve again and require unchanged node and fingerprint before presenting the result.

Before displaying a successful result, recheck expiry. Bound its lifetime by the earliest of certificate expiry, document-signature expiry (when present), status evaluation + 60 seconds, path evaluation + 60 seconds (when used), and check time + 60 seconds. A revoked status must carry a revocation time no later than evaluation; other statuses use null.

Supported material and errors

Supported v4 keys are RSA 2048–8192, ECDSA P-256/P-384 and legacy-OID Ed25519, with SHA-256/384/512 signatures. Public-key armor is capped at 64 KiB; proof armor at 8 KiB. Signing subkeys require verified binding and back-signature. Other key formats can be refused rather than imported partially.

CodeHTTP
EXTERNAL_KEY_REJECTED, EXTERNAL_KEY_UNSUPPORTED, EXTERNAL_KEY_MALFORMED400
EXTERNAL_KEY_PROOF_INVALID, EXTERNAL_KEY_ACTION_UNAUTHORIZED401
EXTERNAL_KEY_UNAVAILABLE409
EXTERNAL_KEY_NOT_FOUND404
EXTERNAL_KEYS_SERVER_UNAVAILABLE, EXTERNAL_KEY_PERSISTENCE_FAILED503

The existing schema, freshness, lifecycle and rate-limit errors also apply. Capability availability depends on the server deployment; unavailable storage returns 503.

Does not establishAttachment establishes current control of one selected key, not control of its siblings or its parent. It makes no claim about historical signing time. The full submitted key block reaches the service for processing; public records omit key material and proof text. Membership alone does not establish a trusted path, human authorship or document safety.
Checked against server 9f47f2d · SDK 12053abLast updated 2026-11-04Edit on GitHub