krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: internal/sig/commit.go · raw
1// Package sig verifies OpenPGP and SSHSIG signatures on git commits and
2// tags, and maps them to the forge's trust states.
3package sig
4
5import (
6 "bytes"
7 "fmt"
8 "strings"
9)
10
11// Commit is a parsed raw commit object.
12type Commit struct {
13 Raw []byte
14 Payload []byte // Raw with the gpgsig header removed, byte-exact
15 Signature []byte // armored signature block, nil if unsigned
16 AuthorName string
17 AuthorEmail string
18 CommitterEmail string
19 Subject string
20 AuthorUnix int64
21}
22
23// ParseCommit splits a raw commit object (as printed by `git cat-file
24// commit`) into its signed payload and signature. The payload must be
25// byte-exact: it is the original object minus the gpgsig header line and its
26// continuation lines, nothing else.
27func ParseCommit(raw []byte) (*Commit, error) {
28 c := &Commit{Raw: raw}
29
30 headerEnd := bytes.Index(raw, []byte("\n\n"))
31 if headerEnd < 0 {
32 return nil, fmt.Errorf("malformed commit: no header/body separator")
33 }
34 headers := raw[:headerEnd+1] // include trailing newline of last header
35 body := raw[headerEnd+2:]
36
37 var payload bytes.Buffer
38 lines := bytes.SplitAfter(headers, []byte("\n"))
39 for i := 0; i < len(lines); i++ {
40 line := lines[i]
41 if sigBody, ok := bytes.CutPrefix(line, []byte("gpgsig ")); ok {
42 // The signature value continues on lines starting with a space.
43 var sig bytes.Buffer
44 sig.Write(sigBody)
45 for i+1 < len(lines) && bytes.HasPrefix(lines[i+1], []byte(" ")) {
46 sig.Write(lines[i+1][1:])
47 i++
48 }
49 c.Signature = bytes.TrimSuffix(sig.Bytes(), []byte("\n"))
50 continue
51 }
52 payload.Write(line)
53
54 switch {
55 case bytes.HasPrefix(line, []byte("author ")):
56 c.AuthorName, c.AuthorEmail, c.AuthorUnix = parseIdent(string(line[len("author "):]))
57 case bytes.HasPrefix(line, []byte("committer ")):
58 _, c.CommitterEmail, _ = parseIdent(string(line[len("committer "):]))
59 }
60 }
61 payload.WriteByte('\n')
62 payload.Write(body)
63 c.Payload = payload.Bytes()
64
65 if i := bytes.IndexByte(body, '\n'); i >= 0 {
66 c.Subject = string(body[:i])
67 } else {
68 c.Subject = strings.TrimRight(string(body), "\n")
69 }
70 return c, nil
71}
72
73// parseIdent parses "Name <email> unix tz".
74func parseIdent(s string) (name, email string, unix int64) {
75 s = strings.TrimSuffix(s, "\n")
76 lt := strings.IndexByte(s, '<')
77 gt := strings.IndexByte(s, '>')
78 if lt < 0 || gt < lt {
79 return s, "", 0
80 }
81 name = strings.TrimSpace(s[:lt])
82 email = s[lt+1 : gt]
83 rest := strings.Fields(s[gt+1:])
84 if len(rest) >= 1 {
85 fmt.Sscanf(rest[0], "%d", &unix)
86 }
87 return name, email, unix
88}
89
90// SigKind reports which signature format a gpgsig block holds.
91type SigKind int
92
93const (
94 SigNone SigKind = iota
95 SigOpenPGP
96 SigSSH
97 SigUnknown
98)
99
100func KindOf(sig []byte) SigKind {
101 switch {
102 case sig == nil:
103 return SigNone
104 case bytes.Contains(sig, []byte("BEGIN PGP SIGNATURE")):
105 return SigOpenPGP
106 case bytes.Contains(sig, []byte("BEGIN SSH SIGNATURE")):
107 return SigSSH
108 default:
109 return SigUnknown
110 }
111}