Refactor commit signature parser (#30228)

To make it more flexible and support SSH signature.

The existing tests are not changed, there are also tests covering
`parseTagRef` which also calls `parsePayloadSignature` now. Add some new
tests to `Test_parseTagData`
This commit is contained in:
wxiaoguang 2024-04-02 04:23:17 +08:00 committed by GitHub
parent ca297a90fb
commit 0db554fa63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 135 additions and 99 deletions

View File

@ -26,14 +26,14 @@ type Commit struct {
Author *Signature Author *Signature
Committer *Signature Committer *Signature
CommitMessage string CommitMessage string
Signature *CommitGPGSignature Signature *CommitSignature
Parents []ObjectID // ID strings Parents []ObjectID // ID strings
submoduleCache *ObjectCache submoduleCache *ObjectCache
} }
// CommitGPGSignature represents a git commit signature part. // CommitSignature represents a git commit signature part.
type CommitGPGSignature struct { type CommitSignature struct {
Signature string Signature string
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
func convertPGPSignature(c *object.Commit) *CommitGPGSignature { func convertPGPSignature(c *object.Commit) *CommitSignature {
if c.PGPSignature == "" { if c.PGPSignature == "" {
return nil return nil
} }
@ -57,7 +57,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
return nil return nil
} }
return &CommitGPGSignature{ return &CommitSignature{
Signature: c.PGPSignature, Signature: c.PGPSignature,
Payload: w.String(), Payload: w.String(),
} }

View File

@ -99,7 +99,7 @@ readLoop:
} }
} }
commit.CommitMessage = messageSB.String() commit.CommitMessage = messageSB.String()
commit.Signature = &CommitGPGSignature{ commit.Signature = &CommitSignature{
Signature: signatureSB.String(), Signature: signatureSB.String(),
Payload: payloadSB.String(), Payload: payloadSB.String(),
} }

View File

@ -185,17 +185,15 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
tag.Message = ref["contents"] tag.Message = ref["contents"]
// strip PGP signature if present in contents field
pgpStart := strings.Index(tag.Message, beginpgp) // strip any signature if present in contents field
if pgpStart >= 0 { _, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
tag.Message = tag.Message[0:pgpStart]
}
// annotated tag with GPG signature // annotated tag with GPG signature
if tag.Type == "tag" && ref["contents:signature"] != "" { if tag.Type == "tag" && ref["contents:signature"] != "" {
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
tag.Signature = &CommitGPGSignature{ tag.Signature = &CommitSignature{
Signature: ref["contents:signature"], Signature: ref["contents:signature"],
Payload: payload, Payload: payload,
} }

View File

@ -315,7 +315,7 @@ qbHDASXl
Type: "tag", Type: "tag",
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"), Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
Signature: &CommitGPGSignature{ Signature: &CommitSignature{
Signature: `-----BEGIN PGP SIGNATURE----- Signature: `-----BEGIN PGP SIGNATURE-----
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3

View File

@ -6,16 +6,10 @@ package git
import ( import (
"bytes" "bytes"
"sort" "sort"
"strings"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
const (
beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
endpgp = "\n-----END PGP SIGNATURE-----"
)
// Tag represents a Git tag. // Tag represents a Git tag.
type Tag struct { type Tag struct {
Name string Name string
@ -24,7 +18,7 @@ type Tag struct {
Type string Type string
Tagger *Signature Tagger *Signature
Message string Message string
Signature *CommitGPGSignature Signature *CommitSignature
} }
// Commit return the commit of the tag reference // Commit return the commit of the tag reference
@ -32,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) {
return gitRepo.getCommit(tag.Object) return gitRepo.getCommit(tag.Object)
} }
func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) {
pos := messageStart
signStart, signEnd := -1, -1
for {
eol := bytes.IndexByte(data[pos:], '\n')
if eol < 0 {
break
}
line := data[pos : pos+eol]
signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN "))
signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----"))
if hasPrefix && hasSuffix {
signEndBytes := append([]byte("\n-----END "), signType...)
signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...)
signEnd = bytes.Index(data[pos:], signEndBytes)
if signEnd != -1 {
signStart = pos
signEnd = pos + signEnd + len(signEndBytes)
}
}
pos += eol + 1
}
if signStart != -1 && signEnd != -1 {
msgEnd := max(messageStart, signStart-1)
return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd])
}
return string(data), string(data[messageStart:]), ""
}
// Parse commit information from the (uncompressed) raw // Parse commit information from the (uncompressed) raw
// data from the commit object. // data from the commit object.
// \n\n separate headers from message // \n\n separate headers from message
@ -40,47 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
tag.ID = objectFormat.EmptyObjectID() tag.ID = objectFormat.EmptyObjectID()
tag.Object = objectFormat.EmptyObjectID() tag.Object = objectFormat.EmptyObjectID()
tag.Tagger = &Signature{} tag.Tagger = &Signature{}
// we now have the contents of the commit object. Let's investigate...
nextline := 0 pos := 0
l:
for { for {
eol := bytes.IndexByte(data[nextline:], '\n') eol := bytes.IndexByte(data[pos:], '\n')
switch { if eol == -1 {
case eol > 0: break // shouldn't happen, but could just tolerate it
line := data[nextline : nextline+eol] }
spacepos := bytes.IndexByte(line, ' ') if eol == 0 {
reftype := line[:spacepos] pos++
switch string(reftype) { break // end of headers
}
line := data[pos : pos+eol]
key, val, _ := bytes.Cut(line, []byte(" "))
switch string(key) {
case "object": case "object":
id, err := NewIDFromString(string(line[spacepos+1:])) id, err := NewIDFromString(string(val))
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag.Object = id tag.Object = id
case "type": case "type":
// A commit can have one or more parents tag.Type = string(val) // A commit can have one or more parents
tag.Type = string(line[spacepos+1:])
case "tagger": case "tagger":
tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:])) tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val))
} }
nextline += eol + 1 pos += eol + 1
case eol == 0:
tag.Message = string(data[nextline+1:])
break l
default:
break l
}
}
idx := strings.LastIndex(tag.Message, beginpgp)
if idx > 0 {
endSigIdx := strings.Index(tag.Message[idx:], endpgp)
if endSigIdx > 0 {
tag.Signature = &CommitGPGSignature{
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
}
tag.Message = tag.Message[:idx+1]
} }
payload, msg, sign := parsePayloadSignature(data, pos)
tag.Message = msg
if len(sign) > 0 {
tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
} }
return tag, nil return tag, nil
} }

