8000 plumbing: support SSH/X509 signed tags · go-git/go-git@f1dc529 · GitHub
[go: up one dir, main page]

Skip to content

Commit f1dc529

Browse files
committed
plumbing: support SSH/X509 signed tags
This commit enables support for extracting the SSH and X509 signatures from (annotated) Git tags, as an initial step to support the verification of more signatures than just PGP in go-git. The ported logic from Git further ensures that we look for a signature at the tail of an annotation, instead of the first signature we find in the annotation, as this could theoretically result in a faulty signature getting detected if part of a an annotation itself (e.g. by being placed in the middle as part of an inherited message). For commits, no further change is required as the current extraction of any signature (format) from `gpgsig` in the commit header is sufficient for manual verification. In a future iteration, we could add `signature/ssh` and `signature/x509` packages to further enable people to deal with verifying other signatures than PGP. As well as adding additional methods to `Commit` and `Tag` to provide glue between the packages and the most prominent user-facing APIs. Signed-off-by: Hidde Beydals <hidde@hhh.computer>
1 parent 7ab4957 commit f1dc529

File tree

4 files changed

+307
-32
lines changed

4 files changed

+307
-32
lines changed

plumbing/object/signature.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package object
2+
3+
import "bytes"
4+
5+
const (
6+
signatureTypeUnknown signatureType = iota
7+
signatureTypeOpenPGP
8+
signatureTypeX509
9+
signatureTypeSSH
10+
)
11+
12+
var (
13+
// openPGPSignatureFormat is the format of an OpenPGP signature.
14+
openPGPSignatureFormat = signatureFormat{
15+
[]byte("-----BEGIN PGP SIGNATURE-----"),
16+
[]byte("-----BEGIN PGP MESSAGE-----"),
17+
}
18+
// x509SignatureFormat is the format of an X509 signature, which is
19+
// a PKCS#7 (S/MIME) signature.
20+
x509SignatureFormat = signatureFormat{
21+
[]byte("-----BEGIN CERTIFICATE-----"),
22+
}
23+
24+
// sshSignatureFormat is the format of an SSH signature.
25+
sshSignatureFormat = signatureFormat{
26+
[]byte("-----BEGIN SSH SIGNATURE-----"),
27+
}
28+
)
29+
30+
var (
31+
// knownSignatureFormats is a map of known signature formats, indexed by
32+
// their signatureType.
33+
knownSignatureFormats = map[signatureType]signatureFormat{
34+
signatureTypeOpenPGP: openPGPSignatureFormat,
35+
signatureTypeX509: x509SignatureFormat,
36+
signatureTypeSSH: sshSignatureFormat,
37+
}
38+
)
39+
40+
// signatureType represents the type of the signature.
41+
type signatureType int8
42+
43+
// signatureFormat represents the beginning of a signature.
44+
type signatureFormat [][]byte
45+
46+
// typeForSignature returns the type of the signature based on its format.
47+
func typeForSignature(b []byte) signatureType {
48+
for t, i := range knownSignatureFormats {
49+
for _, begin := range i {
50+
if bytes.HasPrefix(b, begin) {
51+
return t
52+
}
53+
}
54+
}
55+
return signatureTypeUnknown
56+
}
57+
58+
// parseSignedBytes returns the position of the last signature block found in
59+
// the given bytes. If no signature block is found, it returns -1.
60+
//
61+
// When multiple signature blocks are found, the position of the last one is
62+
// returned. Any tailing bytes after this signature block start should be
63+
// considered part of the signature.
64+
//
65+
// Given this, it would be safe to use the returned position to split the bytes
66+
// into two parts: the first part containing the message, the second part
67+
// containing the signature.
68+
//
69+
// Example:
70+
//
71+
// message := []byte(`Message with signature
72+
//
73+
// -----BEGIN SSH SIGNATURE-----
74+
// ...`)
75+
//
76+
// var signature string
77+
// if pos, _ := parseSignedBytes(message); pos != -1 {
78+
// signature = string(message[pos:])
79+
// message = message[:pos]
80+
// }
81+
//
82+
// This logic is on par with git's gpg-interface.c:parse_signed_buffer().
83+
// https://github.com/git/git/blob/7c2ef319c52c4997256f5807564523dfd4acdfc7/gpg-interface.c#L668
84+
func parseSignedBytes(b []byte) (int, signatureType) {
85+
var n, match = 0, -1
86+
var t signatureType
87+
for n < len(b) {
88+
var i = b[n:]
89+
if st := typeForSignature(i); st != signatureTypeUnknown {
90+
match = n
91+
t = st
92+
}
93+
if eol := bytes.IndexByte(i, '\n'); eol >= 0 {
94+
n += eol + 1
95+
continue
96+
}
97+
// If we reach this point, we've reached the end.
98+
break
99+
}
100+
return match, t
101+
}

