diff options
Diffstat (limited to 'internal/git/catfile/parse_commit_test.go')
-rw-r--r-- | internal/git/catfile/parse_commit_test.go | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/internal/git/catfile/parse_commit_test.go b/internal/git/catfile/parse_commit_test.go new file mode 100644 index 000000000..2e80dc38f --- /dev/null +++ b/internal/git/catfile/parse_commit_test.go @@ -0,0 +1,531 @@ +package catfile + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + pgpSignature = `-----BEGIN PGP SIGNATURE----- +Version: ObjectivePGP +Comment: https://www.objectivepgp.com +Charset: UTF-8 + +wsFcBAABCgAGBQJecon1AAoJEDYMjTn1G2THmSsP/At/jskLdF0i7p0nKf4JLjeeqRJ4k2IUg87U +ZwV6mbLo5XFm8Sq7CJBAGAhlOZE4BAwKALuawmgs5XMEZwK2z6AIgosGTVpmxDTTI11bXt4XIOdz +qF7c/gUrJOZzjFXOqDsd5UuPRupwznC5eKlLbfImR+NYxKryo8JGdF5t52ph4kChcQsKlSkXuYNI ++9UgbaMclEjb0OLm+mcP9QxW+Cs9JS2Jb4Jh6XONWW1nDN3ZTDDskguIqqF47UxIgSImrmpMcEj9 +YSNU0oMoHM4+1DoXp1t99EGPoAMvO+a5g8gd1jouCIrI6KOX+GeG/TFFM0mQwg/d/N9LR049m8ed +vgqg/lMiWUxQGL2IPpYPcgiUEqfn7ete+NMzQV5zstxF/q7Yj2BhM2L7FPHxKaoy/w5Q/DcAO4wN +5gxVmIvbCDk5JOx8I+boIS8ZxSvIlJ5IWaPrcjg5Mc40it+WHvMqxVnCzH0c6KcXaJ2SibVb59HR +pdRhEXXw/hRN65l/xwyM8sklQalAGu755gNJZ4k9ApBVUssZyiu+te2+bDirAcmK8/x1jvMQY6bn +DFxBE7bMHDp24IFPaVID84Ryt3vSSBEkrUGm7OkyDESTpHCr4sfD5o3LCUCIibTqv/CAhe59mhbB +2AXL7X+EzylKy6C1N5KUUiMTW94AuF6f8FqBoxnf +=U6zM +-----END PGP SIGNATURE-----` + + sshSignature = `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgtc+Qk8jhMwVZk/jFEFCM16LNQb +30q5kK30bbetfjyTMAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQADE1oOMKxqQu86XUQbhCoWx8GnnYHQ/i3mHdA0zPycIlDv8N6BRVDS6b0ja2Avj+s +uNvjRqSEGQJ4q6vhKOnQw= +-----END SSH SIGNATURE-----` +) + +func TestParseCommits(t *testing.T) { + t.Parallel() + + type setupData struct { + content string + oid git.ObjectID + expectedCommit *Commit + expectedErr error + } + + // Valid-but-interesting commits should be test at the FindCommit level. + // Invalid objects (that Git would complain about during fsck) can be + // tested here. + // + // Once a repository contains a pathological object it can be hard to get + // rid of it. Because of this I think it's nicer to ignore such objects + // than to throw hard errors. + for _, tc := range []struct { + desc string + setup func(t *testing.T) setupData + }{ + { + desc: "empty commit object", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{Id: gittest.DefaultObjectHash.EmptyTreeOID.String()}, + SignatureData: SignatureData{Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "no email", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Jane Doe", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{Name: []byte("Jane Doe")}, + }, + SignatureData: SignatureData{Payload: []byte("author Jane Doe"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "normal author", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Au Thor <au.thor@example.com> 1625121079 +0000", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Au Thor"), + Email: []byte("au.thor@example.com"), + Date: timestamppb.New(time.Unix(1625121079, 0)), + Timezone: []byte("+0000"), + }, + }, + SignatureData: SignatureData{Payload: []byte("author Au Thor <au.thor@example.com> 1625121079 +0000"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "author with missing mail", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Au Thor <> 1625121079 +0000", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Au Thor"), + Date: timestamppb.New(time.Unix(1625121079, 0)), + Timezone: []byte("+0000"), + }, + }, + SignatureData: SignatureData{Payload: []byte("author Au Thor <> 1625121079 +0000"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "author with missing date", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Au Thor <au.thor@example.com>", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Au Thor"), + Email: []byte("au.thor@example.com"), + }, + }, + SignatureData: SignatureData{Payload: []byte("author Au Thor <au.thor@example.com>"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "unmatched <", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Jane Doe <janedoe@example.com", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{Name: []byte("Jane Doe")}, + }, + SignatureData: SignatureData{Payload: []byte("author Jane Doe <janedoe@example.com"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "unmatched >", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Jane Doe janedoe@example.com>", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{Name: []byte("Jane Doe janedoe@example.com>")}, + }, + SignatureData: SignatureData{Payload: []byte("author Jane Doe janedoe@example.com>"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "date too high", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Jane Doe <janedoe@example.com> 9007199254740993 +0200", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Jane Doe"), + Email: []byte("janedoe@example.com"), + Date: ×tamppb.Timestamp{Seconds: 9223371974719179007}, + Timezone: []byte("+0200"), + }, + }, + SignatureData: SignatureData{Payload: []byte("author Jane Doe <janedoe@example.com> 9007199254740993 +0200"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "date negative", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "author Jane Doe <janedoe@example.com> -1 +0200", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Jane Doe"), + Email: []byte("janedoe@example.com"), + Date: ×tamppb.Timestamp{Seconds: 9223371974719179007}, + Timezone: []byte("+0200"), + }, + }, + SignatureData: SignatureData{Payload: []byte("author Jane Doe <janedoe@example.com> -1 +0200"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "huge", + setup: func(_ *testing.T) setupData { + repeat := strings.Repeat("A", 100000) + content := "author " + repeat + + return setupData{ + content: content, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte(repeat), + }, + }, + SignatureData: SignatureData{Payload: []byte(content), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "has encoding", + setup: func(_ *testing.T) setupData { + return setupData{ + content: "encoding Windows-1251", + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Encoding: "Windows-1251", + }, + SignatureData: SignatureData{Payload: []byte("encoding Windows-1251"), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "PGP signed commit", + setup: func(_ *testing.T) setupData { + commitData, signedCommitData := createSignedCommitData(gpgSignaturePrefix, pgpSignature, "random commit message") + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + SignatureType: gitalypb.SignatureType_PGP, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("random commit message"), + Body: []byte("random commit message\n"), + BodySize: 22, + }, + SignatureData: SignatureData{Payload: []byte(commitData), Signatures: [][]byte{[]byte(pgpSignature)}}, + }, + } + }, + }, + { + desc: "PGP SHA256-signed signed commit", + setup: func(_ *testing.T) setupData { + commitData, signedCommitData := createSignedCommitData(gpgSignaturePrefixSha256, pgpSignature, "random commit message") + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + SignatureType: gitalypb.SignatureType_PGP, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("random commit message"), + Body: []byte("random commit message\n"), + BodySize: 22, + }, + SignatureData: SignatureData{Payload: []byte(commitData), Signatures: [][]byte{[]byte(pgpSignature)}}, + }, + } + }, + }, + { + desc: "SSH signed commit", + setup: func(_ *testing.T) setupData { + commitData, signedCommitData := createSignedCommitData(gpgSignaturePrefix, sshSignature, "random commit message") + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + SignatureType: gitalypb.SignatureType_SSH, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("random commit message"), + Body: []byte("random commit message\n"), + BodySize: 22, + }, + SignatureData: SignatureData{Payload: []byte(commitData), Signatures: [][]byte{[]byte(sshSignature)}}, + }, + } + }, + }, + { + desc: "garbage signed commit", + setup: func(_ *testing.T) setupData { + _, signedCommitData := createSignedCommitData("gpgsig-garbage", sshSignature, "garbage-signed commit message") + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("garbage-signed commit message"), + Body: []byte("garbage-signed commit message\n"), + BodySize: 30, + }, + SignatureData: SignatureData{Payload: []byte(signedCommitData), Signatures: [][]byte{}}, + }, + } + }, + }, + { + desc: "commits with multiple signatures", + setup: func(_ *testing.T) setupData { + commitData, signedCommitData := createSignedCommitData("gpgsig", pgpSignature, "commit message") + + signatureLines := strings.Split(sshSignature, "\n") + for i, signatureLine := range signatureLines { + signatureLines[i] = " " + signatureLine + } + + signedCommitData = strings.Replace(signedCommitData, "\n\n", fmt.Sprintf("\n%s%s\n\n", "gpgsig-sha256", strings.Join(signatureLines, "\n")), 1) + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + SignatureType: gitalypb.SignatureType_PGP, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("commit message"), + Body: []byte("commit message\n"), + BodySize: 15, + }, + SignatureData: SignatureData{Payload: []byte(commitData), Signatures: [][]byte{[]byte(pgpSignature), []byte(sshSignature)}}, + }, + } + }, + }, + { + desc: "PGP signed commit with headers after the signature", + setup: func(_ *testing.T) setupData { + commitMessage := "random commit message" + commitData, signedCommitData := createSignedCommitData(gpgSignaturePrefix, pgpSignature, "random commit message") + + // Headers after the signature shouldn't be ignored and should be + // part of the commit data. If ignored, malicious users can draft a + // commit similar to a previously signed commit but with additional + // headers and the new commit would still show as verified since we + // won't add the additional headers to commit data. + postSignatureHeader := fmt.Sprintf("parent %s", gittest.DefaultObjectHash.EmptyTreeOID) + modifyCommitData := func(data string) string { + return strings.Replace( + data, + fmt.Sprintf("\n%s", commitMessage), + fmt.Sprintf("%s\n\n%s", postSignatureHeader, commitMessage), + 1, + ) + } + + // We expect that these post signature headers are added to the commit data, + // that way any modified commit will fail verification. + signedCommitData = modifyCommitData(signedCommitData) + commitData = modifyCommitData(commitData) + + return setupData{ + content: signedCommitData, + oid: gittest.DefaultObjectHash.EmptyTreeOID, + expectedCommit: &Commit{ + GitCommit: &gitalypb.GitCommit{ + Id: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Author: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + Committer: &gitalypb.CommitAuthor{ + Name: []byte("Bug Fixer"), + Email: []byte("bugfixer@email.com"), + Date: ×tamppb.Timestamp{Seconds: 1584564725}, + Timezone: []byte("+0100"), + }, + SignatureType: gitalypb.SignatureType_PGP, + TreeId: gittest.DefaultObjectHash.EmptyTreeOID.String(), + Subject: []byte("random commit message"), + Body: []byte("random commit message\n"), + BodySize: 22, + }, + SignatureData: SignatureData{Payload: []byte(commitData), Signatures: [][]byte{[]byte(pgpSignature)}}, + }, + } + }, + }, + } { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + setup := tc.setup(t) + + commit, err := NewParser().ParseCommit(newStaticObject(setup.content, "commit", setup.oid)) + require.Equal(t, setup.expectedErr, err) + testhelper.ProtoEqual(t, setup.expectedCommit.GitCommit, commit.GitCommit) + require.Equal(t, setup.expectedCommit.SignatureData.Payload, commit.SignatureData.Payload) + require.Equal(t, setup.expectedCommit.SignatureData.Signatures, commit.SignatureData.Signatures) + }) + } +} + +func createSignedCommitData(signatureField, signature, commitMessage string) (string, string) { + commitData := fmt.Sprintf(`tree %s +author Bug Fixer <bugfixer@email.com> 1584564725 +0100 +committer Bug Fixer <bugfixer@email.com> 1584564725 +0100 + +%s +`, gittest.DefaultObjectHash.EmptyTreeOID, commitMessage) + + // Each line of the signature needs to start with a space so that Git recognizes it as a continuation of the + // field. + signatureLines := strings.Split(signature, "\n") + for i, signatureLine := range signatureLines { + signatureLines[i] = " " + signatureLine + } + + signedCommitData := strings.Replace(commitData, "\n\n", fmt.Sprintf("\n%s%s\n\n", signatureField, strings.Join(signatureLines, "\n")), 1) + + return commitData, signedCommitData +} |