View File

@ -12,24 +12,28 @@ import (
func Test_parseTagData(t *testing.T) { func Test_parseTagData(t *testing.T) {
testData := []struct { testData := []struct {
data []byte data string
tag Tag expected Tag
}{ }{
{data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a {
data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
type commit type commit
tag 1.22.0 tag 1.22.0
tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100 tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
`), tag: Tag{ `,
expected: Tag{
Name: "", Name: "",
ID: Sha1ObjectFormat.EmptyObjectID(), ID: Sha1ObjectFormat.EmptyObjectID(),
Object: &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a}, Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
Type: "commit", Type: "commit",
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)}, Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
Message: "", Message: "",
Signature: nil, Signature: nil,
}}, },
{data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc },
{
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
type commit type commit
tag 1.22.1 tag 1.22.1
tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100 tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
test message test message
o o
ono`), tag: Tag{ ono`,
expected: Tag{
Name: "", Name: "",
ID: Sha1ObjectFormat.EmptyObjectID(), ID: Sha1ObjectFormat.EmptyObjectID(),
Object: &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc}, Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
Type: "commit", Type: "commit",
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)}, Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
Message: "test message\no\n\nono", Message: "test message\no\n\nono",
Signature: nil, Signature: nil,
}}, },
},
{
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
type commit
tag v0
tagger dummy user <dummy-email@example.com> 1484491741 +0100
dummy message
-----BEGIN SSH SIGNATURE-----
dummy signature
-----END SSH SIGNATURE-----
`,
expected: Tag{
Name: "",
ID: Sha1ObjectFormat.EmptyObjectID(),
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
Type: "commit",
Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
Message: "dummy message",
Signature: &CommitSignature{
Signature: `-----BEGIN SSH SIGNATURE-----
dummy signature
-----END SSH SIGNATURE-----`,
Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
type commit
tag v0
tagger dummy user <dummy-email@example.com> 1484491741 +0100
dummy message`,
},
},
},
} }
for _, test := range testData { for _, test := range testData {
tag, err := parseTagData(Sha1ObjectFormat, test.data) tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data))
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, test.tag.ID, tag.ID) assert.Equal(t, test.expected, *tag)
assert.EqualValues(t, test.tag.Object, tag.Object)
assert.EqualValues(t, test.tag.Name, tag.Name)
assert.EqualValues(t, test.tag.Message, tag.Message)
assert.EqualValues(t, test.tag.Type, tag.Type)
if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) {
assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature)
assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload)
} else {
assert.Nil(t, tag.Signature)
}
if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) {
assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name)
assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email)
assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix())
} else {
assert.Nil(t, tag.Tagger)
}
} }
tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
assert.NoError(t, err)
assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
} }