internal/control/sig.go
218 lines · 6734 bytes
1package control
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "strconv"
9 "time"
10
11 "gitbay.org/gitbay/internal/gitutil"
12 "gitbay.org/gitbay/internal/policy"
13 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/sig"
15 "gitbay.org/gitbay/internal/store"
16)
17
18func init() {
19 register(Command{Path: []string{"pgp", "add"},
20 Summary: "register an OpenPGP public key (armored, on stdin)", ReadsStdin: true, Run: runPGPAdd})
21 register(Command{Path: []string{"pgp", "list"},
22 Summary: "list registered OpenPGP keys", ReadOnly: true, Run: runPGPList})
23 register(Command{Path: []string{"pgp", "remove"},
24 Summary: "remove an OpenPGP key by fingerprint", Run: runPGPRemove})
25 register(Command{Path: []string{"repo", "log"},
26 Summary: "commit log with signature states: repo log <owner/name> [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog})
27}
28
29func runPGPAdd(c *Ctx, args []string) int {
30 if len(args) != 0 {
31 return c.fail(protocol.ExitUsage, "usage: pgp add < key.asc")
32 }
33 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 1<<20))
34 if err != nil {
35 return c.fail(protocol.ExitFailure, "reading key: %v", err)
36 }
37 meta, err := sig.ParsePGPKey(raw)
38 if err != nil {
39 return c.fail(protocol.ExitUsage, "%v", err)
40 }
41 uids, _ := json.Marshal(meta.Emails)
42 if err := c.Store.AddPGPKey(c.User.ID, meta.Fingerprint, string(raw), string(uids), meta.ExpiresAt, meta.RevokedAt); err != nil {
43 if errors.Is(err, store.ErrDuplicateKey) {
44 return c.fail(protocol.ExitUsage, "%v", err)
45 }
46 return c.fail(protocol.ExitFailure, "adding key: %v", err)
47 }
48 type out struct {
49 Fingerprint string `json:"fingerprint"`
50 Emails []string `json:"emails"`
51 }
52 d := out{meta.Fingerprint, meta.Emails}
53 return c.emit(d, func(w io.Writer) {
54 fmt.Fprintf(w, "added %s (%v)\n", d.Fingerprint, d.Emails)
55 })
56}
57
58func runPGPList(c *Ctx, args []string) int {
59 keys, err := c.Store.ListPGPKeys(c.User.ID)
60 if err != nil {
61 return c.fail(protocol.ExitFailure, "%v", err)
62 }
63 type out struct {
64 Fingerprint string `json:"fingerprint"`
65 Emails string `json:"emails"`
66 ExpiresAt *time.Time `json:"expires_at,omitempty"`
67 RevokedAt *time.Time `json:"revoked_at,omitempty"`
68 }
69 var ds []out
70 for _, k := range keys {
71 ds = append(ds, out{k.Fingerprint, k.UIDsJSON, k.ExpiresAt, k.RevokedAt})
72 }
73 return c.emit(ds, func(w io.Writer) {
74 for _, d := range ds {
75 fmt.Fprintf(w, "%s\t%s\n", d.Fingerprint, d.Emails)
76 }
77 })
78}
79
80func runPGPRemove(c *Ctx, args []string) int {
81 if len(args) != 1 {
82 return c.fail(protocol.ExitUsage, "usage: pgp remove <fingerprint>")
83 }
84 if err := c.Store.RemovePGPKey(c.User.ID, args[0]); err != nil {
85 if errors.Is(err, store.ErrNotFound) {
86 return c.fail(protocol.ExitNotFound, "no key %s on your account", args[0])
87 }
88 return c.fail(protocol.ExitFailure, "%v", err)
89 }
90 return c.emit(map[string]string{"removed": args[0]}, func(w io.Writer) {
91 fmt.Fprintf(w, "removed %s\n", args[0])
92 })
93}
94
95// sigParse is a package-local alias so callers avoid importing sig directly.
96func sigParse(raw []byte) (*sig.Commit, error) { return sig.ParseCommit(raw) }
97
98// VerifyCommitCached verifies one commit with the epoch cache. Shared with
99// the web UI.
100func VerifyCommitCached(st *store.Store, repo store.Repo, parsed *sig.Commit, sha string) (sig.Result, error) {
101 epoch, err := st.KeyEpoch()
102 if err != nil {
103 return sig.Result{}, err
104 }
105 if res, ok, err := st.CachedSignature(repo.ID, sha, epoch); err != nil {
106 return sig.Result{}, err
107 } else if ok {
108 return res, nil
109 }
110 res, err := sig.VerifyCommit(store.SigDB{Store: st}, parsed)
111 if err != nil {
112 return sig.Result{}, err
113 }
114 if err := st.StoreSignature(repo.ID, sha, res, epoch); err != nil {
115 return sig.Result{}, err
116 }
117 return res, nil
118}
119
120func runRepoLog(c *Ctx, args []string) int {
121 limit := 30
122 var path, filePath string
123 for i := 0; i < len(args); i++ {
124 switch args[i] {
125 case "--limit":
126 if i+1 >= len(args) {
127 return c.fail(protocol.ExitUsage, "--limit requires a value")
128 }
129 n, err := strconv.Atoi(args[i+1])
130 if err != nil || n < 1 || n > 1000 {
131 return c.fail(protocol.ExitUsage, "--limit must be 1..1000")
132 }
133 limit = n
134 i++
135 case "--path":
136 if i+1 >= len(args) {
137 return c.fail(protocol.ExitUsage, "--path requires a value")
138 }
139 filePath = args[i+1]
140 i++
141 default:
142 if path != "" {
143 return c.fail(protocol.ExitUsage, "usage: repo log <owner/name> [--limit n] [--path <file>]")
144 }
145 path = args[i]
146 }
147 }
148 if path == "" {
149 return c.fail(protocol.ExitUsage, "usage: repo log <owner/name> [--limit n] [--path <file>]")
150 }
151 repo, code := resolveRepo(c, path, policy.CanRead)
152 if code >= 0 {
153 return code
154 }
155 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
156 var shas []string
157 var err error
158 if filePath != "" {
159 shas, err = gitutil.RevListPath(dir, repo.DefaultBranch, filePath, limit)
160 } else {
161 shas, err = gitutil.RevList(dir, repo.DefaultBranch, limit)
162 }
163 if err != nil {
164 return c.fail(protocol.ExitFailure, "reading log: %v", err)
165 }
166
167 type sigOut struct {
168 State string `json:"state"`
169 Signer string `json:"signer,omitempty"`
170 Fingerprint string `json:"key_fingerprint,omitempty"`
171 }
172 type out struct {
173 SHA string `json:"sha"`
174 Subject string `json:"subject"`
175 AuthorName string `json:"author_name"`
176 AuthorEmail string `json:"author_email"`
177 CommitterEmail string `json:"committer_email,omitempty"` // only when it differs
178 Date string `json:"date"`
179 Signature sigOut `json:"signature"`
180 }
181 var ds []out
182 for _, sha := range shas {
183 raw, err := gitutil.ReadCommit(dir, sha)
184 if err != nil {
185 return c.fail(protocol.ExitFailure, "%v", err)
186 }
187 parsed, err := sig.ParseCommit(raw)
188 if err != nil {
189 return c.fail(protocol.ExitFailure, "parsing %s: %v", sha, err)
190 }
191 res, err := VerifyCommitCached(c.Store, repo, parsed, sha)
192 if err != nil {
193 return c.fail(protocol.ExitFailure, "verifying %s: %v", sha, err)
194 }
195 d := out{
196 SHA: sha,
197 Subject: parsed.Subject,
198 AuthorName: parsed.AuthorName,
199 AuthorEmail: parsed.AuthorEmail,
200 Date: time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339),
201 Signature: sigOut{State: string(res.State), Fingerprint: res.KeyFingerprint},
202 }
203 if parsed.CommitterEmail != parsed.AuthorEmail {
204 d.CommitterEmail = parsed.CommitterEmail
205 }
206 if res.SignerUserID != 0 {
207 if u, err := c.Store.UserByID(res.SignerUserID); err == nil {
208 d.Signature.Signer = u.Username
209 }
210 }
211 ds = append(ds, d)
212 }
213 return c.emit(ds, func(w io.Writer) {
214 for _, d := range ds {
215 fmt.Fprintf(w, "%.10s %-22s %s (%s <%s>)\n", d.SHA, d.Signature.State, d.Subject, d.AuthorName, d.AuthorEmail)
216 }
217 })
218}