MySQL · 2959 bytes Raw Blame History
1 -- SPDX-License-Identifier: AGPL-3.0-or-later
2 --
3 -- OpenPGP public keys associated with users. Used to verify the
4 -- signature on a commit or annotated tag and render the "Verified"
5 -- badge. Companion table user_gpg_subkeys (0067) carries the per-
6 -- subkey reverse-lookup index that the verification hot path joins
7 -- against — a commit signature carries the *subkey's* fingerprint,
8 -- not the primary's.
9 --
10 -- fingerprint is unique across ALL users (partial index, where
11 -- revoked_at is null) — two users registering the same key would
12 -- produce ambiguous verification lookups. Soft-delete via revoked_at
13 -- preserves audit history and lets re-upload of the same fingerprint
14 -- after revoke succeed.
15 --
16 -- armored holds the ASCII-armored block exactly as uploaded so we
17 -- can round-trip it back over REST and email; the parsed capability
18 -- flags + uids + subkey metadata are decoded once at insert time
19 -- and stored alongside so the REST response doesn't re-parse on read.
20 --
21 -- can_encrypt_comms vs can_encrypt_storage split per RFC 4880
22 -- §5.2.3.21 to match GitHub's /user/gpg_keys response shape exactly.
23 -- can_authenticate is stored but not surfaced over REST in S51
24 -- (GitHub doesn't surface it either; the column lets S52/S53 expose
25 -- it later without a schema change).
26
27 -- +goose Up
28 CREATE TABLE user_gpg_keys (
29 id bigserial PRIMARY KEY,
30 user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
31 name text NOT NULL DEFAULT '',
32 fingerprint text NOT NULL,
33 key_id text NOT NULL,
34 armored text NOT NULL,
35 can_sign boolean NOT NULL,
36 can_encrypt_comms boolean NOT NULL,
37 can_encrypt_storage boolean NOT NULL,
38 can_certify boolean NOT NULL,
39 can_authenticate boolean NOT NULL,
40 uids text[] NOT NULL DEFAULT '{}',
41 subkeys jsonb NOT NULL DEFAULT '[]'::jsonb,
42 primary_algo text NOT NULL,
43 created_at timestamptz NOT NULL DEFAULT now(),
44 last_used_at timestamptz,
45 revoked_at timestamptz,
46 expires_at timestamptz,
47
48 CONSTRAINT user_gpg_keys_name_length CHECK (char_length(name) <= 80),
49 CONSTRAINT user_gpg_keys_fingerprint_format CHECK (fingerprint ~ '^[0-9a-f]{40}$'),
50 CONSTRAINT user_gpg_keys_key_id_format CHECK (key_id ~ '^[0-9a-f]{16}$')
51 );
52
53 CREATE UNIQUE INDEX user_gpg_keys_fingerprint_uniq
54 ON user_gpg_keys (fingerprint)
55 WHERE revoked_at IS NULL;
56
57 CREATE INDEX user_gpg_keys_user_id_idx ON user_gpg_keys (user_id, created_at DESC);
58 CREATE INDEX user_gpg_keys_key_id_idx ON user_gpg_keys (key_id);
59
60 -- +goose Down
61 DROP INDEX IF EXISTS user_gpg_keys_key_id_idx;
62 DROP INDEX IF EXISTS user_gpg_keys_user_id_idx;
63 DROP INDEX IF EXISTS user_gpg_keys_fingerprint_uniq;
64 DROP TABLE IF EXISTS user_gpg_keys;
65