Go · 3858 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package sigverify
4
5 import (
6 "context"
7 "time"
8
9 reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
10 )
11
12 // View is a render-friendly projection of a commit_verification_cache
13 // row. It carries everything the HTML badge partial and the REST
14 // `verification` object need; both consumers translate to their own
15 // surface shapes from here.
16 //
17 // "No cache row" maps to View{Verified: false, Reason: ReasonUnsigned}
18 // — gh's documented behavior for commits we haven't verified yet.
19 //
20 // "Cache row exists but invalidated_at != null" also maps to the
21 // unsigned-shaped default. Rationale: a stale row means the signing
22 // key was revoked between verification and now; pretending it's
23 // verified would be wrong. Treating it as "needs re-verify, render
24 // as unsigned for now" lets the backfill worker catch up
25 // asynchronously without the render path doing crypto work inline.
26 type View struct {
27 Verified bool
28 Reason Reason
29 Signature string
30 Payload []byte
31 VerifiedAt *time.Time
32 SignerUserID int64
33 SignerSubkeyID int64
34 }
35
36 // UnsignedView is the canonical "no cache row" / "row invalidated"
37 // shape. Mirrors gh's "unsigned" rendering.
38 func UnsignedView() View {
39 return View{Verified: false, Reason: ReasonUnsigned}
40 }
41
42 // ViewFromRow translates a sqlc cache row into the View shape.
43 // Honors the invalidated_at flag by returning UnsignedView for
44 // stale rows.
45 func ViewFromRow(row reposdb.CommitVerificationCache) View {
46 if row.InvalidatedAt.Valid {
47 return UnsignedView()
48 }
49 view := View{
50 Verified: row.Verified,
51 Reason: Reason(row.Reason),
52 Payload: row.Payload,
53 }
54 if row.SignatureArmored.Valid {
55 view.Signature = row.SignatureArmored.String
56 }
57 if row.VerifiedAt.Valid {
58 t := row.VerifiedAt.Time
59 view.VerifiedAt = &t
60 }
61 if row.SignerUserID.Valid {
62 view.SignerUserID = row.SignerUserID.Int64
63 }
64 if row.SignerSubkeyID.Valid {
65 view.SignerSubkeyID = row.SignerSubkeyID.Int64
66 }
67 return view
68 }
69
70 // LoadViewsForOIDs batch-reads verification rows for a set of commit
71 // OIDs and returns a map keyed by OID. OIDs without a cache row are
72 // absent from the map — the renderer treats absence as
73 // UnsignedView() via the LookupView helper.
74 //
75 // Used by the commit-list render path (both HTML and REST).
76 func LoadViewsForOIDs(ctx context.Context, db reposdb.DBTX, repoID int64, oids []string) (map[string]View, error) {
77 if len(oids) == 0 {
78 return map[string]View{}, nil
79 }
80 q := reposdb.New()
81 rows, err := q.GetCommitVerificationsForOIDs(ctx, db, reposdb.GetCommitVerificationsForOIDsParams{
82 RepoID: repoID,
83 Oids: oids,
84 })
85 if err != nil {
86 return nil, err
87 }
88 out := make(map[string]View, len(rows))
89 for _, row := range rows {
90 out[row.CommitOid] = ViewFromRow(row)
91 }
92 return out, nil
93 }
94
95 // LoadView reads a single commit's verification row. Returns
96 // UnsignedView() when no row exists or the row is invalidated.
97 func LoadView(ctx context.Context, db reposdb.DBTX, repoID int64, oid string) (View, error) {
98 q := reposdb.New()
99 row, err := q.GetCommitVerification(ctx, db, reposdb.GetCommitVerificationParams{
100 RepoID: repoID,
101 CommitOid: oid,
102 })
103 if err != nil {
104 // Distinguishing "not found" from "DB error" matters: not-
105 // found is the unsigned-default fast path; DB error should
106 // propagate so the caller can decide between fail-open
107 // (render anyway) and fail-closed (5xx). Caller handles via
108 // errors.Is(err, pgx.ErrNoRows).
109 return UnsignedView(), err
110 }
111 return ViewFromRow(row), nil
112 }
113
114 // LookupView returns the View for the given OID from a map, falling
115 // back to UnsignedView() on miss. Template-friendly helper so the
116 // partial doesn't have to do nil-checks inline.
117 func LookupView(m map[string]View, oid string) View {
118 v, ok := m[oid]
119 if !ok {
120 return UnsignedView()
121 }
122 return v
123 }
124