plumbing/object/signature_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package object
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func Test_typeForSignature(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
b []byte
12+
want signatureType
13+
}{
14+
{
15+
name: "known signature format (PGP)",
16+
b: []byte(`-----BEGIN PGP SIGNATURE-----
17+
18+
iHUEABYKAB0WIQTMqU0ycQ3f6g3PMoWMmmmF4LuV8QUCYGebVwAKCRCMmmmF4LuV
19+
8VtyAP9LbuXAhtK6FQqOjKybBwlV70rLcXVP24ubDuz88VVwSgD+LuObsasWq6/U
20+
TssDKHUR2taa53bQYjkZQBpvvwOrLgc=
21+
=YQUf
22+
-----END PGP SIGNATURE-----`),
23+
want: signatureTypeOpenPGP,
24+
},
25+
{
26+
name: "known signature format (SSH)",
27+
b: []byte(`-----BEGIN SSH SIGNATURE-----
28+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
29+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
30+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
31+
MKEQruIQWJb+8HVXwssA4=
32+
-----END SSH SIGNATURE-----`),
33+
want: signatureTypeSSH,
34+
},
35+
{
36+
name: "known signature format (X509)",
37+
b: []byte(`-----BEGIN CERTIFICATE-----
38+
MIIDZjCCAk6gAwIBAgIJALZ9Z3Z9Z3Z9MA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD
39+
VQQGEwJTRTEOMAwGA1UECAwFVGV4YXMxDjAMBgNVBAcMBVRleGFzMQ4wDAYDVQQK
40+
DAVUZXhhczEOMAwGA1UECwwFVGV4YXMxGDAWBgNVBAMMD1RleGFzIENlcnRpZmlj
41+
YXRlMB4XDTE3MDUyNjE3MjY0MloXDTI3MDUyNDE3MjY0MlowgYgxCzAJBgNVBAYT
42+
AlNFMQ4wDAYDVQQIDAVUZXhhczEOMAwGA1UEBwwFVGV4YXMxDjAMBgNVBAoMBVRl
43+
eGFzMQ4wDAYDVQQLDAVUZXhhczEYMBYGA1UEAwwPVGV4YXMgQ2VydGlmaWNhdGUw
44+
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQZ9Z3Z9Z3Z9Z3Z9Z3Z9Z3
45+
-----END CERTIFICATE-----`),
46+
want: signatureTypeX509,
47+
},
48+
{
49+
name: "unknown signature format",
50+
b: []byte(`-----BEGIN ARBITRARY SIGNATURE-----
51+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
52+
-----END UNKNOWN SIGNATURE-----`),
53+
want: signatureTypeUnknown,
54+
},
55+
}
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
if got := typeForSignature(tt.b); got != tt.want {
59+
t.Errorf("typeForSignature() = %v, want %v", got, tt.want)
60+
}
61+
})
62+
}
63+
}
64+
65+
func Test_parseSignedBytes(t *testing.T) {
66+
tests := []struct {
67+
name string
68+
b []byte
69+
wantSignature []byte
70+
wantType signatureType
71+
}{
72+
{
73+
name: "detects signature and type",
74+
b: []byte(`signed tag
75+
-----BEGIN PGP SIGNATURE-----
76+
77+
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop
78+
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un
79+
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI
80+
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN
81+
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3
82+
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI
83+
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o
84+
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV
85+
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J
86+
sZC//k6m
87+
=VhHy
88+
-----END PGP SIGNATURE-----`),
89+
wantSignature: []byte(`-----BEGIN PGP SIGNATURE-----
90+
91+
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop
92+
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un
93+
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI
94+
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN
95+
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3
96+
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI
97+
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o
98+
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV
99+
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J
100+
sZC//k6m
101+
=VhHy
102+
-----END PGP SIGNATURE-----`),
103+
wantType: signatureTypeOpenPGP,
104+
},
105+
{
106+
name: "last signature for multiple signatures",
107+
b: []byte(`signed tag
108+
-----BEGIN PGP SIGNATURE-----
109+
110+
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop
111+
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un
112+
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI
113+
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN
114+
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3
115+
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI
116+
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o
117+
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV
118+
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J
119+
sZC//k6m
120+
=VhHy
121+
-----END PGP SIGNATURE-----
122+
-----BEGIN SSH SIGNATURE-----
123+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
124+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
125+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
126+
MKEQruIQWJb+8HVXwssA4=
127+
-----END SSH SIGNATURE-----`),
128+
wantSignature: []byte(`-----BEGIN SSH SIGNATURE-----
129+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
130+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
131+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
132+
MKEQruIQWJb+8HVXwssA4=
133+
-----END SSH SIGNATURE-----`),
134+
wantType: signatureTypeSSH,
135+
},
136+
{
137+
name: "signature with trailing data",
138+
b: []byte(`An invalid
139+
140+
-----BEGIN SSH SIGNATURE-----
141+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
142+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
143+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
144+
MKEQruIQWJb+8HVXwssA4=
145+
-----END SSH SIGNATURE-----
146+
147+
signed tag`),
148+
wantSignature: []byte(`-----BEGIN SSH SIGNATURE-----
149+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
150+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
151+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
152+
MKEQruIQWJb+8HVXwssA4=
153+
-----END SSH SIGNATURE-----
154+
155+
signed tag`),
156+
wantType: signatureTypeSSH,
157+
},
158+
{
159+
name: "data without signature",
160+
b: []byte(`Some message`),
161+
wantSignature: []byte(``),
162+
wantType: signatureTypeUnknown,
163+
},
164+
}
165+
for _, tt := range tests {
166+
t.Run(tt.name, func(t *testing.T) {
167+
pos, st := parseSignedBytes(tt.b)
168+
var signature []byte
169+
if pos >= 0 {
170+
signature = tt.b[pos:]
171+
}
172+
if !bytes.Equal(signature, tt.wantSignature) {
173+
t.Errorf("parseSignedBytes() got = %s for pos = %v, want %s", signature, pos, tt.wantSignature)
174+
}
175+
if st != tt.wantType {
176+
t.Errorf("parseSignedBytes() got1 = %v, want %v", st, tt.wantType)
177+
}
178+
})
179+
}
180+
}

