diff --git a/plumbing/object/signature.go b/plumbing/object/signature.go new file mode 100644 index 000000000..91cf371f0 --- /dev/null +++ b/plumbing/object/signature.go @@ -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 +} diff --git a/plumbing/object/signature_test.go b/plumbing/object/signature_test.go new file mode 100644 index 000000000..1bdb1d1ca --- /dev/null +++ b/plumbing/object/signature_test.go @@ -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) + } + }) + } +} diff --git a/plumbing/object/tag.go b/plumbing/object/tag.go index 84066f768..cf46c08e1 100644 --- a/plumbing/object/tag.go +++ b/plumbing/object/tag.go @@ -4,11 +4,9 @@ import ( "bytes" "fmt" "io" - stdioutil "io/ioutil" "strings" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/utils/ioutil" @@ -128,40 +126,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) { } } - data, err := stdioutil.ReadAll(r) + data, err := io.ReadAll(r) if err != nil { return err } - - var pgpsig bool - // Check if data contains PGP signature. - if bytes.Contains(data, []byte(beginpgp)) { - // Split the lines at newline. - messageAndSig := bytes.Split(data, []byte("\n")) - - for _, l := range messageAndSig { - if pgpsig { - if bytes.Contains(l, []byte(endpgp)) { - t.PGPSignature += endpgp + "\n" - break - } else { - t.PGPSignature += string(l) + "\n" - } - continue - } - - // Check if it's the beginning of a PGP signature. - if bytes.Contains(l, []byte(beginpgp)) { - t.PGPSignature += beginpgp + "\n" - pgpsig = true - continue - } - - t.Message += string(l) + "\n" - } - } else { - t.Message = string(data) + if sm, _ := parseSignedBytes(data); sm >= 0 { + t.PGPSignature = string(data[sm:]) + data = data[:sm] } + t.Message = string(data) return nil } diff --git a/plumbing/object/tag_test.go b/plumbing/object/tag_test.go index cd1d15d1f..15b943e07 100644 --- a/plumbing/object/tag_test.go +++ b/plumbing/object/tag_test.go @@ -312,6 +312,27 @@ RUysgqjcpT8+iQM1PblGfHR4XAhuOqN5Fx06PSaFZhqvWFezJ28/CLyX5q+oIVk= c.Assert(decoded.PGPSignature, Equals, pgpsignature) } +func (s *TagSuite) TestSSHSignatureSerialization(c *C) { + encoded := &plumbing.MemoryObject{} + decoded := &Tag{} + tag := s.tag(c, plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69")) + + signature := `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgij/EfHS8tCjolj5uEANXgKzFfp +0D7wOhjWVbYZH6KugAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQIYHMhSVV9L2xwJuV8eWMLjThya8yXgCHDzw3p01D19KirrabW0veiichPB5m+Ihtr +MKEQruIQWJb+8HVXwssA4= +-----END SSH SIGNATURE-----` + tag.PGPSignature = signature + + err := tag.Encode(encoded) + c.Assert(err, IsNil) + + err = decoded.Decode(encoded) + c.Assert(err, IsNil) + c.Assert(decoded.PGPSignature, Equals, signature) +} + func (s *TagSuite) TestVerify(c *C) { ts := time.Unix(1617403017, 0) loc, _ := time.LoadLocation("UTC")