Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
Showing
4 changed files
with
307 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package object | ||
|
||
import "bytes" | ||
|
||
const ( | ||
signatureTypeUnknown signatureType = iota | ||
signatureTypeOpenPGP | ||
signatureTypeX509 | ||
signatureTypeSSH | ||
) | ||
|
||
var ( | ||
// openPGPSignatureFormat is the format of an OpenPGP signature. | ||
openPGPSignatureFormat = signatureFormat{ | ||
[]byte("-----BEGIN PGP SIGNATURE-----"), | ||
[]byte("-----BEGIN PGP MESSAGE-----"), | ||
} | ||
// x509SignatureFormat is the format of an X509 signature, which is | ||
// a PKCS#7 (S/MIME) signature. | ||
x509SignatureFormat = signatureFormat{ | ||
[]byte("-----BEGIN CERTIFICATE-----"), | ||
} | ||
|
||
// sshSignatureFormat is the format of an SSH signature. | ||
sshSignatureFormat = signatureFormat{ | ||
[]byte("-----BEGIN SSH SIGNATURE-----"), | ||
} | ||
) | ||
|
||
var ( | ||
// knownSignatureFormats is a map of known signature formats, indexed by | ||
// their signatureType. | ||
knownSignatureFormats = map[signatureType]signatureFormat{ | ||
signatureTypeOpenPGP: openPGPSignatureFormat, | ||
signatureTypeX509: x509SignatureFormat, | ||
signatureTypeSSH: sshSignatureFormat, | ||
} | ||
) | ||
|
||
// signatureType represents the type of the signature. | ||
type signatureType int8 | ||
|
||
// signatureFormat represents the beginning of a signature. | ||
type signatureFormat [][]byte | ||
|
||
// typeForSignature returns the type of the signature based on its format. | ||
func typeForSignature(b []byte) signatureType { | ||
for t, i := range knownSignatureFormats { | ||
for _, begin := range i { | ||
if bytes.HasPrefix(b, begin) { | ||
return t | ||
} | ||
} | ||
} | ||
return signatureTypeUnknown | ||
} | ||
|
||
// parseSignedBytes returns the position of the last signature block found in | ||
// the given bytes. If no signature block is found, it returns -1. | ||
// | ||
// When multiple signature blocks are found, the position of the last one is | ||
// returned. Any tailing bytes after this signature block start should be | ||
// considered part of the signature. | ||
// | ||
// Given this, it would be safe to use the returned position to split the bytes | ||
// into two parts: the first part containing the message, the second part | ||
// containing the signature. | ||
// | ||
// Example: | ||
// | ||
// message := []byte(`Message with signature | ||
// | ||
// -----BEGIN SSH SIGNATURE----- | ||
// ...`) | ||
// | ||
// var signature string | ||
// if pos, _ := parseSignedBytes(message); pos != -1 { | ||
// signature = string(message[pos:]) | ||
// message = message[:pos] | ||
// } | ||
// | ||
// This logic is on par with git's gpg-interface.c:parse_signed_buffer(). | ||
// https://github.com/git/git/blob/7c2ef319c52c4997256f5807564523dfd4acdfc7/gpg-interface.c#L668 | ||
func parseSignedBytes(b []byte) (int, signatureType) { | ||
var n, match = 0, -1 | ||
var t signatureType | ||
for n < len(b) { | ||
var i = b[n:] | ||
if st := typeForSignature(i); st != signatureTypeUnknown { | ||
match = n | ||
t = st | ||
} | ||
if eol := bytes.IndexByte(i, '\n'); eol >= 0 { | ||
n += eol + 1 | ||
continue | ||
} | ||
// If we reach this point, we've reached the end. | ||
break | ||
} | ||
return match, t | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package object | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
) | ||
|
||
func Test_typeForSignature(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
b []byte | ||
want signatureType | ||
}{ | ||
{ | ||
name: "known signature format (PGP)", | ||
b: []byte(`-----BEGIN PGP SIGNATURE----- | ||
iHUEABYKAB0WIQTMqU0ycQ3f6g3PMoWMmmmF4LuV8QUCYGebVwAKCRCMmmmF4LuV | ||
8VtyAP9LbuXAhtK6FQqOjKybBwlV70rLcXVP24ubDuz88VVwSgD+LuObsasWq6/U | ||
TssDKHUR2taa53bQYjkZQBpvvwOrLgc= | ||
=YQUf | ||
-----END PGP SIGNATURE-----`), | ||
want: signatureTypeOpenPGP, | ||
}, | ||
{ | ||
name: "known signature format (SSH)", | ||
b: []byte(`-----BEGIN SSH SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | ||
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr | ||
MKEQruIQWJb+8HVXwssA4= | ||
-----END SSH SIGNATURE-----`), | ||
want: signatureTypeSSH, | ||
}, | ||
{ | ||
name: "known signature format (X509)", | ||
b: []byte(`-----BEGIN CERTIFICATE----- | ||
MIIDZjCCAk6gAwIBAgIJALZ9Z3Z9Z3Z9MA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD | ||
VQQGEwJTRTEOMAwGA1UECAwFVGV4YXMxDjAMBgNVBAcMBVRleGFzMQ4wDAYDVQQK | ||
DAVUZXhhczEOMAwGA1UECwwFVGV4YXMxGDAWBgNVBAMMD1RleGFzIENlcnRpZmlj | ||
YXRlMB4XDTE3MDUyNjE3MjY0MloXDTI3MDUyNDE3MjY0MlowgYgxCzAJBgNVBAYT | ||
AlNFMQ4wDAYDVQQIDAVUZXhhczEOMAwGA1UEBwwFVGV4YXMxDjAMBgNVBAoMBVRl | ||
eGFzMQ4wDAYDVQQLDAVUZXhhczEYMBYGA1UEAwwPVGV4YXMgQ2VydGlmaWNhdGUw | ||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQZ9Z3Z9Z3Z9Z3Z9Z3Z9Z3 | ||
-----END CERTIFICATE-----`), | ||
want: signatureTypeX509, | ||
}, | ||
{ | ||
name: "unknown signature format", | ||
b: []byte(`-----BEGIN ARBITRARY SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
-----END UNKNOWN SIGNATURE-----`), | ||
want: signatureTypeUnknown, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := typeForSignature(tt.b); got != tt.want { | ||
t.Errorf("typeForSignature() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func Test_parseSignedBytes(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
b []byte | ||
wantSignature []byte | ||
wantType signatureType | ||
}{ | ||
{ | ||
name: "detects signature and type", | ||
b: []byte(`signed tag | ||
-----BEGIN PGP SIGNATURE----- | ||
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop | ||
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un | ||
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI | ||
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN | ||
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3 | ||
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI | ||
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o | ||
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV | ||
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J | ||
sZC//k6m | ||
=VhHy | ||
-----END PGP SIGNATURE-----`), | ||
wantSignature: []byte(`-----BEGIN PGP SIGNATURE----- | ||
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop | ||
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un | ||
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI | ||
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN | ||
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3 | ||
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI | ||
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o | ||
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV | ||
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J | ||
sZC//k6m | ||
=VhHy | ||
-----END PGP SIGNATURE-----`), | ||
wantType: signatureTypeOpenPGP, | ||
}, | ||
{ | ||
name: "last signature for multiple signatures", | ||
b: []byte(`signed tag | ||
-----BEGIN PGP SIGNATURE----- | ||
iQGzBAABCAAdFiEE/h5sbbqJFh9j1AdUSqtFFGopTmwFAmB5XFkACgkQSqtFFGop | ||
TmxvgAv+IPjX5WCLFUIMx8hquMZp1VkhQrseE7rljUYaYpga8gZ9s4kseTGhy7Un | ||
61U3Ro6cTPEiQF/FkAGzSdPuGqv0ARBqHDX2tUI9+Zs/K8aG8tN+JTaof0gBcTyI | ||
BLbZVYDTxbS9whxSDewQd0OvBG1m9ISLUhjXo6mbaVvrKXNXTHg40MPZ8ZxjR/vN | ||
hxXXoUVnFyEDo+v6nK56mYtapThDaQQHHzD6D3VaCq3Msog7qAh9/ZNBmgb88aQ3 | ||
FoK8PHMyr5elsV3mE9bciZBUc+dtzjOvp94uQ5ZKUXaPusXaYXnKpVnzhyer6RBI | ||
gJLWtPwAinqmN41rGJ8jDAGrpPNjaRrMhGtbyVUPUf19OxuUIroe77sIIKTP0X2o | ||
Wgp56dYpTst0JcGv/FYCeau/4pTRDfwHAOcDiBQ/0ag9IrZp9P8P9zlKmzNPEraV | ||
pAe1/EFuhv2UDLucAiWM8iDZIcw8iN0OYMOGUmnk0WuGIo7dzLeqMGY+ND5n5Z8J | ||
sZC//k6m | ||
=VhHy | ||
-----END PGP SIGNATURE----- | ||
-----BEGIN SSH SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | ||
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr | ||
MKEQruIQWJb+8HVXwssA4= | ||
-----END SSH SIGNATURE-----`), | ||
wantSignature: []byte(`-----BEGIN SSH SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | ||
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr | ||
MKEQruIQWJb+8HVXwssA4= | ||
-----END SSH SIGNATURE-----`), | ||
wantType: signatureTypeSSH, | ||
}, | ||
{ | ||
name: "signature with trailing data", | ||
b: []byte(`An invalid | ||
-----BEGIN SSH SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | ||
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr | ||
MKEQruIQWJb+8HVXwssA4= | ||
-----END SSH SIGNATURE----- | ||
signed tag`), | ||
wantSignature: []byte(`-----BEGIN SSH SIGNATURE----- | ||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp | ||
0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | ||
AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr | ||
MKEQruIQWJb+8HVXwssA4= | ||
-----END SSH SIGNATURE----- | ||
signed tag`), | ||
wantType: signatureTypeSSH, | ||
}, | ||
{ | ||
name: "data without signature", | ||
b: []byte(`Some message`), | ||
wantSignature: []byte(``), | ||
wantType: signatureTypeUnknown, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
pos, st := parseSignedBytes(tt.b) | ||
var signature []byte | ||
if pos >= 0 { | ||
signature = tt.b[pos:] | ||
} | ||
if !bytes.Equal(signature, tt.wantSignature) { | ||
t.Errorf("parseSignedBytes() got = %s for pos = %v, want %s", signature, pos, tt.wantSignature) | ||
} | ||
if st != tt.wantType { | ||
t.Errorf("parseSignedBytes() got1 = %v, want %v", st, tt.wantType) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters