tenseleyflow/shithub / 63d3b07

Browse files

docs/api: GPG keys REST reference

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
63d3b078f6bf2af64f65b310ab6a519004adae37
Parents
2866785
Tree
56f1ccb

1 changed file

StatusFile+-
A docs/public/api/gpg-keys.md 145 0
docs/public/api/gpg-keys.mdadded
@@ -0,0 +1,145 @@
1
+# GPG keys
2
+
3
+OpenPGP public-key CRUD for the authenticating user. JSON shape
4
+is GitHub-exact — every field on
5
+[gh's `/user/gpg_keys`](https://docs.github.com/en/rest/users/gpg-keys)
6
+is present with the same name and nullability so existing clients
7
+work without per-field shims.
8
+
9
+All endpoints require `Authorization: Bearer <pat>` and the
10
+[common API conventions](overview.md) apply.
11
+
12
+## Key shape
13
+
14
+```json
15
+{
16
+  "id": 17,
17
+  "name": "laptop",
18
+  "primary_key_id": null,
19
+  "key_id": "ABCDEF0123456789",
20
+  "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n…",
21
+  "raw_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n…",
22
+  "emails": [
23
+    { "email": "alice@example.com", "verified": true }
24
+  ],
25
+  "subkeys": [
26
+    {
27
+      "id": 0,
28
+      "primary_key_id": 17,
29
+      "key_id": "FEDC...",
30
+      "public_key": "",
31
+      "emails": [],
32
+      "subkeys": [],
33
+      "can_sign": true,
34
+      "can_encrypt_comms": false,
35
+      "can_encrypt_storage": false,
36
+      "can_certify": false,
37
+      "created_at": "2026-05-12T04:00:00Z",
38
+      "expires_at": null,
39
+      "raw_key": null,
40
+      "revoked": false
41
+    }
42
+  ],
43
+  "can_sign": false,
44
+  "can_encrypt_comms": false,
45
+  "can_encrypt_storage": false,
46
+  "can_certify": true,
47
+  "created_at": "2026-05-12T04:00:00Z",
48
+  "expires_at": null,
49
+  "revoked": false,
50
+  "raw_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n…"
51
+}
52
+```
53
+
54
+Field notes:
55
+
56
+- `key_id` / `subkeys[].key_id` — uppercase hex. The primary
57
+  surfaces its short 16-hex key id; full 40-hex fingerprints are
58
+  used internally for verification lookups but not exposed in
59
+  the response (gh-parity).
60
+- `public_key` and `raw_key` — both carry the same armored block.
61
+  gh historically distinguishes them; shithub follows suit for
62
+  client compatibility.
63
+- `can_encrypt_comms` and `can_encrypt_storage` — RFC 4880 §5.2.3.21
64
+  splits encryption capability into two bits; both surface
65
+  honestly. Encryption-only keys (no signing subkey) **are
66
+  accepted**; they just can't verify commits.
67
+- `emails[].verified` — `true` when the UID email matches a
68
+  verified email on the authenticating user's account.
69
+- `expires_at` — null for keys that don't expire.
70
+- `revoked` — `true` after a `DELETE` (soft-delete in the DB).
71
+
72
+## List GPG keys
73
+
74
+```
75
+GET /api/v1/user/gpg_keys
76
+```
77
+
78
+Required scope: `user:read`. Paginated via `?page=` and
79
+`?per_page=` (≤ 100, default 30). Response carries the standard
80
+`Link:` header.
81
+
82
+## Get a single GPG key
83
+
84
+```
85
+GET /api/v1/user/gpg_keys/{id}
86
+```
87
+
88
+Required scope: `user:read`. 404 when the id does not belong to
89
+the authenticating user (existence-leak safe — the same status
90
+is returned regardless of whether the row exists for another
91
+user).
92
+
93
+## Add a GPG key
94
+
95
+```
96
+POST /api/v1/user/gpg_keys
97
+```
98
+
99
+Required scope: `user:write`.
100
+
101
+### Request body
102
+
103
+```json
104
+{
105
+  "name": "laptop",
106
+  "armored_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n…"
107
+}
108
+```
109
+
110
+| Field                | Type   | Notes                                                                  |
111
+|----------------------|--------|------------------------------------------------------------------------|
112
+| `name`               | string | 1–80 characters; user-visible label.                                   |
113
+| `armored_public_key` | string | Full ASCII-armored block, including the BEGIN/END envelope.            |
114
+
115
+Returns `201` on success with the same shape as the list
116
+endpoint. Successful inserts dispatch a background backfill
117
+that retroactively stamps verification rows for any existing
118
+commits whose signing subkey matches.
119
+
120
+### Errors
121
+
122
+| Status | When                                                                                |
123
+|-------:|-------------------------------------------------------------------------------------|
124
+|    401 | PAT missing/invalid.                                                                |
125
+|    403 | PAT lacks `user:write` scope.                                                       |
126
+|    422 | Block unparseable, key already registered, RSA<2048, no UID, expired primary, etc. |
127
+
128
+Rejection classes the parser raises explicitly: private-key
129
+blocks, signature blocks, RSA<2048, DSA-only, expired primary,
130
+no-UID entities. Encryption-only keys with valid encryption
131
+subkeys are accepted with `can_sign: false`.
132
+
133
+## Delete a GPG key
134
+
135
+```
136
+DELETE /api/v1/user/gpg_keys/{id}
137
+```
138
+
139
+Required scope: `user:write`. Returns `204 No Content` on
140
+success. Soft-delete: the row stays in the DB with
141
+`revoked_at = now()`. Verification cache rows that resolved
142
+against the deleted key are invalidated; affected commits
143
+revert to no badge until another matching key is uploaded.
144
+
145
+404 when the id does not belong to the authenticating user.