Go · 4432 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package sigverify
4
5 import (
6 "bytes"
7 "strings"
8 )
9
10 // splitSignedObject takes the raw bytes of a git commit or annotated
11 // tag object (as returned by `git cat-file -p <oid>`) and splits it
12 // into the signature-payload (the bytes the signature was computed
13 // over) and the armored signature block.
14 //
15 // Git stores the signature as a header line whose value is a multi-
16 // line PGP armor block; the lines after the first are continuation
17 // lines starting with a single space. For example:
18 //
19 // tree abc123...
20 // parent def456...
21 // author Alice <alice@example.com> 1700000000 +0000
22 // committer Alice <alice@example.com> 1700000000 +0000
23 // gpgsig -----BEGIN PGP SIGNATURE-----
24 // <body>
25 // -----END PGP SIGNATURE-----
26 //
27 // commit message body
28 //
29 // The canonical payload is the same object with the gpgsig header
30 // (and its continuation lines) removed entirely. The signature is
31 // the concatenation of the gpgsig-value first line + the un-indented
32 // continuation lines.
33 //
34 // Returns (payload, armoredSig, true) when a gpgsig header was found,
35 // or (rawBody, "", false) when the object carries no signature.
36 //
37 // Tag objects can store the signature either inline at the end of the
38 // body (the classic format, with `-----BEGIN PGP SIGNATURE-----`
39 // appearing in the message body) OR via a header (the newer SSH-sig
40 // convention). For OpenPGP tags we look at both forms — the legacy
41 // trailing-block form is the dominant one in the wild.
42 func splitSignedObject(body []byte) (payload []byte, armoredSig string, signed bool) {
43 // Find the end of the header block (the first blank line).
44 headerEnd := bytes.Index(body, []byte("\n\n"))
45 if headerEnd < 0 {
46 return body, "", false
47 }
48 header := body[:headerEnd]
49 rest := body[headerEnd:]
50
51 // Walk the header lines looking for a gpgsig header. There's
52 // exactly zero or one per object in practice.
53 lines := bytes.Split(header, []byte("\n"))
54 var (
55 sigBuilder strings.Builder
56 newHeader bytes.Buffer
57 inSig bool
58 foundSig bool
59 )
60 for i, line := range lines {
61 if inSig {
62 if bytes.HasPrefix(line, []byte(" ")) {
63 // Continuation line — strip one leading space.
64 sigBuilder.Write(line[1:])
65 sigBuilder.WriteByte('\n')
66 continue
67 }
68 // End of signature continuation — fall through to
69 // header handling for this line.
70 inSig = false
71 }
72 if bytes.HasPrefix(line, []byte("gpgsig ")) {
73 foundSig = true
74 inSig = true
75 sigBuilder.Write(line[len("gpgsig "):])
76 sigBuilder.WriteByte('\n')
77 continue
78 }
79 // Tag-object signature header is named differently in some
80 // gitformats but the modern convention also uses 'gpgsig'.
81 // Treat any other line as a regular header to preserve.
82 if i > 0 {
83 newHeader.WriteByte('\n')
84 }
85 newHeader.Write(line)
86 }
87
88 if !foundSig {
89 // Check tag-object inline trailing-signature form: the body
90 // (rest, after the blank line) may end with a PGP signature
91 // block. This form has no `gpgsig` header.
92 return splitTagInlineSignature(body)
93 }
94
95 payload = append(newHeader.Bytes(), rest...)
96 return payload, sigBuilder.String(), true
97 }
98
99 // splitTagInlineSignature handles annotated tags that embed the
100 // signature at the end of the message body (the legacy git-tag
101 // signing convention) rather than as a header. The signature block
102 // runs from `-----BEGIN PGP SIGNATURE-----` to `-----END PGP
103 // SIGNATURE-----` inclusive at the tail of the body.
104 //
105 // Returns (payload, armoredSig, true) when an inline block is
106 // detected, or (body, "", false) otherwise.
107 func splitTagInlineSignature(body []byte) (payload []byte, armoredSig string, signed bool) {
108 const begin = "-----BEGIN PGP SIGNATURE-----"
109 const end = "-----END PGP SIGNATURE-----"
110
111 beginIdx := bytes.Index(body, []byte(begin))
112 if beginIdx < 0 {
113 return body, "", false
114 }
115 endIdx := bytes.Index(body[beginIdx:], []byte(end))
116 if endIdx < 0 {
117 return body, "", false
118 }
119 endIdx += beginIdx + len(end)
120 // Include trailing newline if present.
121 if endIdx < len(body) && body[endIdx] == '\n' {
122 endIdx++
123 }
124 armoredSig = string(body[beginIdx:endIdx])
125 // Payload is everything before the signature block. git tag -s
126 // appends the signature directly to the tag body — the signed
127 // payload IS the body up to (and including the trailing newline
128 // before) the BEGIN marker. No further trimming.
129 payload = body[:beginIdx]
130 return payload, armoredSig, true
131 }
132