plumbing/object/tag.go

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import (
44
"bytes"
55
"fmt"
66
"io"
7-
stdioutil "io/ioutil"
87
"strings"
98

109
"github.com/ProtonMail/go-crypto/openpgp"
11-
1210
"github.com/go-git/go-git/v5/plumbing"
1311
"github.com/go-git/go-git/v5/plumbing/storer"
1412
"github.com/go-git/go-git/v5/utils/ioutil"
@@ -128,40 +126,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
128126
}
129127
}
130128

131-
data, err := stdioutil.ReadAll(r)
129+
data, err := io.ReadAll(r)
132130
if err != nil {
133131
return err
134132
}
135-
136-
var pgpsig bool
137-
// Check if data contains PGP signature.
138-
if bytes.Contains(data, []byte(beginpgp)) {
139-
// Split the lines at newline.
140-
messageAndSig := bytes.Split(data, []byte("\n"))
141-
142-
for _, l := range messageAndSig {
143-
if pgpsig {
144-
if bytes.Contains(l, []byte(endpgp)) {
145-
t.PGPSignature += endpgp + "\n"
146-
break
147-
} else {
148-
t.PGPSignature += string(l) + "\n"
149-
}
150-
continue
151-
}
152-
153-
// Check if it's the beginning of a PGP signature.
154-
if bytes.Contains(l, []byte(beginpgp)) {
155-
t.PGPSignature += beginpgp + "\n"
156-
pgpsig = true
157-
continue
158-
}
159-
160-
t.Message += string(l) + "\n"
161-
}
162-
} else {
163-
t.Message = string(data)
133+
if sm, _ := parseSignedBytes(data); sm >= 0 {
134+
t.PGPSignature = string(data[sm:])
135+
data = data[:sm]
164136
}
137+
t.Message = string(data)
165138

166139
return nil
167140
}

plumbing/object/tag_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,27 @@ RUysgqjcpT8+iQM1PblGfHR4XAhuOqN5Fx06PSaFZhqvWFezJ28/CLyX5q+oIVk=
312312
c.Assert(decoded.PGPSignature, Equals, pgpsignature)
313313
}
314314

315+
func (s *TagSuite) TestSSHSignatureSerialization(c *C) {
316+
encoded := &plumbing.MemoryObject{}
317+
decoded := &Tag{}
318+
tag := s.tag(c, plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69"))
319+
320+
signature := `-----BEGIN SSH SIGNATURE-----
321+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp
322+
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
323+
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr
324+
MKEQruIQWJb+8HVXwssA4=
325+
-----END SSH SIGNATURE-----`
326+
tag.PGPSignature = signature
327+
328+
err := tag.Encode(encoded)
329+
c.Assert(err, IsNil)
330+
331+
err = decoded.Decode(encoded)
332+
c.Assert(err, IsNil)
333+
c.Assert(decoded.PGPSignature, Equals, signature)
334+
}
335+
315336
func (s *TagSuite) TestVerify(c *C) {
316337
ts := time.Unix(1617403017, 0)
317338
loc, _ := time.LoadLocation("UTC")

0 commit comments

Comments
 (0)
0