Go · 5118 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package sigverify orchestrates OpenPGP signature verification for
4 // commits and annotated tags. The shape of the Result type mirrors
5 // GitHub's documented `verification` object on the REST commits
6 // response so the API layer can render it directly.
7 //
8 // The package is read-only relative to the git repository — it shells
9 // out to `git cat-file` for object bodies but never modifies the
10 // repo. Cache writes go through the commit_verification_cache table
11 // via the sqlc helpers in WriteResult.
12 package sigverify
13
14 import "time"
15
16 // Reason mirrors GitHub's documented verification.reason enum on the
17 // REST commits response. The full set is:
18 //
19 // - valid — signature checks out + signer email
20 // matches one of the user's verified emails.
21 // - unsigned — the commit/tag carries no gpgsig header.
22 // - unknown_key — gpgsig present but the signing subkey
23 // fingerprint isn't registered with shithub.
24 // - bad_email — signature checks cryptographically but
25 // the signer email doesn't match any of the
26 // registered user's emails at all.
27 // - unverified_email — signature checks cryptographically; the
28 // email matches a user-emails row but the
29 // row's verified flag is false.
30 // - malformed_signature — gpgsig header present but the armored
31 // block doesn't parse.
32 // - invalid — signature parses, key matches, but the
33 // cryptographic check fails.
34 // - expired_key — signature is by a subkey that had already
35 // expired by the commit's signature time.
36 // - not_signing_key — signature is by a subkey that exists in
37 // the registry but doesn't carry the sign
38 // capability flag.
39 //
40 // Add new strings here only when GitHub adds them; the value is the
41 // JSON wire format and is load-bearing for shithub-cli compatibility.
42 type Reason string
43
44 const (
45 ReasonValid Reason = "valid"
46 ReasonUnsigned Reason = "unsigned"
47 ReasonUnknownKey Reason = "unknown_key"
48 ReasonBadEmail Reason = "bad_email"
49 ReasonUnverifiedEmail Reason = "unverified_email"
50 ReasonMalformedSignature Reason = "malformed_signature"
51 ReasonInvalid Reason = "invalid"
52 ReasonExpiredKey Reason = "expired_key"
53 ReasonNotSigningKey Reason = "not_signing_key"
54 )
55
56 // Kind discriminates commit-object verifications from annotated-tag
57 // verifications in the cache table.
58 type Kind string
59
60 const (
61 KindCommit Kind = "commit"
62 KindTag Kind = "tag"
63 )
64
65 // Result is the orchestrator's per-object verification verdict. All
66 // fields except Verified+Reason+VerifiedAt are populated only when
67 // the corresponding information was available — see field comments.
68 //
69 // The shape matches GitHub's verification object on the REST commits
70 // response (api.go marshalls these directly when assembling the
71 // `verification` field). Fields that gh exposes as nullable strings
72 // (`signature`, `payload`) map to empty-string here; the JSON
73 // encoder layer translates "" back to null at the surface.
74 type Result struct {
75 // Verified is true if and only if Reason == ReasonValid. Stored
76 // denormalized so consumers don't have to compare against the
77 // string constant.
78 Verified bool
79
80 // Reason carries the gh-documented enum value. Always populated.
81 Reason Reason
82
83 // Signature is the armored signature block extracted from the
84 // gpgsig header. Empty for ReasonUnsigned. Populated for every
85 // other reason where a signature was actually present in the
86 // object (even ReasonMalformedSignature — we surface the raw
87 // armor so the REST consumer can inspect the bad payload).
88 Signature string
89
90 // Payload is the canonical commit-object bytes that the signature
91 // was computed over (the commit object body with the gpgsig
92 // header lines removed). Empty for ReasonUnsigned and when the
93 // signature couldn't be parsed.
94 Payload []byte
95
96 // SignerUserID is the user-id of the registered GPG key that the
97 // signature resolved to. Set whenever Reason ∈ {valid, bad_email,
98 // unverified_email, expired_key, not_signing_key, invalid} — any
99 // case where the subkey lookup succeeded. Zero when
100 // Reason ∈ {unsigned, unknown_key, malformed_signature}.
101 SignerUserID int64
102
103 // SignerSubkeyID is the user_gpg_subkeys row id of the signing
104 // subkey. Set under the same conditions as SignerUserID.
105 SignerSubkeyID int64
106
107 // SignerEmail is the email extracted from the signature's UID
108 // embedding (or from the GPG key's primary UID if the signature
109 // didn't carry one). Used by the popover UI to render
110 // "Signer: <email>".
111 SignerEmail string
112
113 // VerifiedAt is the wall-clock time the verification ran.
114 VerifiedAt time.Time
115 }
116
117 // unsignedResult is the canonical Result returned when an object has
118 // no gpgsig header. Constructed once per call so callers always see
119 // a fresh VerifiedAt; we don't share a package-level value.
120 func unsignedResult() Result {
121 return Result{
122 Verified: false,
123 Reason: ReasonUnsigned,
124 VerifiedAt: time.Now(),
125 }
126 }
127