diff --git a/.gitignore b/.gitignore index 892d69ea2..f0050ac90 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ -build/ +/build/ bin/ testbin/ diff --git a/git/git.go b/git/git.go index 1bff7cfb2..e0cbf35e9 100644 --- a/git/git.go +++ b/git/git.go @@ -17,13 +17,12 @@ limitations under the License. package git import ( - "bytes" "errors" "fmt" "strings" "time" - "github.com/ProtonMail/go-crypto/openpgp" + "github.com/fluxcd/pkg/git/signature" ) const ( @@ -113,19 +112,37 @@ func (c *Commit) AbsoluteReference() string { return c.Hash.Digest() } +// Deprecated: Verify is deprecated, use VerifySSH or VerifyGPG +// wrapper function to ensure backwards compatibility +func (c *Commit) Verify(keyRings ...string) (string, error) { + return c.VerifyGPG(keyRings...) +} + // Verify the Signature of the commit with the given key rings. // It returns the fingerprint of the key the signature was verified // with, or an error. It does not verify the signature of the referencing // tag (if present). Users are expected to explicitly verify the referencing // tag's signature using `c.ReferencingTag.Verify()` -func (c *Commit) Verify(keyRings ...string) (string, error) { - fingerprint, err := verifySignature(c.Signature, c.Encoded, keyRings...) +func (c *Commit) VerifyGPG(keyRings ...string) (string, error) { + fingerprint, err := signature.VerifyPGPSignature(c.Signature, c.Encoded, keyRings...) if err != nil { return "", fmt.Errorf("unable to verify Git commit: %w", err) } return fingerprint, nil } +// VerifySSH verifies the SSH signature of the commit with the given authorized keys. +// It returns the fingerprint of the key the signature was verified with, or an error. +// It does not verify the signature of the referencing tag (if present). Users are +// expected to explicitly verify the referencing tag's signature using `c.ReferencingTag.VerifySSH()` +func (c *Commit) VerifySSH(authorizedKeys ...string) (string, error) { + fingerprint, err := signature.VerifySSHSignature(c.Signature, c.Encoded, authorizedKeys...) + if err != nil { + return "", fmt.Errorf("unable to verify Git commit SSH signature: %w", err) + } + return fingerprint, nil +} + // ShortMessage returns the first 50 characters of a commit subject. func (c *Commit) ShortMessage() string { subject := strings.Split(c.Message, "\n")[0] @@ -152,17 +169,33 @@ type Tag struct { Message string } +// Deprecated: Verify is deprecated, use VerifySSH or VerifyGPG +// wrapper function to ensure backwards compatibility +func (t *Tag) Verify(keyRings ...string) (string, error) { + return t.VerifyGPG(keyRings...) +} + // Verify the Signature of the tag with the given key rings. // It returns the fingerprint of the key the signature was verified // with, or an error. -func (t *Tag) Verify(keyRings ...string) (string, error) { - fingerprint, err := verifySignature(t.Signature, t.Encoded, keyRings...) +func (t *Tag) VerifyGPG(keyRings ...string) (string, error) { + fingerprint, err := signature.VerifyPGPSignature(t.Signature, t.Encoded, keyRings...) if err != nil { return "", fmt.Errorf("unable to verify Git tag: %w", err) } return fingerprint, nil } +// VerifySSH verifies the SSH signature of the tag with the given authorized keys. +// It returns the fingerprint of the key the signature was verified with, or an error. +func (t *Tag) VerifySSH(authorizedKeys ...string) (string, error) { + fingerprint, err := signature.VerifySSHSignature(t.Signature, t.Encoded, authorizedKeys...) + if err != nil { + return "", fmt.Errorf("unable to verify Git tag SSH signature: %w", err) + } + return fingerprint, nil +} + // String returns a short string representation of the tag in the format // of , for eg: "1.0.0@a0c14dc8580a23f79bc654faa79c4f62b46c2c22" // If the tag is lightweight, it won't have a hash, so it'll simply return @@ -210,21 +243,36 @@ func IsSignedTag(t Tag) bool { return t.Signature != "" } -func verifySignature(sig string, payload []byte, keyRings ...string) (string, error) { - if sig == "" { - return "", fmt.Errorf("unable to verify payload as the provided signature is empty") - } +// IsPGPSigned returns true if the commit has a PGP signature. +func (c *Commit) IsPGPSigned() bool { + return signature.IsPGPSignature(c.Signature) +} - for _, r := range keyRings { - reader := strings.NewReader(r) - keyring, err := openpgp.ReadArmoredKeyRing(reader) - if err != nil { - return "", fmt.Errorf("unable to read armored key ring: %w", err) - } - signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewBuffer(payload), bytes.NewBufferString(sig), nil) - if err == nil { - return signer.PrimaryKey.KeyIdString(), nil - } - } - return "", fmt.Errorf("unable to verify payload with any of the given key rings") +// IsSSHSigned returns true if the commit has an SSH signature. +func (c *Commit) IsSSHSigned() bool { + return signature.IsSSHSignature(c.Signature) +} + +// SignatureType returns the type of the commit signature as a string. +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, +// and "unknown" for unrecognized or empty signatures. +func (c *Commit) SignatureType() string { + return signature.GetSignatureType(c.Signature) +} + +// IsPGPSigned returns true if the tag has a PGP signature. +func (t *Tag) IsPGPSigned() bool { + return signature.IsPGPSignature(t.Signature) +} + +// IsSSHSigned returns true if the tag has an SSH signature. +func (t *Tag) IsSSHSigned() bool { + return signature.IsSSHSignature(t.Signature) +} + +// SignatureType returns the type of the tag signature as a string. +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, +// and "unknown" for unrecognized or empty signatures. +func (t *Tag) SignatureType() string { + return signature.GetSignatureType(t.Signature) } diff --git a/git/git_test.go b/git/git_test.go index 1c18d61f5..2c7b6ba8c 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -17,106 +17,25 @@ limitations under the License. package git import ( + "io" + "os" + "path/filepath" "testing" "time" + "github.com/fluxcd/pkg/git/testutils" + "github.com/go-git/go-git/v5/plumbing" . "github.com/onsi/gomega" ) const ( - encodedCommitFixture = `tree f0c522d8cc4c90b73e2bc719305a896e7e3c108a -parent eb167bc68d0a11530923b1f24b4978535d10b879 -author Stefan Prodan 1633681364 +0300 -committer Stefan Prodan 1633681364 +0300 - -Update containerd and runc to fix CVEs - -Signed-off-by: Stefan Prodan -` - - malformedEncodedCommitFixture = `parent eb167bc68d0a11530923b1f24b4978535d10b879 -author Stefan Prodan 1633681364 +0300 -committer Stefan Prodan 1633681364 +0300 - -Update containerd and runc to fix CVEs - -Signed-off-by: Stefan Prodan -` - - signatureCommitFixture = `-----BEGIN PGP SIGNATURE----- - -iHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb -r7nJAQCQU4zEJu04/Q0ac/UaL6htjhq/wTDNMeUM+aWG/LcBogEAqFUea1oR2BJQ -JCJmEtERFh39zNWSazQmxPAFhEE0kbc= -=+Wlj ------END PGP SIGNATURE-----` - - armoredKeyRingFixture = `-----BEGIN PGP PUBLIC KEY BLOCK----- - -mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8 -mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths -TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ -rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K -Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT -C05OtQOiDXjSpkLim81vNVPtI2XEug+9fEA+jeJakyGwwB+K8xqV3QILKCoWHKGx -yWcMHSR6cP9tdXCk2JHZBm1PLSJ8hIgMH/YwBJLYg90u8lLAs9WtpVBKkLplzzgm -B4Z4VxCC+xI1kt+3ZgYvYC+oUXJXrjyAzy+J1f+aWl2+S/79glWgl/xz2VibWMz6 -nZUE+wLMxOQqyOsBALsoE6z81y/7gfn4R/BziBASi1jq/r/wdboFYowmqd39DACX -+i+V0OplP2TN/F5JajzRgkrlq5cwZHinnw+IFwj9RTfOkdGb3YwhBt/h2PP38969 -ZG+y8muNtaIqih1pXj1fz9HRtsiCABN0j+JYpvV2D2xuLL7P1O0dt5BpJ3KqNCRw -mGgO2GLxbwvlulsLidCPxdK/M8g9Eeb/xwA5LVwvjVchHkzHuUT7durn7AT0RWiK -BT8iDfeBB9RKienAbWyybEqRaR6/Tv+mghFIalsDiBPbfm4rsNzsq3ohfByqECiy -yUvs2O3NDwkoaBDkA3GFyKv8/SVpcuL5OkVxAHNCIMhNzSgotQ3KLcQc0IREfFCa -3CsBAC7CsE2bJZ9IA9sbBa3jimVhWUQVudRWiLFeYHUF/hjhqS8IHyFwprjEOLaV -EG0kBO6ELypD/bOsmN9XZLPYyI3y9DM6Vo0KMomE+yK/By/ZMxVfex8/TZreUdhP -VdCLL95Rc4w9io8qFb2qGtYBij2wm0RWLcM0IhXWAtjI3B17IN+6hmv+JpiZccsM -AMNR5/RVdXIl0hzr8LROD0Xe4sTyZ+fm3mvpczoDPQNRrWpmI/9OT58itnVmZ5jM -7djV5y/NjBk63mlqYYfkfWto97wkhg0MnTnOhzdtzSiZQRzj+vf+ilLfIlLnuRr1 -JRV9Skv6xQltcFArx4JyfZCo7JB1ZXcbdFAvIXXS11RTErO0XVrXNm2RenpW/yZA -9f+ESQ/uUB6XNuyqVUnJDAFJFLdzx8sO3DXo7dhIlgpFqgQobUl+APpbU5LT95sm -89UrV0Lt9vh7k6zQtKOjEUhm+dErmuBnJo8MvchAuXLagHjvb58vYBCUxVxzt1KG -2IePwJ/oXIfawNEGad9Lmdo1FYG1u53AKWZmpYOTouu92O50FG2+7dBh0V2vO253 -aIGFRT1r14B1pkCIun7z7B/JELqOkmwmlRrUnxlADZEcQT3z/S8/4+2P7P6kXO7X -/TAX5xBhSqUbKe3DhJSOvf05/RVL5ULc2U2JFGLAtmBOFmnD/u0qoo5UvWliI+v/ -47QnU3RlZmFuIFByb2RhbiA8c3RlZmFuLnByb2RhbkBnbWFpbC5jb20+iJAEExEI -ADgWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34eAwIbAwULCQgHAgYVCgkICwIE -FgIDAQIeAQIXgAAKCRAyma6w5Ahbrzu/AP9l2YpRaWZr6wSQuEn0gMN8DRzsWJPx -pn0akdY7SRP3ngD9GoKgu41FAItnHAJ2KiHv/fHFyHMndNP3kPGPNW4BF+65Aw0E -X34eAxAMAMdYFCHmVA8TZxSTMBDpKYave8RiDCMMMjk26Gl0EPN9f2Y+s5++DhiQ -hojNH9VmJkFwZX1xppxe1y1aLa/U6fBAqMP/IdNH8270iv+A9YIxdsWLmpm99BDO -3suRfsHcOe9T0x/CwRfDNdGM/enGMhYGTgF4VD58DRDE6WntaBhl4JJa300NG6X0 -GM4Gh59DKWDnez/Shulj8demlWmakP5imCVoY+omOEc2k3nH02U+foqaGG5WxZZ+ -GwEPswm2sBxvn8nwjy9gbQwEtzNI7lWYiz36wCj2VS56Udqt+0eNg8WzocUT0XyI -moe1qm8YJQ6fxIzaC431DYi/mCDzgx4EV9ww33SXX3Yp2NL6PsdWJWw2QnoqSMpM -z5otw2KlMgUHkkXEKs0apmK4Hu2b6KD7/ydoQRFUqR38Gb0IZL1tOL6PnbCRUcig -Aypy016W/WMCjBfQ8qxIGTaj5agX2t28hbiURbxZkCkz+Z3OWkO0Rq3Y2hNAYM5s -eTn94JIGGwADBgv/dbSZ9LrBvdMwg8pAtdlLtQdjPiT1i9w5NZuQd7OuKhOxYTEB -NRDTgy4/DgeNThCeOkMB/UQQPtJ3Et45S2YRtnnuvfxgnlz7xlUn765/grtnRk4t -ONjMmb6tZos1FjIJecB/6h4RsvUd2egvtlpD/Z3YKr6MpNjWg4ji7m27e9pcJfP6 -YpTDrq9GamiHy9FS2F2pZlQxriPpVhjCLVn9tFGBIsXNxxn7SP4so6rJBmyHEAlq -iym9wl933e0FIgAw5C1vvprYu2amk+jmVBsJjjCmInW5q/kWAFnFaHBvk+v+/7tX -hywWUI7BqseikgUlkgJ6eU7E9z1DEyuS08x/cViDoNh2ntVUhpnluDu48pdqBvvY -a4uL/D+KI84THUAJ/vZy+q6G3BEb4hI9pFjgrdJpUKubxyZolmkCFZHjV34uOcTc -LQr28P8xW8vQbg5DpIsivxYLqDGXt3OyiItxvLMtw/ypt6PkoeP9A4KDST4StITE -1hrOrPtJ/VRmS2o0iHgEGBEIACAWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34e -AwIbDAAKCRAyma6w5Ahbr6QWAP9/pl2R6r1nuCnXzewSbnH1OLsXf32hFQAjaQ5o -Oomb3gD/TRf/nAdVED+k81GdLzciYdUGtI71/qI47G0nMBluLRE= -=/4e+ ------END PGP PUBLIC KEY BLOCK----- -` - - keyRingFingerprintFixture = "3299AEB0E4085BAF" - - malformedKeyRingFixture = ` ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8 -mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths -TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ -rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K -Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT ------END PGP PUBLIC KEY BLOCK----- -` + signaturePGPSignature = "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----" + signaturePGPMessage = "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----" + signatureSSH = "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----" + signatureX509 = "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----" + signatureUnknown = "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----" + signaturePGPSignatureWithLeadingWhitespace = " " + signaturePGPSignature + signatureSSHWithLeadingWhitespace = " " + signatureSSH ) func TestHash_Algorithm(t *testing.T) { @@ -155,61 +74,6 @@ func TestHash_Algorithm(t *testing.T) { } } -func Test_verifySignature(t *testing.T) { - tests := []struct { - name string - payload []byte - sig string - keyRings []string - want string - wantErr string - }{ - { - name: "Valid commit signature", - payload: []byte(encodedCommitFixture), - sig: signatureCommitFixture, - keyRings: []string{armoredKeyRingFixture}, - want: keyRingFingerprintFixture, - }, - { - name: "Malformed encoded commit", - payload: []byte(malformedEncodedCommitFixture), - sig: signatureCommitFixture, - keyRings: []string{armoredKeyRingFixture}, - wantErr: "unable to verify payload with any of the given key rings", - }, - { - name: "Malformed key ring", - payload: []byte(encodedCommitFixture), - sig: signatureCommitFixture, - keyRings: []string{malformedKeyRingFixture}, - wantErr: "unable to read armored key ring: unexpected EOF", - }, - { - name: "Missing signature", - payload: []byte(encodedCommitFixture), - keyRings: []string{armoredKeyRingFixture}, - wantErr: "unable to verify payload as the provided signature is empty", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - got, err := verifySignature(tt.sig, tt.payload, tt.keyRings...) - if tt.wantErr != "" { - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) - g.Expect(got).To(BeEmpty()) - return - } - - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got).To(Equal(tt.want)) - }) - } -} - func TestHash_Digest(t *testing.T) { tests := []struct { name string @@ -403,3 +267,680 @@ func TestIsConcreteCommit(t *testing.T) { }) } } + +func TestIsAnnotatedTag(t *testing.T) { + tests := []struct { + name string + tag Tag + result bool + }{ + { + name: "annotated tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Encoded: []byte("tag-content"), + }, + result: true, + }, + { + name: "lightweight tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + }, + result: false, + }, + { + name: "empty encoded", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Encoded: []byte{}, + }, + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(IsAnnotatedTag(tt.tag)).To(Equal(tt.result)) + }) + } +} + +func TestIsSignedTag(t *testing.T) { + tests := []struct { + name string + tag Tag + result bool + }{ + { + name: "signed tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Signature: signaturePGPSignature, + }, + result: true, + }, + { + name: "unsigned tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + }, + result: false, + }, + { + name: "empty signature", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Signature: "", + }, + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(IsSignedTag(tt.tag)).To(Equal(tt.result)) + }) + } +} + +func TestTag_String(t *testing.T) { + tests := []struct { + name string + tag *Tag + want string + }{ + { + name: "annotated tag with hash", + tag: &Tag{ + Hash: Hash("5394cb7f48332b2de7c17dd8b8384bbc84b7e738"), + Name: "v1.0.0", + }, + want: "v1.0.0@5394cb7f48332b2de7c17dd8b8384bbc84b7e738", + }, + { + name: "lightweight tag without hash", + tag: &Tag{ + Name: "v1.0.0", + }, + want: "v1.0.0", + }, + { + name: "tag with empty hash", + tag: &Tag{ + Hash: Hash(""), + Name: "v2.0.0", + }, + want: "v2.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.tag.String()).To(Equal(tt.want)) + }) + } +} + +func TestIsSigned(t *testing.T) { + tests := []struct { + name string + commit *Commit + tag *Tag + wantPGPCommit bool + wantSSHCommit bool + wantPGPTag bool + wantSSHTag bool + }{ + { + name: "PGP signed with SIGNATURE prefix", + commit: &Commit{ + Signature: signaturePGPSignature, + }, + tag: &Tag{ + Signature: signaturePGPSignature, + }, + wantPGPCommit: true, + wantSSHCommit: false, + wantPGPTag: true, + wantSSHTag: false, + }, + { + name: "PGP signed with MESSAGE prefix", + commit: &Commit{ + Signature: signaturePGPMessage, + }, + tag: &Tag{ + Signature: signaturePGPMessage, + }, + wantPGPCommit: true, + wantSSHCommit: false, + wantPGPTag: true, + wantSSHTag: false, + }, + { + name: "SSH signed", + commit: &Commit{ + Signature: signatureSSH, + }, + tag: &Tag{ + Signature: signatureSSH, + }, + wantPGPCommit: false, + wantSSHCommit: true, + wantPGPTag: false, + wantSSHTag: true, + }, + { + name: "X509 signed", + commit: &Commit{ + Signature: signatureX509, + }, + tag: &Tag{ + Signature: signatureX509, + }, + wantPGPCommit: false, + wantSSHCommit: false, + wantPGPTag: false, + wantSSHTag: false, + }, + { + name: "unsigned", + commit: &Commit{}, + tag: &Tag{}, + wantPGPCommit: false, + wantSSHCommit: false, + wantPGPTag: false, + wantSSHTag: false, + }, + { + name: "PGP signed with leading whitespace", + commit: &Commit{ + Signature: signaturePGPSignatureWithLeadingWhitespace, + }, + tag: &Tag{ + Signature: signaturePGPSignatureWithLeadingWhitespace, + }, + wantPGPCommit: true, + wantSSHCommit: false, + wantPGPTag: true, + wantSSHTag: false, + }, + { + name: "SSH signed with leading whitespace", + commit: &Commit{ + Signature: signatureSSHWithLeadingWhitespace, + }, + tag: &Tag{ + Signature: signatureSSHWithLeadingWhitespace, + }, + wantPGPCommit: false, + wantSSHCommit: true, + wantPGPTag: false, + wantSSHTag: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.commit.IsPGPSigned()).To(Equal(tt.wantPGPCommit)) + g.Expect(tt.commit.IsSSHSigned()).To(Equal(tt.wantSSHCommit)) + g.Expect(tt.tag.IsPGPSigned()).To(Equal(tt.wantPGPTag)) + g.Expect(tt.tag.IsSSHSigned()).To(Equal(tt.wantSSHTag)) + }) + } +} + +func TestSignatureType(t *testing.T) { + tests := []struct { + name string + commit *Commit + tag *Tag + want string + }{ + { + name: "PGP signed with SIGNATURE prefix", + commit: &Commit{ + Signature: signaturePGPSignature, + }, + tag: &Tag{ + Signature: signaturePGPSignature, + }, + want: "openpgp", + }, + { + name: "PGP signed with MESSAGE prefix", + commit: &Commit{ + Signature: signaturePGPMessage, + }, + tag: &Tag{ + Signature: signaturePGPMessage, + }, + want: "openpgp", + }, + { + name: "SSH signed", + commit: &Commit{ + Signature: signatureSSH, + }, + tag: &Tag{ + Signature: signatureSSH, + }, + want: "ssh", + }, + { + name: "X509 signed", + commit: &Commit{ + Signature: signatureX509, + }, + tag: &Tag{ + Signature: signatureX509, + }, + want: "x509", + }, + { + name: "unsigned", + commit: &Commit{}, + tag: &Tag{}, + want: "empty", + }, + { + name: "unknown signature type", + commit: &Commit{ + Signature: signatureUnknown, + }, + tag: &Tag{ + Signature: signatureUnknown, + }, + want: "unknown", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.commit.SignatureType()).To(Equal(tt.want)) + g.Expect(tt.tag.SignatureType()).To(Equal(tt.want)) + }) + } +} + +func TestCommit_VerifyGPG(t *testing.T) { + testDataDir := filepath.Join("signature", "testdata", "gpg_signatures") + + tests := []struct { + name string + sigFile string + keyFile string + wantErr string + }{ + { + name: "valid PGP signature", + sigFile: "commit_rsa_2048_signed.txt", + keyFile: "key_rsa_2048.pub", + }, + { + name: "missing signature", + sigFile: "commit_unsigned.txt", + keyFile: "key_rsa_2048.pub", + wantErr: "unable to verify Git commit: unable to verify payload as the provided signature is empty", + }, + { + name: "invalid signature", + sigFile: "commit_rsa_2048_signed.txt", + keyFile: "key_ed25519.pub", + wantErr: "unable to verify Git commit: unable to verify payload with any of the given key rings", + }, + { + name: "no key rings provided", + sigFile: "commit_rsa_2048_signed.txt", + wantErr: "unable to verify Git commit: unable to verify payload with any of the given key rings", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.sigFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Create a git.Commit from the parsed object + encoded := &plumbing.MemoryObject{} + err = commitObj.EncodeWithoutSignature(encoded) + g.Expect(err).ToNot(HaveOccurred()) + reader, err := encoded.Reader() + g.Expect(err).ToNot(HaveOccurred()) + b, err := io.ReadAll(reader) + g.Expect(err).ToNot(HaveOccurred()) + + gitCommit := &Commit{ + Signature: commitObj.PGPSignature, + Encoded: b, + } + + // Prepare key rings + var keyRings []string + if tt.keyFile != "" { + publicKey, err := os.ReadFile(filepath.Join(testDataDir, tt.keyFile)) + g.Expect(err).ToNot(HaveOccurred()) + keyRings = append(keyRings, string(publicKey)) + } + + // get result from deprecated function + depFingerprint, depErr := gitCommit.Verify(keyRings...) + + // Verify the signature using the git.Commit's VerifyGPG method + fingerprint, err := gitCommit.VerifyGPG(keyRings...) + + g.Expect(fingerprint).To(ContainSubstring(depFingerprint)) + if err == nil { + g.Expect(depErr).ToNot(HaveOccurred()) + } else { + g.Expect(err.Error()).To(ContainSubstring(depErr.Error())) + } + + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + }) + } +} + +func TestTag_VerifyGPG(t *testing.T) { + testDataDir := filepath.Join("signature", "testdata", "gpg_signatures") + + tests := []struct { + name string + sigFile string + keyFile string + wantErr string + }{ + { + name: "valid PGP signature", + sigFile: "tag_rsa_2048_signed.txt", + keyFile: "key_rsa_2048.pub", + }, + { + name: "missing signature", + sigFile: "tag_unsigned.txt", + keyFile: "key_rsa_2048.pub", + wantErr: "unable to verify Git tag: unable to verify payload as the provided signature is empty", + }, + { + name: "invalid signature", + sigFile: "tag_rsa_2048_signed.txt", + keyFile: "key_ed25519.pub", + wantErr: "unable to verify Git tag: unable to verify payload with any of the given key rings", + }, + { + name: "no key rings provided", + sigFile: "tag_rsa_2048_signed.txt", + wantErr: "unable to verify Git tag: unable to verify payload with any of the given key rings", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the tag from the fixture file + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.sigFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Create a git.Tag from the parsed object + encoded := &plumbing.MemoryObject{} + err = tagObj.EncodeWithoutSignature(encoded) + g.Expect(err).ToNot(HaveOccurred()) + reader, err := encoded.Reader() + g.Expect(err).ToNot(HaveOccurred()) + b, err := io.ReadAll(reader) + g.Expect(err).ToNot(HaveOccurred()) + + gitTag := &Tag{ + Signature: tagObj.PGPSignature, + Encoded: b, + } + + // Prepare key rings + var keyRings []string + if tt.keyFile != "" { + publicKey, err := os.ReadFile(filepath.Join(testDataDir, tt.keyFile)) + g.Expect(err).ToNot(HaveOccurred()) + keyRings = append(keyRings, string(publicKey)) + } + + // get result from deprecated function + depFingerprint, depErr := gitTag.Verify(keyRings...) + + // Verify the signature using the git.Tag's VerifyGPG method + fingerprint, err := gitTag.VerifyGPG(keyRings...) + + g.Expect(fingerprint).To(ContainSubstring(depFingerprint)) + if err == nil { + g.Expect(depErr).ToNot(HaveOccurred()) + } else { + g.Expect(err.Error()).To(ContainSubstring(depErr.Error())) + } + + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + }) + } +} + +func TestCommit_VerifySSH(t *testing.T) { + testDataDir := filepath.Join("signature", "testdata", "ssh_signatures") + + tests := []struct { + name string + sigFile string + authorizedKeys string + wantErr string + }{ + { + name: "valid SSH signature", + sigFile: "commit_rsa_signed.txt", + authorizedKeys: "key_rsa.pub", + }, + { + name: "missing signature", + sigFile: "commit_unsigned.txt", + authorizedKeys: "key_rsa.pub", + wantErr: "unable to verify Git commit SSH signature: unable to verify payload as the provided signature is empty", + }, + { + name: "invalid signature", + sigFile: "commit_rsa_signed.txt", + authorizedKeys: "key_ed25519.pub", + wantErr: "unable to verify Git commit SSH signature: unable to verify payload with any of the given authorized keys", + }, + { + name: "no authorized keys provided", + sigFile: "commit_rsa_signed.txt", + wantErr: "unable to verify Git commit SSH signature: unable to verify payload with any of the given authorized keys", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.sigFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Create a git.Commit from the parsed object + encoded := &plumbing.MemoryObject{} + err = commitObj.EncodeWithoutSignature(encoded) + g.Expect(err).ToNot(HaveOccurred()) + reader, err := encoded.Reader() + g.Expect(err).ToNot(HaveOccurred()) + b, err := io.ReadAll(reader) + g.Expect(err).ToNot(HaveOccurred()) + + gitCommit := &Commit{ + Signature: commitObj.PGPSignature, + Encoded: b, + } + + // Prepare authorized keys + var authorizedKeys []string + if tt.authorizedKeys != "" { + authorizedKey, err := os.ReadFile(filepath.Join(testDataDir, tt.authorizedKeys)) + g.Expect(err).ToNot(HaveOccurred()) + authorizedKeys = append(authorizedKeys, string(authorizedKey)) + } + + // Verify the signature using the git.Commit's VerifySSH method + fingerprint, err := gitCommit.VerifySSH(authorizedKeys...) + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + }) + } +} + +func TestTag_VerifySSH(t *testing.T) { + testDataDir := filepath.Join("signature", "testdata", "ssh_signatures") + + tests := []struct { + name string + sigFile string + authorizedKeys string + wantErr string + }{ + { + name: "valid SSH signature", + sigFile: "tag_rsa_signed.txt", + authorizedKeys: "key_rsa.pub", + }, + { + name: "missing signature", + sigFile: "tag_unsigned.txt", + authorizedKeys: "key_rsa.pub", + wantErr: "unable to verify Git tag SSH signature: unable to verify payload as the provided signature is empty", + }, + { + name: "invalid signature", + sigFile: "tag_rsa_signed.txt", + authorizedKeys: "key_ed25519.pub", + wantErr: "unable to verify Git tag SSH signature: unable to verify payload with any of the given authorized keys", + }, + { + name: "no authorized keys provided", + sigFile: "tag_rsa_signed.txt", + wantErr: "unable to verify Git tag SSH signature: unable to verify payload with any of the given authorized keys", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the tag from the fixture file + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.sigFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Create a git.Tag from the parsed object + encoded := &plumbing.MemoryObject{} + err = tagObj.EncodeWithoutSignature(encoded) + g.Expect(err).ToNot(HaveOccurred()) + reader, err := encoded.Reader() + g.Expect(err).ToNot(HaveOccurred()) + b, err := io.ReadAll(reader) + g.Expect(err).ToNot(HaveOccurred()) + + gitTag := &Tag{ + Signature: tagObj.PGPSignature, + Encoded: b, + } + + // Prepare authorized keys + var authorizedKeys []string + if tt.authorizedKeys != "" { + authorizedKey, err := os.ReadFile(filepath.Join(testDataDir, tt.authorizedKeys)) + g.Expect(err).ToNot(HaveOccurred()) + authorizedKeys = append(authorizedKeys, string(authorizedKey)) + } + + // Verify the signature using the git.Tag's VerifySSH method + fingerprint, err := gitTag.VerifySSH(authorizedKeys...) + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + }) + } +} + +func TestErrRepositoryNotFound_Error(t *testing.T) { + tests := []struct { + name string + err ErrRepositoryNotFound + want string + }{ + { + name: "with message and URL", + err: ErrRepositoryNotFound{ + Message: "repository not found", + URL: "https://github.com/example/repo.git", + }, + want: "repository not found: git repository: 'https://github.com/example/repo.git'", + }, + { + name: "with empty message", + err: ErrRepositoryNotFound{ + Message: "", + URL: "https://github.com/example/repo.git", + }, + want: ": git repository: 'https://github.com/example/repo.git'", + }, + { + name: "with empty URL", + err: ErrRepositoryNotFound{ + Message: "repository not found", + URL: "", + }, + want: "repository not found: git repository: ''", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.err.Error()).To(Equal(tt.want)) + }) + } +} diff --git a/git/go.mod b/git/go.mod index a825eba8f..77b9f89e3 100644 --- a/git/go.mod +++ b/git/go.mod @@ -20,6 +20,7 @@ require ( github.com/fluxcd/pkg/version v0.15.0 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.1 + github.com/hiddeco/sshsig v0.2.0 github.com/onsi/gomega v1.40.0 golang.org/x/crypto v0.50.0 ) diff --git a/git/go.sum b/git/go.sum index 8d3ffe964..4e0ae2093 100644 --- a/git/go.sum +++ b/git/go.sum @@ -42,6 +42,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/git/gogit/clone.go b/git/gogit/clone.go index 592044500..b59744db0 100644 --- a/git/gogit/clone.go +++ b/git/gogit/clone.go @@ -19,7 +19,6 @@ package gogit import ( "context" "fmt" - "io" "os" "sort" "strings" @@ -29,11 +28,11 @@ import ( extgogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/storage/memory" "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/internal/build" "github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/version" ) @@ -136,7 +135,7 @@ func (g *Client) cloneBranch(ctx context.Context, url, branch string, opts repos } g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return buildCommitWithRef(cc, nil, ref) + return build.CommitWithRef(cc, nil, ref) } func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -236,7 +235,7 @@ func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository. g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return buildCommitWithRef(cc, tagObj, ref) + return build.CommitWithRef(cc, tagObj, ref) } func (g *Client) cloneCommit(ctx context.Context, url, commit string, opts repository.CloneConfig) (*git.Commit, error) { @@ -305,7 +304,7 @@ func (g *Client) cloneCommit(ctx context.Context, url, commit string, opts repos g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return buildCommitWithRef(cc, nil, cloneOpts.ReferenceName) + return build.CommitWithRef(cc, nil, cloneOpts.ReferenceName) } func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -439,7 +438,7 @@ func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts re g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return buildCommitWithRef(cc, tagObj, tagRef.Name()) + return build.CommitWithRef(cc, tagObj, tagRef.Name()) } func (g *Client) cloneRefName(ctx context.Context, url string, refName string, cloneOpts repository.CloneConfig) (*git.Commit, error) { @@ -574,83 +573,6 @@ func filterRefs(refs []*plumbing.Reference, currentRef plumbing.ReferenceName) s return "" } -func buildSignature(s object.Signature) git.Signature { - return git.Signature{ - Name: s.Name, - Email: s.Email, - When: s.When, - } -} - -func buildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { - if t == nil { - return &git.Tag{ - Name: ref.Short(), - }, nil - } - - encoded := &plumbing.MemoryObject{} - if err := t.EncodeWithoutSignature(encoded); err != nil { - return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) - } - reader, err := encoded.Reader() - if err != nil { - return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) - } - b, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("unable to read encoded tag '%s': %w", t.Name, err) - } - - return &git.Tag{ - Hash: []byte(t.Hash.String()), - Name: t.Name, - Author: buildSignature(t.Tagger), - Signature: t.PGPSignature, - Encoded: b, - Message: t.Message, - }, nil -} - -func buildCommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceName) (*git.Commit, error) { - if c == nil { - return nil, fmt.Errorf("unable to construct commit: no object") - } - - // Encode commit components excluding signature into SignedData. - encoded := &plumbing.MemoryObject{} - if err := c.EncodeWithoutSignature(encoded); err != nil { - return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) - } - reader, err := encoded.Reader() - if err != nil { - return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) - } - b, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("unable to read encoded commit '%s': %w", c.Hash, err) - } - cc := &git.Commit{ - Hash: []byte(c.Hash.String()), - Reference: ref.String(), - Author: buildSignature(c.Author), - Committer: buildSignature(c.Committer), - Signature: c.PGPSignature, - Encoded: b, - Message: c.Message, - } - - if ref.IsTag() { - tt, err := buildTag(t, ref) - if err != nil { - return nil, err - } - cc.ReferencingTag = tt - } - - return cc, nil -} - func isRemoteBranchNotFoundErr(err error, ref string) bool { return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref '%s'", ref)) } diff --git a/git/internal/build/build.go b/git/internal/build/build.go new file mode 100644 index 000000000..773da26cb --- /dev/null +++ b/git/internal/build/build.go @@ -0,0 +1,103 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package build + +import ( + "fmt" + "io" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/fluxcd/pkg/git" +) + +func signature(s object.Signature) git.Signature { + return git.Signature{ + Name: s.Name, + Email: s.Email, + When: s.When, + } +} + +func Tag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { + if t == nil { + return &git.Tag{ + Name: ref.Short(), + }, nil + } + + encoded := &plumbing.MemoryObject{} + if err := t.EncodeWithoutSignature(encoded); err != nil { + return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) + } + reader, err := encoded.Reader() + if err != nil { + return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) + } + b, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("unable to read encoded tag '%s': %w", t.Name, err) + } + + return &git.Tag{ + Hash: []byte(t.Hash.String()), + Name: t.Name, + Author: signature(t.Tagger), + Signature: t.PGPSignature, + Encoded: b, + Message: t.Message, + }, nil +} + +func CommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceName) (*git.Commit, error) { + if c == nil { + return nil, fmt.Errorf("unable to construct commit: no object") + } + + encoded := &plumbing.MemoryObject{} + if err := c.EncodeWithoutSignature(encoded); err != nil { + return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) + } + reader, err := encoded.Reader() + if err != nil { + return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) + } + b, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("unable to read encoded commit '%s': %w", c.Hash, err) + } + cc := &git.Commit{ + Hash: []byte(c.Hash.String()), + Reference: ref.String(), + Author: signature(c.Author), + Committer: signature(c.Committer), + Signature: c.PGPSignature, + Encoded: b, + Message: c.Message, + } + + if ref.IsTag() { + tt, err := Tag(t, ref) + if err != nil { + return nil, err + } + cc.ReferencingTag = tt + } + + return cc, nil +} diff --git a/git/internal/e2e/go.mod b/git/internal/e2e/go.mod index f6406c7a6..dfc9450b3 100644 --- a/git/internal/e2e/go.mod +++ b/git/internal/e2e/go.mod @@ -49,6 +49,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hiddeco/sshsig v0.2.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect diff --git a/git/internal/e2e/go.sum b/git/internal/e2e/go.sum index 33e7a1e6a..edd773f33 100644 --- a/git/internal/e2e/go.sum +++ b/git/internal/e2e/go.sum @@ -77,6 +77,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= diff --git a/git/signature/gpg_signature.go b/git/signature/gpg_signature.go new file mode 100644 index 000000000..2f0d09200 --- /dev/null +++ b/git/signature/gpg_signature.go @@ -0,0 +1,71 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "bytes" + "fmt" + "strings" + + "github.com/ProtonMail/go-crypto/openpgp" +) + +// PGPSignaturePrefix is the prefix used by Git to identify PGP signatures. +// https://github.com/git/git/blob/7b2bccb0d58d4f24705bf985de1f4612e4cf06e5/gpg-interface.c#L56 +var PGPSignaturePrefix = []string{ + "-----BEGIN PGP SIGNATURE-----", + "-----BEGIN PGP MESSAGE-----", +} + +// VerifyPGPSignature verifies the PGP signature against the payload using +// the provided key rings. It returns the key ID of the key that +// successfully verified the signature, or an error. +func VerifyPGPSignature(signature string, payload []byte, keyRings ...string) (string, error) { + if signature == "" { + return "", fmt.Errorf("unable to verify payload as the provided signature is empty") + } + + if len(payload) == 0 { + return "", fmt.Errorf("unable to verify payload as the provided payload is empty") + } + + if !IsPGPSignature(signature) { + return "", fmt.Errorf("unable to verify openPGP signature, detected signature format: %s", GetSignatureType(signature)) + } + + // record reading of armored key error. This error will be returned of no valid key was found. + var readKeyRingError error + + for _, r := range keyRings { + reader := strings.NewReader(r) + keyring, err := openpgp.ReadArmoredKeyRing(reader) + if err != nil && readKeyRingError == nil { + readKeyRingError = fmt.Errorf("unable to read armored key ring: %w", err) + continue + } + signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(payload), strings.NewReader(signature), nil) + if err == nil { + return signer.PrimaryKey.KeyIdString(), nil + } + } + + if readKeyRingError != nil { + return "", readKeyRingError + } + + return "", fmt.Errorf("unable to verify payload with any of the given key rings") +} diff --git a/git/signature/gpg_signature_test.go b/git/signature/gpg_signature_test.go new file mode 100644 index 000000000..28571fe27 --- /dev/null +++ b/git/signature/gpg_signature_test.go @@ -0,0 +1,411 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/fluxcd/pkg/git/internal/build" + "github.com/fluxcd/pkg/git/signature" + "github.com/fluxcd/pkg/git/testutils" + "github.com/go-git/go-git/v5/plumbing" + . "github.com/onsi/gomega" +) + +const ( + encodedCommitFixture = `tree f0c522d8cc4c90b73e2bc719305a896e7e3c108a +parent eb167bc68d0a11530923b1f24b4978535d10b879 +author Stefan Prodan 1633681364 +0300 +committer Stefan Prodan 1633681364 +0300 + +Update containerd and runc to fix CVEs + +Signed-off-by: Stefan Prodan +` + + malformedEncodedCommitFixture = `parent eb167bc68d0a11530923b1f24b4978535d10b879 +author Stefan Prodan 1633681364 +0300 +committer Stefan Prodan 1633681364 +0300 + +Update containerd and runc to fix CVEs + +Signed-off-by: Stefan Prodan +` + + signatureCommitFixture = `-----BEGIN PGP SIGNATURE----- + +iHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb +r7nJAQCQU4zEJu04/Q0ac/UaL6htjhq/wTDNMeUM+aWG/LcBogEAqFUea1oR2BJQ +JCJmEtERFh39zNWSazQmxPAFhEE0kbc= +=+Wlj +-----END PGP SIGNATURE-----` + + armoredKeyRingFixture = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8 +mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths +TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ +rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K +Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT +C05OtQOiDXjSpkLim81vNVPtI2XEug+9fEA+jeJakyGwwB+K8xqV3QILKCoWHKGx +yWcMHSR6cP9tdXCk2JHZBm1PLSJ8hIgMH/YwBJLYg90u8lLAs9WtpVBKkLplzzgm +B4Z4VxCC+xI1kt+3ZgYvYC+oUXJXrjyAzy+J1f+aWl2+S/79glWgl/xz2VibWMz6 +nZUE+wLMxOQqyOsBALsoE6z81y/7gfn4R/BziBASi1jq/r/wdboFYowmqd39DACX ++i+V0OplP2TN/F5JajzRgkrlq5cwZHinnw+IFwj9RTfOkdGb3YwhBt/h2PP38969 +ZG+y8muNtaIqih1pXj1fz9HRtsiCABN0j+JYpvV2D2xuLL7P1O0dt5BpJ3KqNCRw +mGgO2GLxbwvlulsLidCPxdK/M8g9Eeb/xwA5LVwvjVchHkzHuUT7durn7AT0RWiK +BT8iDfeBB9RKienAbWyybEqRaR6/Tv+mghFIalsDiBPbfm4rsNzsq3ohfByqECiy +yUvs2O3NDwkoaBDkA3GFyKv8/SVpcuL5OkVxAHNCIMhNzSgotQ3KLcQc0IREfFCa +3CsBAC7CsE2bJZ9IA9sbBa3jimVhWUQVudRWiLFeYHUF/hjhqS8IHyFwprjEOLaV +EG0kBO6ELypD/bOsmN9XZLPYyI3y9DM6Vo0KMomE+yK/By/ZMxVfex8/TZreUdhP +VdCLL95Rc4w9io8qFb2qGtYBij2wm0RWLcM0IhXWAtjI3B17IN+6hmv+JpiZccsM +AMNR5/RVdXIl0hzr8LROD0Xe4sTyZ+fm3mvpczoDPQNRrWpmI/9OT58itnVmZ5jM +7djV5y/NjBk63mlqYYfkfWto97wkhg0MnTnOhzdtzSiZQRzj+vf+ilLfIlLnuRr1 +JRV9Skv6xQltcFArx4JyfZCo7JB1ZXcbdFAvIXXS11RTErO0XVrXNm2RenpW/yZA +9f+ESQ/uUB6XNuyqVUnJDAFJFLdzx8sO3DXo7dhIlgpFqgQobUl+APpbU5LT95sm +89UrV0Lt9vh7k6zQtKOjEUhm+dErmuBnJo8MvchAuXLagHjvb58vYBCUxVxzt1KG +2IePwJ/oXIfawNEGad9Lmdo1FYG1u53AKWZmpYOTouu92O50FG2+7dBh0V2vO253 +aIGFRT1r14B1pkCIun7z7B/JELqOkmwmlRrUnxlADZEcQT3z/S8/4+2P7P6kXO7X +/TAX5xBhSqUbKe3DhJSOvf05/RVL5ULc2U2JFGLAtmBOFmnD/u0qoo5UvWliI+v/ +47QnU3RlZmFuIFByb2RhbiA8c3RlZmFuLnByb2RhbkBnbWFpbC5jb20+iJAEExEI +ADgWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34eAwIbAwULCQgHAgYVCgkICwIE +FgIDAQIeAQIXgAAKCRAyma6w5Ahbrzu/AP9l2YpRaWZr6wSQuEn0gMN8DRzsWJPx +pn0akdY7SRP3ngD9GoKgu41FAItnHAJ2KiHv/fHFyHMndNP3kPGPNW4BF+65Aw0E +X34eAxAMAMdYFCHmVA8TZxSTMBDpKYave8RiDCMMMjk26Gl0EPN9f2Y+s5++DhiQ +hojNH9VmJkFwZX1xppxe1y1aLa/U6fBAqMP/IdNH8270iv+A9YIxdsWLmpm99BDO +3suRfsHcOe9T0x/CwRfDNdGM/enGMhYGTgF4VD58DRDE6WntaBhl4JJa300NG6X0 +GM4Gh59DKWDnez/Shulj8demlWmakP5imCVoY+omOEc2k3nH02U+foqaGG5WxZZ+ +GwEPswm2sBxvn8nwjy9gbQwEtzNI7lWYiz36wCj2VS56Udqt+0eNg8WzocUT0XyI +moe1qm8YJQ6fxIzaC431DYi/mCDzgx4EV9ww33SXX3Yp2NL6PsdWJWw2QnoqSMpM +z5otw2KlMgUHkkXEKs0apmK4Hu2b6KD7/ydoQRFUqR38Gb0IZL1tOL6PnbCRUcig +Aypy016W/WMCjBfQ8qxIGTaj5agX2t28hbiURbxZkCkz+Z3OWkO0Rq3Y2hNAYM5s +eTn94JIGGwADBgv/dbSZ9LrBvdMwg8pAtdlLtQdjPiT1i9w5NZuQd7OuKhOxYTEB +NRDTgy4/DgeNThCeOkMB/UQQPtJ3Et45S2YRtnnuvfxgnlz7xlUn765/grtnRk4t +ONjMmb6tZos1FjIJecB/6h4RsvUd2egvtlpD/Z3YKr6MpNjWg4ji7m27e9pcJfP6 +YpTDrq9GamiHy9FS2F2pZlQxriPpVhjCLVn9tFGBIsXNxxn7SP4so6rJBmyHEAlq +iym9wl933e0FIgAw5C1vvprYu2amk+jmVBsJjjCmInW5q/kWAFnFaHBvk+v+/7tX +hywWUI7BqseikgUlkgJ6eU7E9z1DEyuS08x/cViDoNh2ntVUhpnluDu48pdqBvvY +a4uL/D+KI84THUAJ/vZy+q6G3BEb4hI9pFjgrdJpUKubxyZolmkCFZHjV34uOcTc +LQr28P8xW8vQbg5DpIsivxYLqDGXt3OyiItxvLMtw/ypt6PkoeP9A4KDST4StITE +1hrOrPtJ/VRmS2o0iHgEGBEIACAWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34e +AwIbDAAKCRAyma6w5Ahbr6QWAP9/pl2R6r1nuCnXzewSbnH1OLsXf32hFQAjaQ5o +Oomb3gD/TRf/nAdVED+k81GdLzciYdUGtI71/qI47G0nMBluLRE= +=/4e+ +-----END PGP PUBLIC KEY BLOCK-----` + + keyRingFingerprintFixture = "3299AEB0E4085BAF" + + malformedKeyRingFixture = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8 +mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths +TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ +rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K +Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT +-----END PGP PUBLIC KEY BLOCK-----` +) + +func TestVerifyPGPSignature(t *testing.T) { + tests := []struct { + name string + payload []byte + sig string + keyRings []string + want string + wantErr string + }{ + { + name: "Valid commit signature", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{armoredKeyRingFixture}, + want: keyRingFingerprintFixture, + }, + { + name: "Malformed encoded commit", + payload: []byte(malformedEncodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Malformed key ring", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{malformedKeyRingFixture}, + wantErr: "unable to read armored key ring: unexpected EOF", + }, + { + name: "Missing signature", + payload: []byte(encodedCommitFixture), + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload as the provided signature is empty", + }, + { + name: "Empty payload", + payload: []byte{}, + sig: signatureCommitFixture, + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload as the provided payload is empty", + }, + { + name: "Non-PGP signature", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify openPGP signature, detected signature format: ssh", + }, + { + name: "Malformed key ring followed by valid key ring", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{malformedKeyRingFixture, armoredKeyRingFixture}, + want: keyRingFingerprintFixture, + }, + { + name: "Valid key ring followed by malformed key ring", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{armoredKeyRingFixture, malformedKeyRingFixture}, + want: keyRingFingerprintFixture, + }, + { + name: "Multiple malformed key rings", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture, + keyRings: []string{malformedKeyRingFixture, malformedKeyRingFixture}, + wantErr: "unable to read armored key ring: unexpected EOF", + }, + { + name: "Missing END PGP SIGNATURE marker", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP SIGNATURE-----\n\niHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Missing END PGP MESSAGE marker", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP MESSAGE-----\n\niHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Corrupted base64 body", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP SIGNATURE-----\n\n!!!!!!\n-----END PGP SIGNATURE-----", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Empty body between markers", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP SIGNATURE-----\n\n-----END PGP SIGNATURE-----", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Truncated signature mid-base64", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP SIGNATURE-----\n\niHUEABEIAB0WIQQHgExUr4FrLdKzpNYy", + keyRings: []string{armoredKeyRingFixture}, + wantErr: "unable to verify payload with any of the given key rings", + }, + { + name: "Mismatched BEGIN/END markers", + payload: []byte(encodedCommitFixture), + sig: "-----BEGIN PGP SIGNATURE-----\n\niHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb\nr7nJAQCQU4zEJu04/Q0ac/UaL6htjhq/wTDNMeUM+aWG/LcBogEAqFUea1oR2BJQ\nJCJmEtERFh39zNWSazQmxPAFhEE0kbc=\n=+Wlj\n-----END PGP MESSAGE-----", + keyRings: []string{armoredKeyRingFixture}, + want: keyRingFingerprintFixture, + }, + { + name: "Extra data after END marker", + payload: []byte(encodedCommitFixture), + sig: signatureCommitFixture + "\ngarbage-data-after-end", + keyRings: []string{armoredKeyRingFixture}, + want: keyRingFingerprintFixture, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := signature.VerifyPGPSignature(tt.sig, tt.payload, tt.keyRings...) + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(got).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + +func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { + testDataDir := filepath.Join("testdata", "gpg_signatures") + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + commitFile string + tagFile string + keyFile string + wantErr bool + }{ + {"rsa_2048 valid signature", "commit_rsa_2048_signed.txt", "tag_rsa_2048_signed.txt", "key_rsa_2048.pub", false}, + {"rsa_4096 valid signature", "commit_rsa_4096_signed.txt", "tag_rsa_4096_signed.txt", "key_rsa_4096.pub", false}, + {"ed25519 valid signature", "commit_ed25519_signed.txt", "tag_ed25519_signed.txt", "key_ed25519.pub", false}, + {"ecdsa_p256 valid signature", "commit_ecdsa_p256_signed.txt", "tag_ecdsa_p256_signed.txt", "key_ecdsa_p256.pub", false}, + {"ecdsa_p384 valid signature", "commit_ecdsa_p384_signed.txt", "tag_ecdsa_p384_signed.txt", "key_ecdsa_p384.pub", false}, + {"ecdsa_p521 valid signature", "commit_ecdsa_p521_signed.txt", "tag_ecdsa_p521_signed.txt", "key_ecdsa_p521.pub", false}, + {"brainpool_p256 valid signature", "commit_brainpool_p256_signed.txt", "tag_brainpool_p256_signed.txt", "key_brainpool_p256.pub", false}, + {"brainpool_p384 valid signature", "commit_brainpool_p384_signed.txt", "tag_brainpool_p384_signed.txt", "key_brainpool_p384.pub", false}, + {"brainpool_p512 valid signature", "commit_brainpool_p512_signed.txt", "tag_brainpool_p512_signed.txt", "key_brainpool_p512.pub", false}, + } + + var allKeysRing []string + for _, kt := range keyTypes { + publicKey, err := os.ReadFile(filepath.Join(testDataDir, kt.keyFile)) + if err != nil { + t.Fatalf("failed to read public key file %s: %v", kt.keyFile, err) + } + allKeysRing = append(allKeysRing, string(publicKey)) + } + + for _, kt := range keyTypes { + t.Run(kt.name+" tag", func(t *testing.T) { + g := NewWithT(t) + + // Parse the tag from the fixture file + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.tagFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Build a git.Tag using build.Tag + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + g.Expect(err).ToNot(HaveOccurred()) + + // Read the public key + publicKey, err := os.ReadFile(filepath.Join(testDataDir, kt.keyFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Verify the signature using the git.Tag's Signature and Encoded fields + fingerprint, err := signature.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, string(publicKey)) + if kt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + + // Verify the signature using the multi-key keyring + fingerprint, err = signature.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, allKeysRing...) + if kt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + + }) + } + + for _, kt := range keyTypes { + t.Run(kt.name+" commit", func(t *testing.T) { + g := NewWithT(t) + + // Parse the commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.commitFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + g.Expect(err).ToNot(HaveOccurred()) + + // Read the public key + publicKey, err := os.ReadFile(filepath.Join(testDataDir, kt.keyFile)) + g.Expect(err).ToNot(HaveOccurred()) + + // Verify the signature using the git.Commit's Signature and Encoded fields + fingerprint, err := signature.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, string(publicKey)) + if kt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + + // Verify the signature using the multi-key keyring + fingerprint, err = signature.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, allKeysRing...) + if kt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) + + }) + } + + // Test error cases + t.Run("unsigned commit", func(t *testing.T) { + g := NewWithT(t) + + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) + g.Expect(err).ToNot(HaveOccurred()) + + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + g.Expect(err).ToNot(HaveOccurred()) + + publicKey, err := os.ReadFile(filepath.Join(testDataDir, "key_rsa_2048.pub")) + g.Expect(err).ToNot(HaveOccurred()) + + fingerprint, err := signature.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, string(publicKey)) + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + }) + + t.Run("unsigned tag", func(t *testing.T) { + g := NewWithT(t) + + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_unsigned.txt")) + g.Expect(err).ToNot(HaveOccurred()) + + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + g.Expect(err).ToNot(HaveOccurred()) + + publicKey, err := os.ReadFile(filepath.Join(testDataDir, "key_rsa_2048.pub")) + g.Expect(err).ToNot(HaveOccurred()) + + fingerprint, err := signature.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, string(publicKey)) + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + }) +} diff --git a/git/signature/signature.go b/git/signature/signature.go new file mode 100644 index 000000000..1fd833c76 --- /dev/null +++ b/git/signature/signature.go @@ -0,0 +1,94 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "slices" + "strings" +) + +// SignatureType represents the type of a signature. +type signatureType string + +const ( + // SignatureTypePGP represents a openPGP signature. + signatureTypePGP signatureType = "openpgp" + // SignatureTypeSSH represents an SSH signature. + signatureTypeSSH signatureType = "ssh" + // SignatureTypeX509 represents an x509 signature. + signatureTypeX509 signatureType = "x509" + // SignatureTypeUnknown represents an unknown signature type. + signatureTypeUnknown signatureType = "unknown" + // SignatureTypeEmpty represents an empty signature. + signatureTypeEmpty signatureType = "empty" +) + +// IsX509Signature is the prefix used by Git to identify x509 signatures. +// https://github.com/git/git/blob/7b2bccb0d58d4f24705bf985de1f4612e4cf06e5/gpg-interface.c#L65 +var X509SignaturePrefix = []string{"-----BEGIN SIGNED MESSAGE-----"} + +// IsPGPSignature tests if the given signature is of type PGP. +// It returns true if the signature starts with the PGP signature prefix. +func IsPGPSignature(signature string) bool { + return slices.ContainsFunc(PGPSignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) +} + +// IsSSHSignature tests if the given signature is of type SSH. +// It returns true if the signature starts with the SSH signature prefix. +func IsSSHSignature(signature string) bool { + return slices.ContainsFunc(SSHSignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) +} + +// IsX509Signature tests if the given signature is of type x509. +// It returns true if the signature starts with the x509 signature prefix. +// This is a place holder / compatibility implementation to embed the signature +// type into the error message to inform the user about the wrong type of signature +func IsX509Signature(signature string) bool { + return slices.ContainsFunc(X509SignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) +} + +// IsEmptySignature tests if the given signature string is empty. +// It returns true if the signature string has a length of 0. +func IsEmptySignature(signature string) bool { + return len(signature) == 0 +} + +// GetSignatureType returns the type of the signature as a string. +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, +// "x509" for S/MIME signatures, "empty" for an empty signature +// and "unknown" for unrecognized signatures. +func GetSignatureType(signature string) string { + if IsPGPSignature(signature) { + return string(signatureTypePGP) + } + if IsSSHSignature(signature) { + return string(signatureTypeSSH) + } + if IsX509Signature(signature) { + return string(signatureTypeX509) + } + if IsEmptySignature(signature) { + return string(signatureTypeEmpty) + } + return string(signatureTypeUnknown) +} diff --git a/git/signature/signature_test.go b/git/signature/signature_test.go new file mode 100644 index 000000000..a2223bd77 --- /dev/null +++ b/git/signature/signature_test.go @@ -0,0 +1,269 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "testing" +) + +func TestIsPGPSignature(t *testing.T) { + tests := []struct { + name string + signature string + want bool + }{ + { + name: "valid PGP signature", + signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: true, + }, + { + name: "PGP signature with leading whitespace", + signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: true, + }, + { + name: "valid PGP signature", + signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + want: true, + }, + { + name: "PGP signature with leading whitespace", + signature: " -----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + want: true, + }, + { + name: "empty signature", + signature: "", + want: false, + }, + { + name: "SSH signature", + signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: false, + }, + { + name: "unknown signature", + signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + want: false, + }, + { + name: "whitespace only", + signature: " \n\t ", + want: false, + }, + { + name: "PGP SIGNATURE without END marker", + signature: "-----BEGIN PGP SIGNATURE-----", + want: true, + }, + { + name: "PGP MESSAGE without END marker", + signature: "-----BEGIN PGP MESSAGE-----", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsPGPSignature(tt.signature); got != tt.want { + t.Errorf("IsPGPSignature() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsSSHSignature(t *testing.T) { + tests := []struct { + name string + signature string + want bool + }{ + { + name: "valid SSH signature", + signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: true, + }, + { + name: "SSH signature with leading whitespace", + signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: true, + }, + { + name: "empty signature", + signature: "", + want: false, + }, + { + name: "PGP signature", + signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: false, + }, + { + name: "unknown signature", + signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + want: false, + }, + { + name: "whitespace only", + signature: " \n\t ", + want: false, + }, + { + name: "SSH signature without END marker", + signature: "-----BEGIN SSH SIGNATURE-----", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsSSHSignature(tt.signature); got != tt.want { + t.Errorf("IsSSHSignature() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsX509Signature(t *testing.T) { + tests := []struct { + name string + signature string + want bool + }{ + { + name: "valid x509 signature", + signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + want: true, + }, + { + name: "x509 signature with leading whitespace", + signature: " -----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + want: true, + }, + { + name: "empty signature", + signature: "", + want: false, + }, + { + name: "PGP signature", + signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: false, + }, + { + name: "SSH signature", + signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: false, + }, + { + name: "unknown signature", + signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + want: false, + }, + { + name: "whitespace only", + signature: " \n\t ", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsX509Signature(tt.signature); got != tt.want { + t.Errorf("IsX509Signature() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetSignatureType(t *testing.T) { + tests := []struct { + name string + signature string + want string + }{ + { + name: "PGP signature", + signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: string(signatureTypePGP), + }, + { + name: "PGP signature with leading whitespace", + signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + want: string(signatureTypePGP), + }, + { + name: "SSH signature", + signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: string(signatureTypeSSH), + }, + { + name: "SSH signature with leading whitespace", + signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + want: string(signatureTypeSSH), + }, + { + name: "x509 signature", + signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + want: string(signatureTypeX509), + }, + { + name: "x509 signature with leading whitespace", + signature: " -----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + want: string(signatureTypeX509), + }, + { + name: "empty signature", + signature: "", + want: string(signatureTypeEmpty), + }, + { + name: "unknown signature", + signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + want: string(signatureTypeUnknown), + }, + { + name: "whitespace only", + signature: " \n\t ", + want: string(signatureTypeUnknown), + }, + { + name: "PGP SIGNATURE without END marker", + signature: "-----BEGIN PGP SIGNATURE-----", + want: string(signatureTypePGP), + }, + { + name: "PGP MESSAGE without END marker", + signature: "-----BEGIN PGP MESSAGE-----", + want: string(signatureTypePGP), + }, + { + name: "SSH signature without END marker", + signature: "-----BEGIN SSH SIGNATURE-----", + want: string(signatureTypeSSH), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetSignatureType(tt.signature); got != tt.want { + t.Errorf("GetSignatureType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/git/signature/ssh_signature.go b/git/signature/ssh_signature.go new file mode 100644 index 000000000..11696d6b6 --- /dev/null +++ b/git/signature/ssh_signature.go @@ -0,0 +1,108 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature + +import ( + "bytes" + "fmt" + "strings" + + "github.com/hiddeco/sshsig" + gossh "golang.org/x/crypto/ssh" +) + +const SSHSignatureNamespace = "git" + +// SSHSignaturePrefix is the prefix used by Git to identify SSH signatures. +// https://github.com/git/git/blob/7b2bccb0d58d4f24705bf985de1f4612e4cf06e5/gpg-interface.c#L71 +var SSHSignaturePrefix = []string{"-----BEGIN SSH SIGNATURE-----"} + +// ParseAuthorizedKeys parses the given authorized keys string and returns +// a slice of public keys. It supports comments and empty lines. +func ParseAuthorizedKeys(authorizedKeys string) ([]gossh.PublicKey, error) { + var publicKeys []gossh.PublicKey + + for line := range strings.Lines(authorizedKeys) { + line = strings.TrimSpace(line) + + // Skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // Parse the authorized key line + pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(line)) + if err != nil { + return nil, fmt.Errorf("unable to parse authorized key: %w", err) + } + + publicKeys = append(publicKeys, pubKey) + } + + return publicKeys, nil +} + +// VerifySSHSignature verifies the SSH signature against the payload using +// the provided authorized keys. It returns the fingerprint of the key that +// successfully verified the signature, or an error. +func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...string) (string, error) { + if signature == "" { + return "", fmt.Errorf("unable to verify payload as the provided signature is empty") + } + + if len(payload) == 0 { + return "", fmt.Errorf("unable to verify payload as the provided payload is empty") + } + + if !IsSSHSignature(signature) { + return "", fmt.Errorf("unable to verify SSH signature, detected signature format: %s", GetSignatureType(signature)) + } + + // Unarmor the signature (remove PEM-like armor) + sig, err := sshsig.Unarmor([]byte(signature)) + if err != nil { + return "", fmt.Errorf("unable to unarmor SSH signature: %w", err) + } + + // record reading of authorized keys error. This error will be returned if no valid key was found. + var readAuthorizedKeysError error + + // Try to verify with each set of authorized keys + for _, keys := range authorizedKeys { + publicKeys, err := ParseAuthorizedKeys(keys) + if err != nil && readAuthorizedKeysError == nil { + readAuthorizedKeysError = fmt.Errorf("unable to parse authorized keys: %w", err) + continue + } + + // Try to verify with each public key + for _, pubKey := range publicKeys { + // Verify the signature using sshsig library + err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, sig.HashAlgorithm, SSHSignatureNamespace) + if err == nil { + // Signature verified successfully + return gossh.FingerprintSHA256(pubKey), nil + } + } + } + + if readAuthorizedKeysError != nil { + return "", readAuthorizedKeysError + } + + return "", fmt.Errorf("unable to verify payload with any of the given authorized keys") +} diff --git a/git/signature/ssh_signature_test.go b/git/signature/ssh_signature_test.go new file mode 100644 index 000000000..b61a19e0d --- /dev/null +++ b/git/signature/ssh_signature_test.go @@ -0,0 +1,641 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signature_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/fluxcd/pkg/git/internal/build" + "github.com/fluxcd/pkg/git/signature" + "github.com/fluxcd/pkg/git/testutils" + "github.com/go-git/go-git/v5/plumbing" + . "github.com/onsi/gomega" + gossh "golang.org/x/crypto/ssh" +) + +// these tests are in a different package to avoid circular dependencies with build.CommitWithRef and build.Tag + +func TestVerifySSHSignature(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + pubKeysAll, err := os.ReadFile(filepath.Join(testDataDir, "keys_all.pub")) + if err != nil { + t.Fatalf("Failed to read combined authorized keys: %v", err) + } + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + signedCommitFile string + signedTagFile string + pubKeyFile string + fingerPrintFile string + }{ + { + name: "ed25519 valid signature", + signedCommitFile: "commit_ed25519_signed.txt", + signedTagFile: "tag_ed25519_signed.txt", + pubKeyFile: "key_ed25519.pub", + fingerPrintFile: "key_ed25519.pub_fingerprint", + }, + { + name: "rsa valid signature", + signedCommitFile: "commit_rsa_signed.txt", + signedTagFile: "tag_rsa_signed.txt", + pubKeyFile: "key_rsa.pub", + fingerPrintFile: "key_rsa.pub_fingerprint", + }, + { + name: "ecdsa_p256 valid signature", + signedCommitFile: "commit_ecdsa_p256_signed.txt", + signedTagFile: "tag_ecdsa_p256_signed.txt", + pubKeyFile: "key_ecdsa_p256.pub", + fingerPrintFile: "key_ecdsa_p256.pub_fingerprint", + }, + { + name: "ecdsa_p384 valid signature", + signedCommitFile: "commit_ecdsa_p384_signed.txt", + signedTagFile: "tag_ecdsa_p384_signed.txt", + pubKeyFile: "key_ecdsa_p384.pub", + fingerPrintFile: "key_ecdsa_p384.pub_fingerprint", + }, + { + name: "ecdsa_p521 valid signature", + signedCommitFile: "commit_ecdsa_p521_signed.txt", + signedTagFile: "tag_ecdsa_p521_signed.txt", + pubKeyFile: "key_ecdsa_p521.pub", + fingerPrintFile: "key_ecdsa_p521.pub_fingerprint", + }, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + + // Parse the commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.signedCommitFile)) + if err != nil { + t.Fatalf("Failed to parse commit from fixture: %v", err) + } + + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + if err != nil { + t.Fatalf("Failed to build commit: %v", err) + } + + // Parse the commit from the fixture file + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.signedTagFile)) + if err != nil { + t.Fatalf("Failed to parse commit from fixture: %v", err) + } + + // Build a git.Commit using build.CommitWithRef + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + if err != nil { + t.Fatalf("Failed to build commit: %v", err) + } + + // Read the authorized keys + authorizedKey, err := os.ReadFile(filepath.Join(testDataDir, kt.pubKeyFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + expectedFingerprintBytes, err := os.ReadFile(filepath.Join(testDataDir, kt.fingerPrintFile)) + if err != nil { + t.Fatalf("Failed to read fingerprint file %s: %v", kt.fingerPrintFile, err) + } + expectedFingerprint := strings.TrimSpace(string(expectedFingerprintBytes)) + + // Verify the signature using the git.Commit's Signature and Encoded fields + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKey)) + if err != nil { + t.Errorf("Commit signature VerifySSHSignature() error = %v", err) + } + if fingerprint == "" { + t.Errorf("Commit signature VerifySSHSignature() returned empty fingerprint") + } + if fingerprint != expectedFingerprint { + t.Errorf("Commit signature VerifySSHSignature() fingerprint mismatch, got '%s', want '%s'", fingerprint, expectedFingerprint) + } + + // Verifying the correct fingerprint is returned from a list of public keys + fingerprint, err = signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(pubKeysAll)) + if err != nil { + t.Errorf("Commit signature VerifySSHSignature() error = %v", err) + } + if fingerprint == "" { + t.Errorf("Commit signature VerifySSHSignature() returned empty fingerprint") + } + if fingerprint != expectedFingerprint { + t.Errorf("Commit signature VerifySSHSignature() fingerprint mismatch, got '%s', want '%s'", fingerprint, expectedFingerprint) + } + + // Verify the signature using the git.Tag's Signature and Encoded fields + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKey)) + if err != nil { + t.Errorf("Tag signature VerifySSHSignature() error = %v", err) + } + if fingerprint == "" { + t.Errorf("Tag signature VerifySSHSignature() returned empty fingerprint") + } + if fingerprint != expectedFingerprint { + t.Errorf("Tag signature VerifySSHSignature() fingerprint mismatch, got '%s', want '%s'", fingerprint, expectedFingerprint) + } + + // Verifying the correct fingerprint is returned from a list of public keys + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(pubKeysAll)) + if err != nil { + t.Errorf("Tag signature VerifySSHSignature() error = %v", err) + } + if fingerprint == "" { + t.Errorf("Tag signature VerifySSHSignature() returned empty fingerprint") + } + if fingerprint != expectedFingerprint { + t.Errorf("Tag signature VerifySSHSignature() fingerprint mismatch, got '%s', want '%s'", fingerprint, expectedFingerprint) + } + + }) + } + + // Test error cases + t.Run("unsigned commit", func(t *testing.T) { + g := NewWithT(t) + + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) + g.Expect(err).ToNot(HaveOccurred()) + + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + g.Expect(err).ToNot(HaveOccurred()) + + pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_ed25519.pub")) + g.Expect(err).ToNot(HaveOccurred()) + + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(pubKey)) + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + }) + + t.Run("unsigned tag", func(t *testing.T) { + g := NewWithT(t) + + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_unsigned.txt")) + g.Expect(err).ToNot(HaveOccurred()) + + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + g.Expect(err).ToNot(HaveOccurred()) + + pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_ed25519.pub")) + g.Expect(err).ToNot(HaveOccurred()) + + fingerprint, err := signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(pubKey)) + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + }) +} + +func TestSSHSignatureValidationCases(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + keyType := "ed25519" + + pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_"+keyType+".pub")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + expectedFingerprintBytes, err := os.ReadFile(filepath.Join(testDataDir, "key_ed25519.pub_fingerprint")) + if err != nil { + t.Fatalf("Failed to read fingerprint file: %v", err) + } + expectedFingerprint := strings.TrimSpace(string(expectedFingerprintBytes)) + + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_"+keyType+"_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse commit from fixture: %v", err) + } + + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + if err != nil { + t.Fatalf("Failed to build commit: %v", err) + } + + const invalidAuthKeys = "invalid-key-data" + + tests := []struct { + name string + sig string + payload []byte + authorizedKeys []string + want string + wantErr string + }{ + { + name: "Empty signature", + sig: "", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to verify payload as the provided signature is empty", + }, + { + name: "Empty payload", + sig: gitCommit.Signature, + payload: []byte{}, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to verify payload as the provided payload is empty", + }, + { + name: "Wrong authorized keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEyM97VxLgOCuB9Eg5cDtTc8ogkdM1xAyJhzODB9cK1 wrong@example.com"}, + wantErr: "unable to parse authorized key", + }, + { + name: "Empty authorized keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{""}, + wantErr: "unable to verify payload with any of the given authorized keys", + }, + { + name: "Invalid signature", + sig: "-----BEGIN SSH SIGNATURE-----\n invalid\n -----END SSH SIGNATURE-----", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to unarmor SSH signature", + }, + { + name: "Non-SSH signature", + sig: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + payload: gitCommit.Encoded, + authorizedKeys: []string{""}, + wantErr: "unable to verify SSH signature, detected signature format: openpgp", + }, + { + name: "Invalid authorized keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{invalidAuthKeys}, + wantErr: "unable to parse authorized key", + }, + { + name: "Invalid keys followed by valid keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{invalidAuthKeys, string(pubKey)}, + want: expectedFingerprint, + }, + { + name: "Valid keys followed by invalid keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey), invalidAuthKeys}, + want: expectedFingerprint, + }, + { + name: "Multiple invalid authorized keys", + sig: gitCommit.Signature, + payload: gitCommit.Encoded, + authorizedKeys: []string{invalidAuthKeys, invalidAuthKeys}, + wantErr: "unable to parse authorized key", + }, + { + name: "Missing END SSH SIGNATURE marker", + sig: "-----BEGIN SSH SIGNATURE-----\nAAAA", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to unarmor SSH signature", + }, + { + name: "Empty body between markers", + sig: "-----BEGIN SSH SIGNATURE-----\n\n-----END SSH SIGNATURE-----", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to unarmor SSH signature", + }, + { + name: "Truncated signature mid-base64", + sig: "-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAABYAAAhlZDI1NTE5AAAAIM", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to unarmor SSH signature", + }, + { + name: "Corrupted base64 body", + sig: "-----BEGIN SSH SIGNATURE-----\n!!!!!!\n-----END SSH SIGNATURE-----", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + wantErr: "unable to unarmor SSH signature", + }, + { + name: "Extra data after END marker", + sig: gitCommit.Signature + "\ngarbage-data-after-end", + payload: gitCommit.Encoded, + authorizedKeys: []string{string(pubKey)}, + want: expectedFingerprint, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := signature.VerifySSHSignature(tt.sig, tt.payload, tt.authorizedKeys...) + if tt.wantErr != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) + g.Expect(got).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + +func TestParseAuthorizedKeysAndPublicFingerprint(t *testing.T) { + tests := []struct { + name string + authorizedKeys string + wantCount int + wantErr bool + wantFingerprints []string + }{ + { + name: "single key", + authorizedKeys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com", + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM"}, + }, + { + name: "key with additional directives", + authorizedKeys: "no-user-rc,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com additional long comment about nothing", + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM"}, + }, + { + name: "multiple keys", + authorizedKeys: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test1@example.com +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com`, + wantCount: 2, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM", "SHA256:oU8IT7UOnJlOTOvr/W1cYf1SkdocFm5F7SAXOwuo8Kc"}, + }, + { + name: "with comments", + authorizedKeys: `# This is a comment +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com +# Another comment`, + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:+vwrYGpHfAAWIzT2x+uV+duJG7ZnSvCbRKwdPApx7JA"}, + }, + { + name: "with empty lines", + authorizedKeys: `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com + +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCpreiO+8XsB4xXGNmwuO48a7WPghb5ihCJNPyQZpnaPfq6vhNVWSgq8AIjBmJOJYo4HZyiHqpS4OBc86glk6qMv8YHRt4VRVBP+DjPLDIsOR7+2HBlOPHMm8lTDi+iMPHBDxqFy7mSDB4+v7n700+49vYhWjZJpesnnE6JoitxSVhmqp75jeNRNU6PD00z+gMUcviv8UOs/Apg1Cw5f+4T9yOnjlOHaFH/ButvZ0t2VF0cs28tfCuLAoumjine5Gm6tCRQlZOoapNJzvnYT+86f/PEU/4kDYf3wT7S+NnUDfCsIpDVlOXPvjnQ/DudhqEnnXvfch+eBCI7rtJBHIGPKFdmC4cUROa0UDGR6o/JxLtx4ZTbkGpq6MVwdrb7qJ+Oib1U8xVimWFfarkm7deVXWD3wB5Wa8Ko/a/WuYfE3gYRhb8iXPYd71FsEy4F41JCMZDcIqMiQRe3e2gvY+z2sf02kHOFeWJmrAY9FFjPL85VD0Dg++jrExkGFjcBTw9gUG5OPGpwqQ9WHO8E8DPza+i5J/wu4DODyLrLxuXHPeSYUjcvh5ln8P70qL+Irwn1mgn2PkIZW0XCPBt6Iylg55t5sfyy03P0Kmb4U3TrppMeig7Lr9LDU4Doh7Fj6oLYDGFUV+F52SSuPs5SfrWd6Apiz+VPjsAh5btPPJNlzQ== test-rsa@example.com`, + wantCount: 2, + wantErr: false, + wantFingerprints: []string{"SHA256:3FcWgX5RsACruglrcBJP/hefUZcYHJGnrk07U6yKin8", "SHA256:TxoYgaeIj5A7Md4rHNfxPdqawooc4NIGjIMbcQ7YKbw"}, + }, + { + name: "empty", + authorizedKeys: "", + wantCount: 0, + wantErr: false, + wantFingerprints: []string{}, + }, + { + name: "invalid key", + authorizedKeys: "invalid-key-data", + wantCount: 0, + wantErr: true, + wantFingerprints: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keys, err := signature.ParseAuthorizedKeys(tt.authorizedKeys) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + // Validate expected fingerprint if specified + if len(tt.wantFingerprints) > 0 && len(keys) > 0 { + for _, key := range keys { + found := false + fingerprint := gossh.FingerprintSHA256(key) + for _, wantedFingerprint := range tt.wantFingerprints { + if fingerprint == wantedFingerprint { + found = true + } + } + if !found { + t.Errorf("ParseAuthorizedKeys() fingerprint '%s'not in list of wanted fingerprints %s", fingerprint, tt.wantFingerprints) + } + } + } + }) + } +} + +func TestParseAuthorizedKeysFromFixtures(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + fingerprintFile string + wantCount int + wantErr bool + }{ + { + name: "ed25519 key", + fixture: "key_ed25519.pub", + fingerprintFile: "key_ed25519.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "rsa key", + fixture: "key_rsa.pub", + fingerprintFile: "key_rsa.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p256 key", + fixture: "key_ecdsa_p256.pub", + fingerprintFile: "key_ecdsa_p256.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p384 key", + fixture: "key_ecdsa_p384.pub", + fingerprintFile: "key_ecdsa_p384.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p521 key", + fixture: "key_ecdsa_p521.pub", + fingerprintFile: "key_ecdsa_p521.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "all key types combined", + fixture: "keys_all.pub", + wantCount: 5, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.fixture)) + if err != nil { + t.Fatalf("Failed to read fixture file %s: %v", tt.fixture, err) + } + + keys, err := signature.ParseAuthorizedKeys(string(authorizedKeys)) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + + // Read expected fingerprint from file if provided + var expectedFingerprint string + if tt.fingerprintFile != "" { + fingerprintData, err := os.ReadFile(filepath.Join(testDataDir, tt.fingerprintFile)) + if err != nil { + t.Fatalf("Failed to read fingerprint file %s: %v", tt.fingerprintFile, err) + } + expectedFingerprint = strings.TrimSpace(string(fingerprintData)) + } + + // Verify that each key has a valid fingerprint + for i, key := range keys { + fingerprint := gossh.FingerprintSHA256(key) + if fingerprint == "" { + t.Errorf("Key %d has empty fingerprint", i) + } + if !strings.HasPrefix(fingerprint, "SHA256:") { + t.Errorf("Key %d fingerprint %s does not have SHA256: prefix", i, fingerprint) + } + // Validate fingerprint against the one read from file + if expectedFingerprint != "" { + if fingerprint != expectedFingerprint { + t.Errorf("Key %d got fingerprint %s, want %s (from %s)", i, fingerprint, expectedFingerprint, tt.fingerprintFile) + } + } + } + }) + } +} + +func TestParseAuthorizedKeysCombinations(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixtures []string + wantCount int + wantErr bool + }{ + { + name: "ed25519 + rsa", + fixtures: []string{"key_ed25519.pub", "key_rsa.pub"}, + wantCount: 2, + wantErr: false, + }, + { + name: "ed25519 + ecdsa p256", + fixtures: []string{"key_ed25519.pub", "key_ecdsa_p256.pub"}, + wantCount: 2, + wantErr: false, + }, + { + name: "rsa + ecdsa p384 + ecdsa p521", + fixtures: []string{"key_rsa.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 3, + wantErr: false, + }, + { + name: "all ecdsa variants", + fixtures: []string{"key_ecdsa_p256.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 3, + wantErr: false, + }, + { + name: "ed25519 + rsa + all ecdsa", + fixtures: []string{"key_ed25519.pub", "key_rsa.pub", "key_ecdsa_p256.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 5, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var combinedKeys strings.Builder + for _, fixture := range tt.fixtures { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, fixture)) + if err != nil { + t.Fatalf("Failed to read fixture file %s: %v", fixture, err) + } + combinedKeys.Write(authorizedKeys) + combinedKeys.WriteString("\n") + } + + keys, err := signature.ParseAuthorizedKeys(combinedKeys.String()) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + + // Verify that each key has a valid fingerprint + for i, key := range keys { + fingerprint := gossh.FingerprintSHA256(key) + if fingerprint == "" { + t.Errorf("Key %d has empty fingerprint", i) + } + if !strings.HasPrefix(fingerprint, "SHA256:") { + t.Errorf("Key %d fingerprint %s does not have SHA256: prefix", i, fingerprint) + } + } + }) + } +} diff --git a/git/signature/testdata/gpg_signatures/README.md b/git/signature/testdata/gpg_signatures/README.md new file mode 100644 index 000000000..1b18c5d7c --- /dev/null +++ b/git/signature/testdata/gpg_signatures/README.md @@ -0,0 +1,354 @@ +# GPG Signature Test Fixtures + +This directory contains test fixtures for GPG signature validation. + +## Quick Start + +To generate all test fixtures at once, simply run: + +```bash +./generate_gpg_fixtures.sh +``` + +This script will automatically create all GPG keys, signed commits, and signed tags. + +## How to Generate Test Fixtures + +### Using the Automated Script + +The [`generate_gpg_fixtures.sh`](generate_gpg_fixtures.sh) script automates the entire process of creating GPG signature test fixtures. It generates: + +1. **GPG Key Pairs** in supported variants: + - RSA (2048 and 4096 bits) + - ECC/ECDSA (NIST P-256, P-384, P-521) + - Brainpool curves (P-256, P-384, P-512) + - EdDSA (Ed25519) + + **Note:** currently it is not possible to use gnupg generate ed448 keys to validate signed commits with backends which implemented RFC9580: https://github.com/ProtonMail/go-crypto/issues/300 + +2. **Public Keys**: + - Individual public key files for each key type + +3. **Signed Git Commits**: + - One signed commit for each key type + - All commits are verified using `git verify-commit` + +4. **Signed Git Tags**: + - One signed tag for each key type + - All tags are verified using `git verify-tag` + +5. **Unsigned Objects**: + - One unsigned commit and one unsigned tag for testing negative cases + +### Manual Generation + +If you need to generate test fixtures manually, follow these steps: + +#### 1. Generate GPG Key Pairs + +```bash +# Set up a temporary GPG home directory +export GNUPGHOME=$(mktemp -d) +mkdir -p "$GNUPGHOME" +chmod 700 "$GNUPGHOME" + +# Configure GPG for batch mode +echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf" +echo "no-tty" >> "$GNUPGHOME/gpg.conf" + +# RSA 2048-bit key +cat > batch_rsa_2048.txt < batch_rsa_4096.txt < batch_ecdsa_p256.txt < batch_ecdsa_p384.txt < batch_ecdsa_p521.txt < batch_brainpool_p256.txt < batch_brainpool_p384.txt < batch_brainpool_p512.txt < batch_ed25519.txt < key_rsa_2048.pub +gpg --armor --export test-rsa-4096@example.com > key_rsa_4096.pub +gpg --armor --export test-ecdsa-p256@example.com > key_ecdsa_p256.pub +gpg --armor --export test-ecdsa-p384@example.com > key_ecdsa_p384.pub +gpg --armor --export test-ecdsa-p521@example.com > key_ecdsa_p521.pub +gpg --armor --export test-brainpool-p256@example.com > key_brainpool_p256.pub +gpg --armor --export test-brainpool-p384@example.com > key_brainpool_p384.pub +gpg --armor --export test-brainpool-p512@example.com > key_brainpool_p512.pub +gpg --armor --export test-ed25519@example.com > key_ed25519.pub +``` + +#### 2. Create a Test Git Repository + +```bash +mkdir test_repo && cd test_repo +git init +echo "test content" > test.txt +git add test.txt +git commit -m "Test commit" +git config user.name "Test User" +git config user.email "sign-user@example.com" +git config gpg.program gpg + +# Get the key ID for the key you want to use +KEY_ID=$(gpg --list-keys --with-colons test-ed25519@example.com | grep '^fpr' | head -1 | cut -d: -f10) +git config user.signingkey "$KEY_ID" +``` + +#### 3. Sign a Commit with GPG + +```bash +# Sign the last commit +git commit --amend --allow-empty -S -m "Test commit signed with ed25519" + +# Verify the signed commit +git verify-commit HEAD +``` + +#### 4. Export the Signed Commit + +```bash +# Get the commit object +git cat-file commit HEAD > commit_ed25519_signed.txt +``` + +#### 5. Create a Tag and Sign It + +```bash +git tag -a test-tag -m "Test tag" -s +git verify-tag test-tag +git cat-file tag test-tag > tag_ed25519_signed.txt +``` + +## File Format + +The signed Git objects follow the standard Git object format with GPG signatures: + +### Signed Commit Format + +``` +tree +parent +author +committer +gpgsig -----BEGIN PGP SIGNATURE----- + + -----END PGP SIGNATURE----- + + +``` + +### Signed Tag Format + +``` +object +type commit +tag +tagger + + +-----BEGIN PGP SIGNATURE----- + + -----END PGP SIGNATURE----- +``` + +## Generated Files + +The script generates the following files: + +### Public Keys +- `key_rsa_2048.pub` - RSA 2048-bit public key +- `key_rsa_4096.pub` - RSA 4096-bit public key +- `key_ecdsa_p256.pub` - ECDSA P-256 public key +- `key_ecdsa_p384.pub` - ECDSA P-384 public key +- `key_ecdsa_p521.pub` - ECDSA P-521 public key +- `key_brainpool_p256.pub` - Brainpool P-256 public key +- `key_brainpool_p384.pub` - Brainpool P-384 public key +- `key_brainpool_p512.pub` - Brainpool P-512 public key +- `key_ed25519.pub` - Ed25519 public key + +### Signed Commits +- `commit_rsa_2048_signed.txt` - RSA 2048-bit signed commit +- `commit_rsa_4096_signed.txt` - RSA 4096-bit signed commit +- `commit_ecdsa_p256_signed.txt` - ECDSA P-256 signed commit +- `commit_ecdsa_p384_signed.txt` - ECDSA P-384 signed commit +- `commit_ecdsa_p521_signed.txt` - ECDSA P-521 signed commit +- `commit_brainpool_p256_signed.txt` - Brainpool P-256 signed commit +- `commit_brainpool_p384_signed.txt` - Brainpool P-384 signed commit +- `commit_brainpool_p512_signed.txt` - Brainpool P-512 signed commit +- `commit_ed25519_signed.txt` - Ed25519 signed commit + +### Signed Tags +- `tag_rsa_2048_signed.txt` - RSA 2048-bit signed tag +- `tag_rsa_4096_signed.txt` - RSA 4096-bit signed tag +- `tag_ecdsa_p256_signed.txt` - ECDSA P-256 signed tag +- `tag_ecdsa_p384_signed.txt` - ECDSA P-384 signed tag +- `tag_ecdsa_p521_signed.txt` - ECDSA P-521 signed tag +- `tag_brainpool_p256_signed.txt` - Brainpool P-256 signed tag +- `tag_brainpool_p384_signed.txt` - Brainpool P-384 signed tag +- `tag_brainpool_p512_signed.txt` - Brainpool P-512 signed tag +- `tag_ed25519_signed.txt` - Ed25519 signed tag + +### Unsigned Objects +- `commit_unsigned.txt` - Unsigned commit for testing negative cases +- `tag_unsigned.txt` - Unsigned tag for testing negative cases + +## Key Types Explained + +### RSA (Rivest-Shamir-Adleman) +- **RSA 2048**: Standard RSA key with 2048-bit modulus +- **RSA 4096**: Stronger RSA key with 4096-bit modulus +- Widely supported, but slower than ECC keys + +### ECDSA (Elliptic Curve Digital Signature Algorithm) +- **P-256**: NIST P-256 curve (secp256r1) +- **P-384**: NIST P-384 curve (secp384r1) +- **P-521**: NIST P-521 curve (secp521r1) +- Efficient and secure, widely supported + +### Brainpool Curves +- **P-256**: brainpoolP256r1 curve +- **P-384**: brainpoolP384r1 curve +- **P-512**: brainpoolP512r1 curve +- Alternative to NIST curves with different security properties + +### EdDSA (Edwards-curve Digital Signature Algorithm) +- **Ed25519**: Modern, fast, and secure curve +- Recommended for new applications + +## Security Note + +These test fixtures use generated test keys and should NOT be used in production. The keys are created without passphrases for testing purposes only. + +## Requirements + +- GnuPG (gpg) version 2.0 or higher +- Git with GPG support +- Bash shell + +## Troubleshooting + +### GPG version compatibility + +```bash +gpg --version +``` + +### Key generation failures +The script uses `set -euo pipefail` and will abort on any error. + +If key generation fails, ensure that: +1. You have sufficient entropy on your system +2. The GPG home directory has proper permissions (700) +3. No other GPG agents are interfering +4. Your GPG version supports the requested key type + +### Script structure +The script uses separate functions for different operations: +- `generate_key()` - For generating key pairs (RSA, ECDSA, EdDSA) with type-specific parameters +- `create_signed_object()` - For creating signed commits and tags +- `create_unsigned_commit_and_tag()` - For creating unsigned test commits and tags + +### Signature verification failures +If signature verification fails, ensure that: +1. The public key is properly imported +2. The GPG trust database is configured correctly +3. The signature was created with the corresponding private key \ No newline at end of file diff --git a/git/signature/testdata/gpg_signatures/commit_brainpool_p256_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p256_signed.txt new file mode 100644 index 000000000..d20861042 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_brainpool_p256_signed.txt @@ -0,0 +1,13 @@ +tree 1673f4226b68c3c29e8d038052698fd10706eb7e +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iJEEABMIADkWIQRlBCAxO+6ynqGKIEdbY0atdy3seQUCagyJ+hsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQW2NGrXct7HkyogD+NzBPXbA/WmTV3S3OJw2v + uCJlsozEIdBemRPlud/bdqcA/2VXBWBx/GNDVERCz+CoIgk1r+UYNHPY2sKC4Rof + XZ/b + =iLlU + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p256 diff --git a/git/signature/testdata/gpg_signatures/commit_brainpool_p384_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p384_signed.txt new file mode 100644 index 000000000..3c2b2a8ed --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_brainpool_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff5f115ae071fc5b5984c3cf8a2e14fb86e54596 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iLEEABMJADkWIQTNs8DDx+7VeXH5/sB3IHg7nCO39gUCagyJ+hsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQdyB4O5wjt/Z2rgGAheub5m+E+SDusCBJWvEo + kjuBFeZuzB94m8gjMCH3dSxiC7LtSsobbf99tcf4YYH6AX4th44TivpvLO9rh54C + qf0BiV7Exf+3i/rXhwC2eHCmA/SFZvcbpMwtmhgg7kswhpk= + =PNo6 + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p384 diff --git a/git/signature/testdata/gpg_signatures/commit_brainpool_p512_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p512_signed.txt new file mode 100644 index 000000000..7073c90e4 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_brainpool_p512_signed.txt @@ -0,0 +1,14 @@ +tree a9ac3b19ae895b654fadecbf65d68b6b904e9015 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iNEEABMKADkWIQTohLjZUdUvESasnEsiUQykOmaHcQUCagyJ+xsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQIlEMpDpmh3GorgH+Ipb5VC7CTo1ytrctn0NR + 77xD4xAwp+ut+PwkT2RMoxGqbEkwrW1Y1wGQAKPGnijSfUupISIgzdLJUShKDciz + JgH/US1NCBLnOuMdMdoc3AIqflJ8xu0pdf+0/XWOFGidJ6oiNI3QOxmAYjFvCee0 + TKi0AQTiCNokBpkV+cwe2GHEeg== + =QAlW + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p512 diff --git a/git/signature/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt new file mode 100644 index 000000000..7b19e0f0b --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt @@ -0,0 +1,13 @@ +tree 2f0fa5393a2120151c5446eb34b99d1f3713ff12 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iJEEABMIADkWIQSra878nXqQhGlG0J5198+uMUSa1AUCagyJ+xsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQdffPrjFEmtQU6gD+IPI8ltvbuafpIpKVLLyY + W9SMNKzfjyqYdUrJ07qpzJMA/3IPL05fc18C0cPrAxZG+Z/aa5ETXKuVSyIpozCB + Ux8o + =N0QF + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p256 diff --git a/git/signature/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt new file mode 100644 index 000000000..7d95e1f30 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff58328bd5797f45f6f300c6c39d2cd357b9f3cd +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iLEEABMJADkWIQQcZZ6vAXQZm7YZRrpR4RR22DRNPgUCagyJ+xsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQUeEUdtg0TT7kuQF/TMAeDhAWLFbTSBGV3Jvt + /3QfkM1xJxLYX67kxAV4E6kjIrytC9RGI60YpFhjghcmAYC/wkJ+0qEz35dMhHSs + rN7V40lItKU0Avjd0cjhW8S1h5e6OGQNRQ9UhqmNZ62YAjg= + =6DV/ + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p384 diff --git a/git/signature/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt new file mode 100644 index 000000000..d08b9616b --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt @@ -0,0 +1,14 @@ +tree 63af4f62a108a6c684181a4488b4bd3a5b51dc8e +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iNQEABMKADkWIQTKslrsgiOj48BflFB3P0KJyycQKwUCagyJ/BsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQdz9CicsnECsOcAIJAV0t6Pyt/4lxZwD9q7Lz + gQG3qhoCyXQM2rS52WhPjfIUR9yZMzavbvr0dPJyRJcAc90qY8mCFUqKt+xycjX7 + YNQPAgiiPlew3hXRt+bLf1yEwIWHSN8Gal/Vxu/Fff8xQya7CRMAxwc10SZtdkkd + 4Lyff1c/HX1n9OpUb/2lRjCBp3ZTng== + =z0F4 + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p521 diff --git a/git/signature/testdata/gpg_signatures/commit_ed25519_signed.txt b/git/signature/testdata/gpg_signatures/commit_ed25519_signed.txt new file mode 100644 index 000000000..df98a84b1 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_ed25519_signed.txt @@ -0,0 +1,13 @@ +tree 7c5bd8f246ab8e8c6a5749c3d2f44018aa029fb8 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iJEEABYKADkWIQRxLmKjJPnrrVipKXm4uLZD9k5L5AUCagyJ/BsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMiwwLDMACgkQuLi2Q/ZOS+TAiAD/bpWyWxFULhqWJ3zDrPGk + bWl8Cu5MNGW/ovAhBi8zmQ0A/3LoqL701wdC8hbvQUJjh39N9LAOIjxIpO+K6p9C + +bMM + =1Lp5 + -----END PGP SIGNATURE----- + +Test commit signed with ed25519 diff --git a/git/signature/testdata/gpg_signatures/commit_rsa_2048_signed.txt b/git/signature/testdata/gpg_signatures/commit_rsa_2048_signed.txt new file mode 100644 index 000000000..d1205f66d --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_rsa_2048_signed.txt @@ -0,0 +1,17 @@ +tree e3ca2325bfa8013dca224a2f62f0582d70c07b12 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iQFPBAABCAA5FiEE6CiVtmexoZsKIW5tW10ZTs+Zr8QFAmoMifwbFIAAAAAABAAO + bWFudTIsMi41KzEuMTIsMCwzAAoJEFtdGU7Pma/ELqEH/0/nA3Cdf8zSi/Ut5aOV + gDZx4LJc4O8B/Uxp0lTOuh1OeG22tRxz89moj10e9G2SiwJPlxZ1VAYsociBDmEN + gqJ9qxSY3cJgUk5GpI1mYgESa4QBrxF5vrLIjfqux2f2jJAp4KV3ec720cGmF8zL + KdOzX/RjjgsRwxzCtOEZ8Yf9BWagRdtyghhupyObMai29W2fvNcdA483bMRPuiQ1 + sTLNaH0RhevedFGKj/sSA8adJvWPS27rokyvmIVpbkA10qpC7MHw0tbu1P721vuw + ZzymK0kpjUK3QxIlPhRZ35gQtybcr3pbYcrmUBldTkRnqunLqqkT3+zPQSi7ywfb + CoQ= + =DRTU + -----END PGP SIGNATURE----- + +Test commit signed with rsa_2048 diff --git a/git/signature/testdata/gpg_signatures/commit_rsa_4096_signed.txt b/git/signature/testdata/gpg_signatures/commit_rsa_4096_signed.txt new file mode 100644 index 000000000..ce0a55cde --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_rsa_4096_signed.txt @@ -0,0 +1,22 @@ +tree 596e4c43898dcf2a6aa08cb9c0f3e0bbb8ecc26d +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN PGP SIGNATURE----- + + iQJPBAABCAA5FiEE3uXEv0PUIgHzrTdgH/mFHFWyCW4FAmoMifwbFIAAAAAABAAO + bWFudTIsMi41KzEuMTIsMCwzAAoJEB/5hRxVsglu1hUP/RI2L1LGtK8zocDCRbwz + YlO656I8YLNWETVDLcYCJPH0uEDVQIeNbZv+9+4Ehh2F/u6gzANX/zJTTxw9gzTh + q9m8h3e2l8MfOTu/p8RuzGExXEMR+0txFRXcmrUy9tljdTZqSXZSTPTntWC74RXJ + 2IQtMLLEM5VARf/5Oi5htzoYBO+zp6iqmSbcQ2BnzlxbBxmNpfBnOMiSgF1itrX3 + dOK0Uf82iugbWBjhC8v6qXC94xKKXjZHuGwbYFRRNWoH0nEWFsjIlcKUpjSZBGfC + 6FC0tTQuOsnbt9+WJFfi88V24/NKZg6UWFbBoTDX3mG1eIUwT1/k4j9KJaapriSo + L5/zNXso/mnSOuo3H5uYUXIRt2oNsyc/wuxgtapJv33w/5O6Jp956NHpR6QZ/3H6 + tJnGmt55IUcTlT/wLD1cggQoCCDngeiaYach4L2+DQfL5WuMPx871MFkAPXgzmm0 + QZBc6Au12NDRzvG964ieAm/TMoZjX0+FGZT27f5oJZPwiq+CmigeZhT+c4jyhiJD + AeiHkCK1v+/h5GFhbWZlv9lFmQ9VJnkIFmLH+Tm21L375UCgnQ8qCPlUB9nJMxi5 + zgLZA0bhhFHBbTmP8rzkYX0eYU5YE1sA5PHjnN6twfwMIcBe/8A0UhG7x009yHzf + +3HA1c++VHcyBEiGkARQaHOW + =8b6C + -----END PGP SIGNATURE----- + +Test commit signed with rsa_4096 diff --git a/git/signature/testdata/gpg_signatures/commit_unsigned.txt b/git/signature/testdata/gpg_signatures/commit_unsigned.txt new file mode 100644 index 000000000..39c5b0a71 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/commit_unsigned.txt @@ -0,0 +1,5 @@ +tree 4650a2cda631bc795fc254fe20b598135b265036 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 + +Test commit unsigned diff --git a/git/signature/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signature/testdata/gpg_signatures/generate_gpg_fixtures.sh new file mode 100755 index 000000000..2cafb3a77 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -0,0 +1,293 @@ +#!/usr/bin/env bash +# generate_gpg_fixtures.sh - Script to generate GPG signature test fixtures +# Generates GPG keys in all variants and signed Git objects + +set -euo pipefail + +# Configuration variables +TEST_USER_NAME="Test User" +TEST_USER_EMAIL="sign-user@example.com" +FIXTURE_DATE="2026-01-01T00:00:00+0000" + +# Isolate Git from user and system configuration for deterministic output +export TZ=UTC +export GIT_AUTHOR_DATE="$FIXTURE_DATE" +export GIT_COMMITTER_DATE="$FIXTURE_DATE" +export GIT_AUTHOR_NAME="$TEST_USER_NAME" +export GIT_AUTHOR_EMAIL="$TEST_USER_EMAIL" +export GIT_COMMITTER_NAME="$TEST_USER_NAME" +export GIT_COMMITTER_EMAIL="$TEST_USER_EMAIL" +export GIT_CONFIG_NOSYSTEM=1 +export GIT_CONFIG_GLOBAL=/dev/null + +# Directory for temporary files +TEMP_DIR=$(mktemp -d) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# define script dependencies +DEPENDENCY=( + gpg + cat + grep + head + cut + awk + git + find + sort +) + +echo "=== GPG Signature Test Fixtures Generator ===" +echo "Temporary directory: $TEMP_DIR" +echo "Output directory: $SCRIPT_DIR" +echo "" + +# GPG home directory for test keys +export GNUPGHOME="$TEMP_DIR/gnupg" +mkdir -p "$GNUPGHOME" +chmod 700 "$GNUPGHOME" + +# Configure GPG for batch mode (no interaction) +echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf" +echo "no-tty" >> "$GNUPGHOME/gpg.conf" + + +# cleanup on exit +cleanup() { + if [[ -d "${TEMP_DIR}" ]]; then + echo "=== Cleanup ===" + rm -rf "${TEMP_DIR}" + echo "Temporary directory removed" + fi +} + +# check necessary commands +check_dependencies() { + local exit_state=0 + + # check presence of dependencies + for COMMAND in "${DEPENDENCY[@]}"; do + if ! command -v "${COMMAND}" >/dev/null 2>&1; then + echo "command '${COMMAND}' not found, needs to be installed first." + exit_state=1 + fi + done + + if [[ ${exit_state} -ne 0 ]]; then + exit 1 + fi +} + +# Function to generate GPG key pair +generate_key() { + local key_type=$1 + local key_param=$2 + local key_name=$3 + + echo "Generating $key_type key pair ($key_name)..." + + # Create batch configuration for GPG + local batch_file="$TEMP_DIR/batch_${key_name}.txt" + cat > "$batch_file" <> "$batch_file" + ;; + ecdsa|eddsa) + echo "Key-Curve: $key_param" >> "$batch_file" + ;; + esac + + cat >> "$batch_file" <&1 + + # Get the key ID + local key_id + key_id=$(gpg --list-keys --with-colons "test-${key_name}@example.com" | grep '^fpr' | head -1 | cut -d: -f10) + + echo " Key ID: $key_id" + + # Export public key + gpg --armor --export "test-${key_name}@example.com" > "$SCRIPT_DIR/key_${key_name}.pub" + echo " ✓ key_${key_name}.pub created" + + # Export secret key (for signing) + gpg --armor --export-secret-keys "test-${key_name}@example.com" > "$TEMP_DIR/${key_name}.sec" + + # Store key ID for later use + echo "$key_id" > "$TEMP_DIR/${key_name}_id.txt" + + rm -f "$batch_file" + echo " ✓ $key_name key pair generated successfully" +} + +# Function to create signed Git objects (commits and tags) +create_signed_object() { + local object_type=$1 + local key_name=$2 + + echo "Creating signed $object_type for $key_name..." + + # Get key ID + local key_id + key_id=$(cat "$TEMP_DIR/${key_name}_id.txt") + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_${key_name}_${object_type}" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init -b main + git config gpg.program gpg + git config user.signingkey "$key_id" + + # Import the secret key for signing + gpg --batch --import "$TEMP_DIR/${key_name}.sec" 2>/dev/null + + # Create file and commit + echo "Test content for $key_name $object_type" > test.txt + git add test.txt + git commit -m "Test commit for $object_type" + + if [[ "$object_type" == "commit" ]]; then + # Sign the commit (amend) + git commit --amend --allow-empty -S -m "Test commit signed with $key_name" + + # Verify the signed commit + echo " Verifying signed commit..." + git verify-commit HEAD 2>&1 | grep -q "Good signature" + echo " ✓ Commit signature verified successfully" + + # Export commit object + git cat-file commit HEAD > "$SCRIPT_DIR/commit_${key_name}_signed.txt" + cd "$SCRIPT_DIR" + echo " ✓ commit_${key_name}_signed.txt created" + + elif [[ "$object_type" == "tag" ]]; then + # Create and sign tag + git tag -a "test-tag-${key_name}" -m "Test tag signed with $key_name" -s + + # Verify the signed tag + echo " Verifying signed tag..." + git verify-tag "test-tag-${key_name}" 2>&1 | grep -q "Good signature" + echo " ✓ Tag signature verified successfully" + + # Export tag object + git cat-file tag "test-tag-${key_name}" > "$SCRIPT_DIR/tag_${key_name}_signed.txt" + cd "$SCRIPT_DIR" + echo " ✓ tag_${key_name}_signed.txt created" + fi +} + +# Function to create unsigned commit and tag +create_unsigned_commit_and_tag() { + echo "Creating unsigned commit and tag..." + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_unsigned" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init -b main + + # Create file and commit (without signature) + echo "Test content unsigned" > test.txt + git add test.txt + git commit -m "Test commit unsigned" + + # Export commit object + git cat-file commit HEAD > "$SCRIPT_DIR/commit_unsigned.txt" + + # Create and export tag object + git tag -a test-tag -m "Test tag" + git cat-file tag test-tag > "$SCRIPT_DIR/tag_unsigned.txt" + + cd "$SCRIPT_DIR" + echo " ✓ commit_unsigned.txt and tag_unsigned.txt created" +} + +# Main program +main() { + + check_dependencies + + echo "Step 1: Generate RSA keys..." + echo "-----------------------------------" + + # RSA keys (different key lengths) + generate_key "RSA" "2048" "rsa_2048" + generate_key "RSA" "4096" "rsa_4096" + + echo "" + echo "Step 2: Generate ECC keys..." + echo "-----------------------------------" + + # ECDSA keys (different curves) + generate_key "ecdsa" "NIST P-256" "ecdsa_p256" + generate_key "ecdsa" "NIST P-384" "ecdsa_p384" + generate_key "ecdsa" "NIST P-521" "ecdsa_p521" + + # Brainpool curves + generate_key "ecdsa" "brainpoolP256r1" "brainpool_p256" + generate_key "ecdsa" "brainpoolP384r1" "brainpool_p384" + generate_key "ecdsa" "brainpoolP512r1" "brainpool_p512" + + # Ed25519 (modern elliptic curve) + generate_key "eddsa" "Ed25519" "ed25519" + + echo "" + echo "Step 3: Create signed commits..." + echo "----------------------------------------" + + # Get list of successfully generated keys + local keys=() key_name="" + for key_file in "$TEMP_DIR"/*_id.txt; do + if [[ -f "$key_file" ]]; then + key_name=$(basename "$key_file" "_id.txt") + keys+=("$key_name") + fi + done + + # Signed commits for each key type + for key_name in "${keys[@]}"; do + create_signed_object "commit" "$key_name" + done + + echo "" + echo "Step 4: Create signed tags..." + echo "-------------------------------------" + + # Signed tags for each key type + for key_name in "${keys[@]}"; do + create_signed_object "tag" "$key_name" + done + + echo "" + echo "Step 5: Create unsigned commit and tag..." + echo "------------------------------------------" + + create_unsigned_commit_and_tag + + echo "" + echo "=== Done! ===" + echo "All test fixtures have been successfully created." + echo "" + echo "Created files:" + find "$SCRIPT_DIR" -maxdepth 1 \( -name "*.txt" -o -name "key_*.pub" \) -exec ls -lh {} \; 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' | sort +} + +trap cleanup EXIT + +main diff --git a/git/signature/testdata/gpg_signatures/key_brainpool_p256.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p256.pub new file mode 100644 index 000000000..cc85e3551 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_brainpool_p256.pub @@ -0,0 +1,11 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mFMEagyJ+RMJKyQDAwIIAQEHAgMEBDv7xtgM0Kzx/hrjFpjlxxtpbFZzKwVot6Jr +Ih1AowMmTjsFN4PFRjmudBk7mnhpF5XEKfU1hLzyMBFbdenCILQrVGVzdCBVc2Vy +IDx0ZXN0LWJyYWlucG9vbF9wMjU2QGV4YW1wbGUuY29tPoivBBMTCABXFiEEZQQg +MTvusp6hiiBHW2NGrXct7HkFAmoMifkbFIAAAAAABAAObWFudTIsMi41KzEuMTIs +MCwzAhsjBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEFtjRq13Lex5w38B +AJuxG9dsTYS3r+EWw3bOHffETiBWOJBalRW1qqn/IVcmAQChIfOdK3x+GMJH+SAr +y0Le1NA7qRwshq4LLS6MiGNQew== +=agvF +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_brainpool_p384.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p384.pub new file mode 100644 index 000000000..8fc2a5416 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_brainpool_p384.pub @@ -0,0 +1,12 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mHMEagyJ+RMJKyQDAwIIAQELAwMELCn65vIdEylxvlKOE1G8KoJydJX8W+5acDIa +yyp4n8ZKi3rwjvwpV9ODhUh2gyV6LEwtkGxR/E2o4RXp3/T8d7xsiwbL/NvJXRlJ +K+wHE9odh08LK/4Z5pEUqh1BEhRHtCtUZXN0IFVzZXIgPHRlc3QtYnJhaW5wb29s +X3AzODRAZXhhbXBsZS5jb20+iM8EExMJAFcWIQTNs8DDx+7VeXH5/sB3IHg7nCO3 +9gUCagyJ+RsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMCGyMFCwkIBwICIgIG +FQoJCAsCBBYCAwECHgcCF4AACgkQdyB4O5wjt/bdfwF/RJdf5fIuRiuoeyydOT1I +qCzc9Ylo8JYOPSXZR4AzXpC4mh3h+ZafxryXV2b3eA6FAX9wdF1JAbyfUXOPT6SB +TB4oM8KvnFeVXLAvXSG4sGobZedZs3JnrQHoKRcnG1+HXa8= +=gri4 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_brainpool_p512.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p512.pub new file mode 100644 index 000000000..3fc68d69b --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_brainpool_p512.pub @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mJMEagyJ+hMJKyQDAwIIAQENBAMEkWej27PAoUlUqKxnqUg5lEeN9I4o/mtCQxv5 +JWvaeKmqkCCh6x/XPyAeUnW+6ylIOpmd4md0S4nbYump7ygPFDz3fYfxZi44NW8A +6aa4UTF2EVhuFixlMVLp010sBBGsnvZ1bnHAhfsCuWjXzvm1E4ZdE7e9AwADYBoY +wqnXHbu0K1Rlc3QgVXNlciA8dGVzdC1icmFpbnBvb2xfcDUxMkBleGFtcGxlLmNv +bT6I7wQTEwoAVxYhBOiEuNlR1S8RJqycSyJRDKQ6ZodxBQJqDIn6GxSAAAAAAAQA +Dm1hbnUyLDIuNSsxLjEyLDAsMwIbIwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIX +gAAKCRAiUQykOmaHcRAuAf0edXQmsyx3lJpT5TwkIa6dOv8Fpjvr2vF2u7i1mwUi +JREXFCsmEPUbz9QNR1ro2obMv/JdGERQByXqrKi8q0CHAf9ZZJ0Uqsv/NTOXOzxL +4Qbbj6vEZkygfJEHpFjBTwtdsd1Q0Sxf16OZX5AdT8nPFDs1uOS7HsJRMC9kV56L +0i7A +=a1cK +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_ecdsa_p256.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p256.pub new file mode 100644 index 000000000..125c82f8a --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_ecdsa_p256.pub @@ -0,0 +1,11 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mFIEagyJ+RMIKoZIzj0DAQcCAwSirbJtjX2fkmtUrg6ppTBNGnM2og6ZV5DSoFwl +8ZGi/jFwbG46q3EERvg17MN/UhD37OWgUHLhqyUKkKoOy6u1tCdUZXN0IFVzZXIg +PHRlc3QtZWNkc2FfcDI1NkBleGFtcGxlLmNvbT6IrwQTEwgAVxYhBKtrzvydepCE +aUbQnnX3z64xRJrUBQJqDIn5GxSAAAAAAAQADm1hbnUyLDIuNSsxLjEyLDAsMwIb +IwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRB198+uMUSa1Dj5AP0dtVFL +fCgq8Rm5aMM2Yd/vBOo2ZVuZukG5iu2uCtVQJwD+JK1aQeX7VZ/I/Y+B10viqjMt +XKqc+bVMwVjrZ9NO6qg= +=vwKL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_ecdsa_p384.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p384.pub new file mode 100644 index 000000000..d4390f08e --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_ecdsa_p384.pub @@ -0,0 +1,12 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mG8EagyJ+RMFK4EEACIDAwTiS6oidH72jJFYn6OK9llncdrmEIbgZz+s0lOPOIAZ +n0Ycc6cBKQH2+UyZwB6BONJPPNTCgncAiApazIMEjjkyJ0tmen0BeckOjeLh567a +vj97aZiZVeIKlcowMVC6nHS0J1Rlc3QgVXNlciA8dGVzdC1lY2RzYV9wMzg0QGV4 +YW1wbGUuY29tPojPBBMTCQBXFiEEHGWerwF0GZu2GUa6UeEUdtg0TT4FAmoMifkb +FIAAAAAABAAObWFudTIsMi41KzEuMTIsMCwzAhsjBQsJCAcCAiICBhUKCQgLAgQW +AgMBAh4HAheAAAoJEFHhFHbYNE0++iwBgI2bwBgFgqvQ88qlo4oWW4yALRLo4I36 +9LHQVC/pugRw+h4hmDW+g0qwFkLF3g1mewGAu9nDtAXQScbYxu/du3d7/AHVpQkn +CsOMB3cJhLhOk/ZrbHmBxEB3Km4qulzuxPaT +=f5RW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_ecdsa_p521.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p521.pub new file mode 100644 index 000000000..5f3edfd26 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_ecdsa_p521.pub @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mJMEagyJ+RMFK4EEACMEIwQAPPYI5Mm2Jsdx5Gictjq/dIRTYjPBNBebG4gkkZLO +ZESgIVXhDHAYGonQBCgIX9xZJ5lipz9oZa54kryRewLYonoANRPhOkATQvJr84WX +qdgn4B7c1iRIXF9uNtQMhpBY68pRAojXX6pFkfei5cdUY3VVtAjUKkLn3g+BsR5t +5l+cRcG0J1Rlc3QgVXNlciA8dGVzdC1lY2RzYV9wNTIxQGV4YW1wbGUuY29tPojz +BBMTCgBXFiEEyrJa7IIjo+PAX5RQdz9CicsnECsFAmoMifkbFIAAAAAABAAObWFu +dTIsMi41KzEuMTIsMCwzAhsjBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ +EHc/QonLJxArE00CCQFO4rZYHnD7ITL/8bd9Uh9ys6aRqW+0hgjyTR1Ks/QRG8uX +bzqyxui6FEuGbVYg3w0oJ3Jdu7LtwUl1w6oOZkbWQQIJAemBhUDcSPsfcFtqEjnN +1d2sdorFHlG67setPAMuIyFfrQ98Go5n0N0i/XEA/UQVHWUrqC+pwiZ9N7RZBLsi ++LPv +=+upk +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_ed25519.pub b/git/signature/testdata/gpg_signatures/key_ed25519.pub new file mode 100644 index 000000000..d9a49eccf --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_ed25519.pub @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEagyJ+hYJKwYBBAHaRw8BAQdADqQpCJt4Rp3sE87GpTrTRn/VWxyzTHvxW0w3 +HyxUzPi0JFRlc3QgVXNlciA8dGVzdC1lZDI1NTE5QGV4YW1wbGUuY29tPoivBBMW +CgBXFiEEcS5ioyT5661YqSl5uLi2Q/ZOS+QFAmoMifobFIAAAAAABAAObWFudTIs +Mi41KzEuMTIsMCwzAhsjBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELi4 +tkP2TkvkWCABAJUgD27HKFU/TGCr1060EeA6FKy63dhUz16tueOibgxsAQDaykTr +J9s5fQDoy6Us7dP5UfrwuPxqpAAyTBrkuIJ4CQ== +=8gTZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_rsa_2048.pub b/git/signature/testdata/gpg_signatures/key_rsa_2048.pub new file mode 100644 index 000000000..3d58baff6 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_rsa_2048.pub @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGoMifYBCAC5Y/Kwd0lmZMuKBLVzIA6yig2fMnZ63P90ZtfCxAqkqxus7TwC +RgIS8xwLvJLCU7T2fBr2AqK3nRzI7N6zeQf9mehlwzFwr56UTgeZpLGFPePQDORL +3AQbFvdhVYGeQHN+tC5rf1nJoR1ln5SyrGXsozqsRtYeRSxEbGAuZr4AZ5tbR7HV +mR9tYyUxtiXOnjpoMtCVtk5BpJaT6op6G1EENsAH4wQceQ8jbqfY20qrbsPRIIn8 +pesbwkwVSfV446Sa5vzCSsEOeqJV4GKHjQoQXpDURvXEZyYaFZri6RjUWb6eTrQZ +QW62muJP/T5fbN6ckEU9igQv8HAOn+9yo5DRABEBAAG0JVRlc3QgVXNlciA8dGVz +dC1yc2FfMjA0OEBleGFtcGxlLmNvbT6JAW4EEwEIAFgWIQToKJW2Z7Ghmwohbm1b +XRlOz5mvxAUCagyJ9hsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMDGy8EBQsJ +CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEFtdGU7Pma/EBIkH/A89adLp7eZp +iHSaGuTm8fjtbEokzQg2v2/g2RVH6iSrCJRxZib4KC7o+6JuQY4A2Wtb8prYkRQG +ccmx2o+QfcSCnCFX7EYxgbbmoU38vZk3avrWEXotUlV/KiPoAhe3/9ZIP7xcQvDT +6Q9wW+WY06Yl5s8WL3wXeb9KcLySX9SABc6GeTzGaj/rzU6o+NNis7K9nfRZf1cK +z/xOuiClpWZlcsWTLp0sxLzZe3CBGMyx0gnLo84vO5Gih0jf+rqDtZxZiZ6p3Jf5 ++5mPkywP+9vxzBlPJskXCp634YUokHfQi//PfpHQseA5K8fsa3WIELFgCv0BWY1m +ddpNkEZDUBI= +=yDvj +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/key_rsa_4096.pub b/git/signature/testdata/gpg_signatures/key_rsa_4096.pub new file mode 100644 index 000000000..61c4a78a3 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/key_rsa_4096.pub @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGoMifYBEAC0k2xUqcCl3DVYUpqGFQqIkEDVGK/ayXAIPCWuguoo4ZR9qAVp +b3TEN/lCRwlp141bXlAVUN1P9bDNf7M4ib910KFlAbNR6e+Lnu9wkDH+ulSjnU3l +lV+Wkzem/pCddAPx6t9nDXC/CQL1DreIuZ5jFOolfrbVFnXXphkJV2dZODD+bWOW +/gqp7v1P3VR+JqQ4Tm7KVhsMcyK2jfb4pLCss+X188++5Oa9jmGOIywVVREjhhUh +vmhQ0y9xIUFBvUdOo63kFLiUJiekppirlg7dViuheYO1+gKZn6kBN1SUO4L1tJC3 +zGrxN5fkMY5YgxKB7dmjHwEqdAROy42uAw3yIiNj5sPtGNyV92zRYTlW7rKznNXg +fieB8/2sW8LkQeuZaJYAKQSgya3ZOutGKaEA+9iBeY1/lx1Y+VaeNtX8zsKjO+gV +zbWfl0ZZhuZLlGeFPzg4DrjExvyyTDuHKfPi7jhzIcd3is4y7qDwohmdiRs57kL/ +0d1oe6B7aAAA/X43r8AQwE7FjqZV2rp4GW/bf3bjVugM4iK3SWMJVL24Jtt1KlCb +O4XAfeYXxsQWKWSUI7e0XDsMBnBQ44qL6+rLd24jLjbdRpq7FlX4GFrytUJyKQAF +fOcpaidz1nyKV3Td73I3eEGzOsKK2LG75l8enBaQ6j2eUVeeKXJw06VTkQARAQAB +tCVUZXN0IFVzZXIgPHRlc3QtcnNhXzQwOTZAZXhhbXBsZS5jb20+iQJuBBMBCABY +FiEE3uXEv0PUIgHzrTdgH/mFHFWyCW4FAmoMifYbFIAAAAAABAAObWFudTIsMi41 +KzEuMTIsMCwzAxsvBAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRAf+YUc +VbIJbnlAD/0X6Cub3kbkqTmUyYxPU7mF9CiB56Bz9PztJdRu1sB8RyBy5IlTcc5y +Fj4UU34bhp7AHF+GQaqe3Jn9YaaA6Co+Ly24cZAi8lsbQHlUay/YayCe/zWbAehu +S49NmthqOpM9pJDbyZBQOGTGWtSptfLp9s15KlAuBDdxRyjrAoAcRAIJkYu86n8x +/AuAkJ0QECJrkehgtKmjz4o8+4+UophjeV9V5zIvHv+uo5aQV/yE3vH6yVgNsSMZ +AdAhA5zoe79wv7naXjudvKCGugP+lUlWvYX0vmZr/twz+TmEHMhJgNfVgVtviNBv +0b4WJHykpqAwKlBWBMqWVdFB9EN0m7oXM3BnWKmhK6pz8U86WqclyTF7nqCz92OY +NAsF7eRoYGgisbQLyosiyP40Q3c0crR6zLgcD1LhtTJ3cV6y5ksw6wpbeTEPrlmj +dd6ryvGwyMKSET/CPxSeKYf46M2qYvxKn4BNUIeUSstql6Y3bcLYYgx2wj/Y4Uq/ +st8/QzD4nkneXUiqphiOazq5USR0LZHFBUiHlKI7UbGUHsvUQtEmlrGoq6BmI9FS +GhoBUr3gbxWPMtVbFOMYrCmVYRfX/KITKtl0S29yWGXpDWnDF0u4sIIFopWObD8E +b4GwOO35FtrGuKRjFqlpDYbtxihkOdKQNgT/KucTQOc9EV03QpSABQ== +=b6dx +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signature/testdata/gpg_signatures/tag_brainpool_p256_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p256_signed.txt new file mode 100644 index 000000000..67e641db7 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_brainpool_p256_signed.txt @@ -0,0 +1,14 @@ +object 060daaa406f6124f553a000db3e5b0a7f65005f5 +type commit +tag test-tag-brainpool_p256 +tagger Test User 1767225600 +0000 + +Test tag signed with brainpool_p256 +-----BEGIN PGP SIGNATURE----- + +iJEEABMIADkWIQRlBCAxO+6ynqGKIEdbY0atdy3seQUCagyJ/RsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQW2NGrXct7HkA6AD+O/esPf4KJss/Civ2/VzZ +KmepNCursKGEzhDlSI2xCb0A/j2J/Y6l7J2+gWMkTOTPgMhFMb2omVABnzOgjQdm +E8FI +=gHvh +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_brainpool_p384_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p384_signed.txt new file mode 100644 index 000000000..e1af5f38b --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_brainpool_p384_signed.txt @@ -0,0 +1,14 @@ +object c8bf2fedd064e636aee6747ea95092449db07e03 +type commit +tag test-tag-brainpool_p384 +tagger Test User 1767225600 +0000 + +Test tag signed with brainpool_p384 +-----BEGIN PGP SIGNATURE----- + +iLEEABMJADkWIQTNs8DDx+7VeXH5/sB3IHg7nCO39gUCagyJ/RsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQdyB4O5wjt/YVlgF/XCVC7XZeLzfCse1G/HkO +GxPLeH/dGysWShFQB8GcdBw6LMrZvimdkhp+OLNJx5SLAYCDRQBNrrUS6yrAeyzx +C2uJJ3hF/Dao3ZUsFojeLhsMV//0N3jB8wvk/lJFOeLY8Qw= +=1X+b +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_brainpool_p512_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p512_signed.txt new file mode 100644 index 000000000..d9465a1e8 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_brainpool_p512_signed.txt @@ -0,0 +1,15 @@ +object 684a723a5eb868b452ac576da351f38f9cd8a49b +type commit +tag test-tag-brainpool_p512 +tagger Test User 1767225600 +0000 + +Test tag signed with brainpool_p512 +-----BEGIN PGP SIGNATURE----- + +iNEEABMKADkWIQTohLjZUdUvESasnEsiUQykOmaHcQUCagyJ/RsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQIlEMpDpmh3E53wH/cyvuxAcYIk79TI7/3VNd +bsHyOEoj7sVSO/8DpzbOf9GJIACa8dzLxmbVaK//x5KOYFK6y4JOb8FyA31JKbWH +MAH+KFIdHIil6XUTRFQCmVLR+pB7i5mBLhW5SXcekFiFX95PG7tnVFGp971l+ugO +QulvbRjXMhYldg3R8Q4E5lZL6Q== +=f770 +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt new file mode 100644 index 000000000..b0f46a93d --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt @@ -0,0 +1,14 @@ +object 22d258d46f5bc50420db6a5e4c8d60f35a78fa8d +type commit +tag test-tag-ecdsa_p256 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p256 +-----BEGIN PGP SIGNATURE----- + +iJEEABMIADkWIQSra878nXqQhGlG0J5198+uMUSa1AUCagyJ/hsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQdffPrjFEmtQp7QD5ARj4NkkgkNvNPYYj+f1s +y1eiGEhv78qT9hAMVL8OHsUBAN8/NVpUkxZaZ///tJybwJW54TF//wse7ADe0kgG +eIoe +=N1G6 +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt new file mode 100644 index 000000000..49629c870 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt @@ -0,0 +1,14 @@ +object 35f5804b751475a1856b7efc13ccba249bdcd32d +type commit +tag test-tag-ecdsa_p384 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p384 +-----BEGIN PGP SIGNATURE----- + +iLEEABMJADkWIQQcZZ6vAXQZm7YZRrpR4RR22DRNPgUCagyJ/hsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQUeEUdtg0TT7axQGAoDxgow0h/roUSeBuvsj4 +2iEzJX7jAviKSBCGHVXqm/V/fp/keO+gioGwZZe3dEWTAX9HdFCOB7Zn/Q73Xg/p +0poDDj5UtSd0Z2BaPuQHCQY489qLfJukDLsPY1Mr3VUKZzs= +=Ay2z +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt new file mode 100644 index 000000000..89dc91e03 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt @@ -0,0 +1,15 @@ +object 18011cecb9ae833a29b75526e801e8c0b7951c3f +type commit +tag test-tag-ecdsa_p521 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p521 +-----BEGIN PGP SIGNATURE----- + +iNQEABMKADkWIQTKslrsgiOj48BflFB3P0KJyycQKwUCagyJ/hsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQdz9CicsnECtH5AIIgfzJtOJnaAFV0kYZIQ8l +wiyWXZx5l1VL5TbYfa53dKWrMf9KUQhmDa4TNzXSLzDJBnEI63idJCrNnhB2ajSY +5ugCCQFX8d2MPDrkqbuTgikUA2L4nyPriCT8cn1a3gaUlSCme1YL65tNpsYTPYwk +SFUiXk2/W++2+Du0/kx791BWX9I8eg== +=C+FE +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_ed25519_signed.txt b/git/signature/testdata/gpg_signatures/tag_ed25519_signed.txt new file mode 100644 index 000000000..657b002df --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_ed25519_signed.txt @@ -0,0 +1,14 @@ +object b01ca16de562c561f62427eb0a30cb775d1c1dab +type commit +tag test-tag-ed25519 +tagger Test User 1767225600 +0000 + +Test tag signed with ed25519 +-----BEGIN PGP SIGNATURE----- + +iJEEABYKADkWIQRxLmKjJPnrrVipKXm4uLZD9k5L5AUCagyJ/hsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQuLi2Q/ZOS+SDJQEAqqdgow+zK3rCLSmkd1ms +GFgpHWgOorNAD0by5LI5Hj4BAJev0pBf61utMzpZT6B3/1sYqgqh9uOdifFAn8h+ +q14J +=Hps8 +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_rsa_2048_signed.txt b/git/signature/testdata/gpg_signatures/tag_rsa_2048_signed.txt new file mode 100644 index 000000000..05232707c --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_rsa_2048_signed.txt @@ -0,0 +1,18 @@ +object adb09c411509ca74c29235aca9336e64b52bd29d +type commit +tag test-tag-rsa_2048 +tagger Test User 1767225600 +0000 + +Test tag signed with rsa_2048 +-----BEGIN PGP SIGNATURE----- + +iQFPBAABCAA5FiEE6CiVtmexoZsKIW5tW10ZTs+Zr8QFAmoMif8bFIAAAAAABAAO +bWFudTIsMi41KzEuMTIsMCwzAAoJEFtdGU7Pma/E6hoIAJeEOuA12XMoC2kTWwpH +LGguh068/mlABcs+b9Ck+gp490SvocX9kC4/0ew1JSo4w/YO1vGJNgmR+snkWFQr +wPi+IU5DjLXWPG09Yl2pCfYItUAAlGxddoqZy9WuqdP3KNU1Ntihv/F8ceKFhXJ8 +RHAylpQ5oUSvwnnwdeZAIRDFuj7gQC9RjPAApzIw/bfIVxlNDeNdNxkQeCd+pWw6 +4LdUTdCk4DnGciwXEjXOkFMWm1P/k4q3nxYa53cXdzTA2w+vx5MSQpqcgnn+bR+4 +ynfK1lkucN6sINgytspASBaZCkUAqbG4zbsnVkC/GH8hp6xBOsfFUyRlZ2O1U+TT +kqc= +=dpy5 +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_rsa_4096_signed.txt b/git/signature/testdata/gpg_signatures/tag_rsa_4096_signed.txt new file mode 100644 index 000000000..2b8261e06 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_rsa_4096_signed.txt @@ -0,0 +1,23 @@ +object 4d0c7c2e471d3320c4d415c390ae6bf3778720b0 +type commit +tag test-tag-rsa_4096 +tagger Test User 1767225600 +0000 + +Test tag signed with rsa_4096 +-----BEGIN PGP SIGNATURE----- + +iQJPBAABCAA5FiEE3uXEv0PUIgHzrTdgH/mFHFWyCW4FAmoMif8bFIAAAAAABAAO +bWFudTIsMi41KzEuMTIsMCwzAAoJEB/5hRxVsgluKQkP/0Nd3f2XpkEQyfHXGpVY +4w+vc/WkAE5x5vDuCLHlxWZD1XCQGMahHvzKhR5xE7CE9MLXSAsWOK/FpvGG3Sld +PxMHN1KHayEdRnQutZ7loBGNaYqTXwnr8XReZRfiAK06RAeBGoxppif4hR0eeIU6 ++5/dCafoXjm1eYn8di+bqQDGkWImS7dliIBKpj0iamzw5IpWWHfw3BQ9tTcwR/fz +w6bat/NDUoCrfFNCHJYeiZHZRR8Lx9NaJYwIgWeL4AdDNzWeKMSMpmuWNMg9AcGG +mzWlIITRJuSyHg/BjyGAIrAv5wPriNFo84TX4K19a1wh8t8agL/czT8pLY1WG845 +kKgF6gPmGAlMboSTQrUBhF1K1GdtcHaIun6F5FAcEaULfkBrwB6//Lyb0JpWh7+C +tb7Ab+n2neNjUi8YrABzAMv3eUnsvD/VaRYiQ2djKnZlgZcMiGmPWyZDXTQ+jnKL +HUEnZ3NuAckogKagtkPCyYXFuGb5Y0lOS4wuTV5TFD9ns0VTNnO8jAc1JrU5GayE +SMFfXQrNoVhWc0xW4WH8sRvWp9hNaLshFwOAnySdf40RFy4aEIDstpx9IXhrO8kb +1CxpQYZy/tlVL/yoMcHCYxMlRsZPv3pp2w6YYuPkwb9tpVBE3YKuNsuzMHXt7xSj +3bRjjd9ILAw7KaytvmF9+uyp +=JzFg +-----END PGP SIGNATURE----- diff --git a/git/signature/testdata/gpg_signatures/tag_unsigned.txt b/git/signature/testdata/gpg_signatures/tag_unsigned.txt new file mode 100644 index 000000000..ec80cabd7 --- /dev/null +++ b/git/signature/testdata/gpg_signatures/tag_unsigned.txt @@ -0,0 +1,6 @@ +object f85d47148d57e658056d9859377ec47ff17d0e98 +type commit +tag test-tag +tagger Test User 1767225600 +0000 + +Test tag diff --git a/git/signature/testdata/ssh_signatures/README.md b/git/signature/testdata/ssh_signatures/README.md new file mode 100644 index 000000000..a902eb198 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/README.md @@ -0,0 +1,192 @@ +# SSH Signature Test Fixtures + +This directory contains test fixtures for SSH signature validation. + +## Quick Start + +To generate all test fixtures at once, simply run: + +```bash +./generate_ssh_fixtures.sh +``` + +This script will automatically create all SSH keys, public key files, fingerprint files, signed commits, and signed tags. + +## How to Generate Test Fixtures + +### Using the Automated Script + +The [`generate_ssh_fixtures.sh`](generate_ssh_fixtures.sh) script automates the entire process of creating SSH signature test fixtures. It generates: + +1. **SSH Key Pairs** in all variants: + - RSA (4096 bits) + - ECDSA (p256, p384, p521) + - ED25519 + +2. **Public Keys**: + - Individual public key files for each key type + - Fingerprint files for each public key + - Combined file with all public keys + +3. **Signed Git Commits**: + - One signed commit for each key type + - All commits are verified using `git verify-commit` + +4. **Signed Git Tags**: + - One signed tag for each key type + - All tags are verified using `git verify-tag` + +5. **Unsigned Objects**: + - One unsigned commit and one unsigned tag for testing negative cases + +### Manual Generation + +If you need to generate test fixtures manually, follow these steps: + +#### 1. Generate SSH Key Pairs + +```bash +# RSA key +ssh-keygen -t rsa -b 4096 -f test_rsa -N "" +mv test_rsa.pub key_rsa.pub + +# ECDSA keys (all variants) +ssh-keygen -t ecdsa -b 256 -f test_ecdsa_p256 -N "" +mv test_ecdsa_p256.pub key_ecdsa_p256.pub + +ssh-keygen -t ecdsa -b 384 -f test_ecdsa_p384 -N "" +mv test_ecdsa_p384.pub key_ecdsa_p384.pub + +ssh-keygen -t ecdsa -b 521 -f test_ecdsa_p521 -N "" +mv test_ecdsa_p521.pub key_ecdsa_p521.pub + +# ED25519 key +ssh-keygen -t ed25519 -f test_ed25519 -N "" +mv test_ed25519.pub key_ed25519.pub +``` + +#### 2. Create Verified Signers File + +```bash +# Create verified signers file with git namespace +echo "$(git config --get user.email) namespaces=\"git\" $(cat key_ed25519.pub)" > verified_signers_ed25519 +``` + +#### 3. Create a Test Git Repository + +```bash +mkdir test_repo && cd test_repo +git init +echo "test content" > test.txt +git add test.txt +git commit -m "Test commit" +git config user.name "Test User" +git config user.email "sign-user@example.com" +git config gpg.format ssh +git config user.signingkey ../key_ed25519.pub +git config gpg.ssh.allowedSignersFile ../verified_signers_ed25519 +``` + +#### 4. Sign a Commit with SSH + +```bash +# Sign the last commit +git commit --amend --allow-empty -S -m "Test commit signed with ed25519" + +# Verify the signed commit +git verify-commit HEAD +``` + +#### 5. Export the Signed Commit + +```bash +# Get the commit object +git cat-file commit HEAD > commit_ed25519_signed.txt +``` + +#### 6. Create a Tag and Sign It + +```bash +git tag -a test-tag -m "Test tag" -s +git verify-tag test-tag +git cat-file tag test-tag > tag_ed25519_signed.txt +``` + +## File Format + +The signed Git objects follow the standard Git object format with SSH signatures: + +### Signed Commit Format + +``` +tree +parent +author +committer +gpgsig -----BEGIN SSH SIGNATURE----- + + -----END SSH SIGNATURE----- + + +``` + +### Signed Tag Format + +``` +object +type commit +tag +tagger + + +-----BEGIN SSH SIGNATURE----- + +-----END SSH SIGNATURE----- +``` + +### Verified Signers Format + +``` + namespaces="git" +``` + +## Generated Files + +The script generates the following files: + +### Public Keys +- `key_rsa.pub` - RSA 4096-bit public key +- `key_ecdsa_p256.pub` - ECDSA P-256 public key +- `key_ecdsa_p384.pub` - ECDSA P-384 public key +- `key_ecdsa_p521.pub` - ECDSA P-521 public key +- `key_ed25519.pub` - ED25519 public key +- `keys_all.pub` - All public keys combined + +### Public Key Fingerprints +- `key_rsa.pub_fingerprint` - SHA256 fingerprint of RSA public key +- `key_ecdsa_p256.pub_fingerprint` - SHA256 fingerprint of ECDSA P-256 public key +- `key_ecdsa_p384.pub_fingerprint` - SHA256 fingerprint of ECDSA P-384 public key +- `key_ecdsa_p521.pub_fingerprint` - SHA256 fingerprint of ECDSA P-521 public key +- `key_ed25519.pub_fingerprint` - SHA256 fingerprint of ED25519 public key + +### Signed Commits +- `commit_rsa_signed.txt` - RSA-signed commit +- `commit_ecdsa_p256_signed.txt` - ECDSA P-256 signed commit +- `commit_ecdsa_p384_signed.txt` - ECDSA P-384 signed commit +- `commit_ecdsa_p521_signed.txt` - ECDSA P-521 signed commit +- `commit_ed25519_signed.txt` - ED25519 signed commit + +### Signed Tags +- `tag_rsa_signed.txt` - RSA-signed tag +- `tag_ecdsa_p256_signed.txt` - ECDSA P-256 signed tag +- `tag_ecdsa_p384_signed.txt` - ECDSA P-384 signed tag +- `tag_ecdsa_p521_signed.txt` - ECDSA P-521 signed tag +- `tag_ed25519_signed.txt` - ED25519 signed tag + +### Unsigned Objects +- `commit_unsigned.txt` - Unsigned commit for testing negative cases +- `tag_unsigned.txt` - Unsigned tag for testing negative cases + +## Security Note + +These test fixtures use generated test keys and should NOT be used in production. \ No newline at end of file diff --git a/git/signature/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt new file mode 100644 index 000000000..372f1518b --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt @@ -0,0 +1,12 @@ +tree 2f0fa5393a2120151c5446eb34b99d1f3713ff12 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE + EE+VmrN/R3AA+VBW+s/32EXhHSFZrQMO42zR4BTGQau9KJWpSuqMs4s1yjl2JsMUJgabtU + GrokE3v2OPHptca7QAAAAANnaXQAAAAAAAAABnNoYTUxMgAAAGQAAAATZWNkc2Etc2hhMi + 1uaXN0cDI1NgAAAEkAAAAhAPeodf77MdlZNmkYbLeIDTwiQwdx/mS2NTuapK0RAKv7AAAA + IBtzmPK1xUu7BALEfqRfhScJ1glf+xcvziyxZ0NaWY9E + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p256 diff --git a/git/signature/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt new file mode 100644 index 000000000..aa09e20e1 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff58328bd5797f45f6f300c6c39d2cd357b9f3cd +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG + EEpeCgMWsTpFYqb4OTBJ7L8W8k597NKpUtDBLHTfzb68owpNlRHsmH06BMS4VtvytNITuw + KGUUEPhU0ya4O8wLPuIZrUQYDiFhOTAYDAA0qb/fjESo1TN79X1PhTrfVU9yAAAAA2dpdA + AAAAAAAAAGc2hhNTEyAAAAhQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAagAAADEA2xJu + CgHho+KGRdfVJBSfr1oSGlcKizC16MsuhA1hscqSFVfeErbsZ59j0Id3Qg64AAAAMQC2SO + +pJcX7Hlyg7dg2KpfBaFSxUWrbVfC/Mdy8vSSROxf8weJKw1gP/s9SCmNzxLs= + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p384 diff --git a/git/signature/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt new file mode 100644 index 000000000..4420a4db7 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt @@ -0,0 +1,15 @@ +tree 63af4f62a108a6c684181a4488b4bd3a5b51dc8e +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAKwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAI + UEAHvvwuT/Fa+NvAjwAG/1dWxWflOpz6m6IQ0lALlGwFu5LsK/DGezWNeiWdkfmcA/58Yw + Fov/0Qc3QIB9h5GpPGRUAZR5H028WhpUU5WV82dYFZDKVFovDie80eqWtJ1/v8Cim5QchX + KJI3zvEwi+LNzg9ghTi0rUdMTbf2ZHeGPIWZjUAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAAA + pQAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAAigAAAEEROu/iLh3XrEXwBNbdLGVShUkipC + adRTF8PUh5lWKiT3yjZPPTH7RvJo5FRAzN/GDFlMqDcRFxoG2GeOIEHut7zAAAAEEvqaFh + 8WylK+yvxjFx6qIE2atS+oT2Fbd6rfSJkeNIJrO5bO2ik6SL1KZ+NAJvzpspJEi9zhMVLH + heD4jqP7n49w== + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p521 diff --git a/git/signature/testdata/ssh_signatures/commit_ed25519_signed.txt b/git/signature/testdata/ssh_signatures/commit_ed25519_signed.txt new file mode 100644 index 000000000..a5d830a58 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_ed25519_signed.txt @@ -0,0 +1,11 @@ +tree 7c5bd8f246ab8e8c6a5749c3d2f44018aa029fb8 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgrYSEhPKV/65kzG2JLYU+586anT + AORbbZ0UW9qzon28EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQD72vwusTGiRbH8ZtPm53vl065Ocv6Sp6VbHq4mkONAM0mzDLrD7BmAgWkjtmL2JpK + msqgFJcKs6Z3E1zH86fQ0= + -----END SSH SIGNATURE----- + +Test commit signed with ed25519 diff --git a/git/signature/testdata/ssh_signatures/commit_rsa_signed.txt b/git/signature/testdata/ssh_signatures/commit_rsa_signed.txt new file mode 100644 index 000000000..873881da8 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_rsa_signed.txt @@ -0,0 +1,29 @@ +tree 1207106d0fef65cd05d7a8428fc871886a36fa78 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAM/nZRSKToZ1F5uTTRUDNo + MM7VWH75aoyE+RphS7k29LHe3qVGobBXQIzlU8TM9nkjw1MairofggcPymTi041VBVSr1g + gmCz0Cq9ONgVtppJ+IiqUs3Gi3gLJzwgSeyRIP1dmhm02NsueusVx04Q5EZptHFtYxRev0 + 7FokYi2kY7voYIIY3sWnLFqvbqwztaDDLobQjzbGWYrKtm4J+vMlAzA24cSzYWQOKy7kzk + JdTltsE3188inIPMNjwMsF+aDxMg0plkuUl7bB/abItAkAHEmihcvI3wVN4+7gx89Aeixb + 61o4zs1qES2d7hFk6F0wbQDVYl+su6S7fD6ldkPnfQhc+HrfADH5vPg8yszfyJH5Tc51XY + LB+TQvRyazB/Y7GFnsTEjz3GoVJ1L7cobNa2XIItSeLw2pBTlKKmxOhBXAQ4W1y0Exgb8/ + J9jVCM9iG5ervrRKwgdQERC8wsouELGQ0X/202ZHJuWE7oHWE6feNl+b2sIdBzbNku8eI9 + xYkADzcY9liB6jsMCwYSov37l8I1ZTRMBqRNGS9iUftMFrHFOCVKMZhtcdOVih6JRUoCRJ + 98BWNl06Ee6+f3KsCtEKLZK0Zd6e54dny3NHAZ+LpcUSR6OShvSlFpVM2bLcKgrgcUZuSE + kAu4feUowiTuTO0MA5IscYuVAOWkig5bAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAACFAAAAA + xyc2Etc2hhMi01MTIAAAIAuF/iEiesuFacqW/3f/6ejF9b/HvvMa8mlZ/xiDYFH/bE1PSg + Pfs+ntjfGQ/YZ3bk5w6jQ1yx/A1OySH8eJCVb14UcKnJj1/NuNmUHdffIGTCuiT8wpSnuE + KVHPrtrRYZQ69QUcjnfpYsn4uG32UZgIzJSgczaB91mTOYkSIrHKRTS5vNq462DdAqXjc3 + 30PF8wCPNKWYuxSISHMuzZtJchIK9EZ278Ac/kj8AsXQmEJ2kHdFtriPYNLjDKq3WTdge+ + uSrGpGBIeuHu/CS7fOEqkSAoyjIPqe7R/U2DxTLZWUBxKRp/ozUqYW+fui5AWXXAzWcWsp + /Fs+JHbPQZh5P79lUZYwxZQIawy8hRmOb088rVhxXlNLF+u5Rc2bSgtj1WgWu5AML8IWwu + w8Uvlprit/Fb3CeNp55wTxu8j/u3lQGUh7dySsdRXExCDUCFQDiOV4pJA4AUxNdJ6urZDP + 4K0vJd0J8YFPoC0V1ugXlaB4Ya+qrsZzjBJFIK2oHBwDkkOTcVynRGqcbMu2PBMWJbGaVE + jTc5P8+zktNhacUa1fwwWlFi9Lcs/k8N7D4nNFiCJn1qjvDik/aOTPP3ek5sYZcinmssPj + TVGDVuHo8fXQmH+y1ppQbkkJ2KwoWnV0A8Zpd75f2WhBCS+R7bXnRi78AT/KPQi5rkV5+5 + vrUOY= + -----END SSH SIGNATURE----- + +Test commit signed with rsa diff --git a/git/signature/testdata/ssh_signatures/commit_unsigned.txt b/git/signature/testdata/ssh_signatures/commit_unsigned.txt new file mode 100644 index 000000000..39c5b0a71 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/commit_unsigned.txt @@ -0,0 +1,5 @@ +tree 4650a2cda631bc795fc254fe20b598135b265036 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 + +Test commit unsigned diff --git a/git/signature/testdata/ssh_signatures/generate_ssh_fixtures.sh b/git/signature/testdata/ssh_signatures/generate_ssh_fixtures.sh new file mode 100755 index 000000000..bcbc8bcc1 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +# generate_fixtures.sh - Script to generate SSH signature test fixtures +# Generates SSH keys in all variants and signed Git objects + +set -euo pipefail + +# Configuration variables +TEST_USER_NAME="Test User" +TEST_USER_EMAIL="sign-user@example.com" +FIXTURE_DATE="2026-01-01T00:00:00+0000" + +# Isolate Git from user and system configuration for deterministic output +export TZ=UTC +export GIT_AUTHOR_DATE="$FIXTURE_DATE" +export GIT_COMMITTER_DATE="$FIXTURE_DATE" +export GIT_AUTHOR_NAME="$TEST_USER_NAME" +export GIT_AUTHOR_EMAIL="$TEST_USER_EMAIL" +export GIT_COMMITTER_NAME="$TEST_USER_NAME" +export GIT_COMMITTER_EMAIL="$TEST_USER_EMAIL" +export GIT_CONFIG_NOSYSTEM=1 +export GIT_CONFIG_GLOBAL=/dev/null + +# Directory for temporary files +TEMP_DIR=$(mktemp -d) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# define script dependencies +DEPENDENCY=( + ssh-keygen + cat + awk + git + find + sort +) + +echo "=== SSH Signature Test Fixtures Generator ===" +echo "Temporary directory: $TEMP_DIR" +echo "Output directory: $SCRIPT_DIR" +echo "" + +# cleanup on exit +cleanup() { + if [[ -d "${TEMP_DIR}" ]]; then + echo "=== Cleanup ===" + rm -rf "${TEMP_DIR}" + echo "Temporary directory removed" + fi +} + +# check necessary commands +check_dependencies() { + local exit_state=0 + + # check presence of dependencies + for COMMAND in "${DEPENDENCY[@]}"; do + if ! command -v "${COMMAND}" >/dev/null 2>&1; then + echo "command '${COMMAND}' not found, needs to be installed first." + exit_state=1 + fi + done + + if [[ ${exit_state} -ne 0 ]]; then + exit 1 + fi +} + +# Function to generate SSH keys +generate_ssh_key() { + local key_type=$1 + local key_bits=$2 + local key_name=$3 + + echo "Generating $key_name key pair..." + + case "$key_type" in + rsa) + ssh-keygen -t rsa -b "$key_bits" -f "$TEMP_DIR/$key_name" -N "" -C "test-$key_name@example.com" + ;; + ecdsa) + ssh-keygen -t ecdsa -b "$key_bits" -f "$TEMP_DIR/$key_name" -N "" -C "test-$key_name@example.com" + ;; + ed25519) + ssh-keygen -t ed25519 -f "$TEMP_DIR/$key_name" -N "" -C "test-$key_name@example.com" + ;; + esac + + # Copy public key to output directory with key_ prefix + cp "$TEMP_DIR/$key_name.pub" "$SCRIPT_DIR/key_${key_name}.pub" + echo " ✓ key_${key_name}.pub created" + + # Calculate and write SHA256 fingerprint to file + ssh-keygen -lf "$TEMP_DIR/$key_name.pub" | awk '{print $2}' > "$SCRIPT_DIR/key_${key_name}.pub_fingerprint" + echo " ✓ key_${key_name}.pub_fingerprint created" +} + +# Function to create verified signers files with git namespace +create_verified_signers() { + local key_name=$1 + local output_file="$TEMP_DIR/verified_signers_${key_name}" + + echo "Creating verified signers file for $key_name..." + + # Create verified signers file with git namespace + echo "$TEST_USER_EMAIL namespaces=\"git\" $(cat "$TEMP_DIR/${key_name}.pub")" > "$output_file" + echo " ✓ $output_file created" +} + +# Function to create combined authorized_keys file +create_combined_pub_keys() { + local output_file="$SCRIPT_DIR/keys_all.pub" + + echo "Creating combined authorized_keys..." + + # Combine all public keys + { + cat "$TEMP_DIR/rsa.pub" + cat "$TEMP_DIR/ecdsa_p256.pub" + cat "$TEMP_DIR/ecdsa_p384.pub" + cat "$TEMP_DIR/ecdsa_p521.pub" + cat "$TEMP_DIR/ed25519.pub" + } > "$output_file" + + echo " ✓ $output_file created" +} + + +# Function to create signed Git objects (commits and tags) +create_signed_object() { + local object_type=$1 + local key_name=$2 + local verified_signers_file="$TEMP_DIR/verified_signers_${key_name}" + + echo "Creating signed $object_type for $key_name..." + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_${key_name}_${object_type}" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init -b main + git config gpg.format ssh + git config user.signingkey "$TEMP_DIR/${key_name}.pub" + git config gpg.ssh.allowedSignersFile "$verified_signers_file" + + # Create file and commit + echo "Test content for $key_name $object_type" > test.txt + git add test.txt + git commit -m "Test commit for $object_type" + + if [[ "$object_type" == "commit" ]]; then + # Sign the commit (amend) + git commit --amend --allow-empty -S -m "Test commit signed with $key_name" + + # Verify the signed commit using git verify-commit + echo " Verifying signed commit with git verify-commit..." + if git verify-commit HEAD; then + echo " ✓ Commit signature verified successfully" + else + echo " ✗ Commit signature verification failed" + exit 1 + fi + + # Export commit object + local output_file="$SCRIPT_DIR/commit_${key_name}_signed.txt" + git cat-file commit HEAD > "$output_file" + cd "$SCRIPT_DIR" + echo " ✓ $output_file created" + + elif [[ "$object_type" == "tag" ]]; then + # Create and sign tag + git tag -a "test-tag-${key_name}" -m "Test tag signed with $key_name" -s + + # Verify the signed tag using git verify-tag + echo " Verifying signed tag with git verify-tag..." + if git verify-tag "test-tag-${key_name}"; then + echo " ✓ Tag signature verified successfully" + else + echo " ✗ Tag signature verification failed" + exit 1 + fi + + # Export tag object + local output_file="$SCRIPT_DIR/tag_${key_name}_signed.txt" + git cat-file tag "test-tag-${key_name}" > "$output_file" + cd "$SCRIPT_DIR" + echo " ✓ $output_file created" + else + echo "Error: unknown object type: ${object_type}" + fi +} + +# Function to create unsigned commit and tag +create_unsigned_commit_and_tag() { + local commit_file="$SCRIPT_DIR/commit_unsigned.txt" + local tag_file="$SCRIPT_DIR/tag_unsigned.txt" + + echo "Creating unsigned commit and tag..." + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_unsigned" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init -b main + + # Create file and commit (without signature) + echo "Test content unsigned" > test.txt + git add test.txt + git commit -m "Test commit unsigned" + + # Export commit object + git cat-file commit HEAD > "$commit_file" + + # Create and export tag object + git tag -a test-tag -m "Test tag" + git cat-file tag test-tag > "$tag_file" + + cd "$SCRIPT_DIR" + echo " ✓ $commit_file and $tag_file created" +} + +# Main program +main() { + + check_dependencies + + echo "Step 1: Generate SSH keys..." + echo "-----------------------------------" + + # RSA key (4096 bits) + generate_ssh_key "rsa" "4096" "rsa" + + # ECDSA keys (all variants: p256, p384, p521) + generate_ssh_key "ecdsa" "256" "ecdsa_p256" + generate_ssh_key "ecdsa" "384" "ecdsa_p384" + generate_ssh_key "ecdsa" "521" "ecdsa_p521" + + # ED25519 key + generate_ssh_key "ed25519" "" "ed25519" + + echo "" + echo "Step 2: Create pub_keys files..." + echo "-----------------------------------------------" + + # Combined pub_keys file + create_combined_pub_keys + + echo "" + echo "Step 3: Create verified signers files..." + echo "-----------------------------------------------" + + # Individual verified signers files with git namespace + create_verified_signers "rsa" + create_verified_signers "ecdsa_p256" + create_verified_signers "ecdsa_p384" + create_verified_signers "ecdsa_p521" + create_verified_signers "ed25519" + + echo "" + echo "Step 4: Create signed commits..." + echo "----------------------------------------" + + # Signed commits for each key type + create_signed_object "commit" "rsa" + create_signed_object "commit" "ecdsa_p256" + create_signed_object "commit" "ecdsa_p384" + create_signed_object "commit" "ecdsa_p521" + create_signed_object "commit" "ed25519" + + echo "" + echo "Step 5: Create signed tags..." + echo "-------------------------------------" + + # Signed tags for each key type + create_signed_object "tag" "rsa" + create_signed_object "tag" "ecdsa_p256" + create_signed_object "tag" "ecdsa_p384" + create_signed_object "tag" "ecdsa_p521" + create_signed_object "tag" "ed25519" + + echo "" + echo "Step 6: Create unsigned commit and tag..." + echo "------------------------------------------" + + create_unsigned_commit_and_tag + + echo "" + echo "=== Done! ===" + echo "All test fixtures have been successfully created." + echo "" + echo "Created files:" + find "$SCRIPT_DIR" -maxdepth 1 \( -name "*.txt" -o -name "key_*.pub" -o -name "authorized_keys*" -o -name "verified_signers*" \) -exec ls -lh {} \; 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' | sort +} + +trap cleanup EXIT + +# Run script +main diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub new file mode 100644 index 000000000..6b72ec718 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPlZqzf0dwAPlQVvrP99hF4R0hWa0DDuNs0eAUxkGrvSiVqUrqjLOLNco5dibDFCYGm7VBq6JBN79jjx6bXGu0A= test-ecdsa_p256@example.com diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint new file mode 100644 index 000000000..e318d1da1 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint @@ -0,0 +1 @@ +SHA256:KmuONXzOKczqEizPDlnkithlWIcGBeFZFxfbyIWNPuI diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub new file mode 100644 index 000000000..b893f2ca9 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKXgoDFrE6RWKm+DkwSey/FvJOfezSqVLQwSx0382+vKMKTZUR7Jh9OgTEuFbb8rTSE7sChlFBD4VNMmuDvMCz7iGa1EGA4hYTkwGAwANKm/34xEqNUze/V9T4U631VPcg== test-ecdsa_p384@example.com diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint new file mode 100644 index 000000000..f337ad857 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint @@ -0,0 +1 @@ +SHA256:sUEVeU46mIrOIYG9o6xrcIDB/0PM0ZcKDrVTIgtpdW0 diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub new file mode 100644 index 000000000..705008d4e --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAB778Lk/xWvjbwI8ABv9XVsVn5Tqc+puiENJQC5RsBbuS7Cvwxns1jXolnZH5nAP+fGMBaL/9EHN0CAfYeRqTxkVAGUeR9NvFoaVFOVlfNnWBWQylRaLw4nvNHqlrSdf7/AopuUHIVyiSN87xMIvizc4PYIU4tK1HTE239mR3hjyFmY1A== test-ecdsa_p521@example.com diff --git a/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint new file mode 100644 index 000000000..74c3ee8a9 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint @@ -0,0 +1 @@ +SHA256:sAL1j3Z4OVuLuUl+beESBkqaTfsAu4qNJe60wHAw0Bc diff --git a/git/signature/testdata/ssh_signatures/key_ed25519.pub b/git/signature/testdata/ssh_signatures/key_ed25519.pub new file mode 100644 index 000000000..d2d083218 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2EhITylf+uZMxtiS2FPufOmp0wDkW22dFFvas6J9vB test-ed25519@example.com diff --git a/git/signature/testdata/ssh_signatures/key_ed25519.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ed25519.pub_fingerprint new file mode 100644 index 000000000..676565164 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_ed25519.pub_fingerprint @@ -0,0 +1 @@ +SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4 diff --git a/git/signature/testdata/ssh_signatures/key_rsa.pub b/git/signature/testdata/ssh_signatures/key_rsa.pub new file mode 100644 index 000000000..a5f229c19 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDP52UUik6GdRebk00VAzaDDO1Vh++WqMhPkaYUu5NvSx3t6lRqGwV0CM5VPEzPZ5I8NTGoq6H4IHD8pk4tONVQVUq9YIJgs9AqvTjYFbaaSfiIqlLNxot4Cyc8IEnskSD9XZoZtNjbLnrrFcdOEORGabRxbWMUXr9OxaJGItpGO76GCCGN7Fpyxar26sM7Wgwy6G0I82xlmKyrZuCfrzJQMwNuHEs2FkDisu5M5CXU5bbBN9fPIpyDzDY8DLBfmg8TINKZZLlJe2wf2myLQJABxJooXLyN8FTePu4MfPQHosW+taOM7NahEtne4RZOhdMG0A1WJfrLuku3w+pXZD530IXPh63wAx+bz4PMrM38iR+U3OdV2Cwfk0L0cmswf2OxhZ7ExI89xqFSdS+3KGzWtlyCLUni8NqQU5SipsToQVwEOFtctBMYG/PyfY1QjPYhuXq760SsIHUBEQvMLKLhCxkNF/9tNmRyblhO6B1hOn3jZfm9rCHQc2zZLvHiPcWJAA83GPZYgeo7DAsGEqL9+5fCNWU0TAakTRkvYlH7TBaxxTglSjGYbXHTlYoeiUVKAkSffAVjZdOhHuvn9yrArRCi2StGXenueHZ8tzRwGfi6XFEkejkob0pRaVTNmy3CoK4HFGbkhJALuH3lKMIk7kztDAOSLHGLlQDlpIoOWw== test-rsa@example.com diff --git a/git/signature/testdata/ssh_signatures/key_rsa.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_rsa.pub_fingerprint new file mode 100644 index 000000000..4ce5c9079 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/key_rsa.pub_fingerprint @@ -0,0 +1 @@ +SHA256:ruOMGhsHMnFnPXNt2DmM3XHHHQAJWib+sokHBG6tWdA diff --git a/git/signature/testdata/ssh_signatures/keys_all.pub b/git/signature/testdata/ssh_signatures/keys_all.pub new file mode 100644 index 000000000..e08b73097 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/keys_all.pub @@ -0,0 +1,5 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDP52UUik6GdRebk00VAzaDDO1Vh++WqMhPkaYUu5NvSx3t6lRqGwV0CM5VPEzPZ5I8NTGoq6H4IHD8pk4tONVQVUq9YIJgs9AqvTjYFbaaSfiIqlLNxot4Cyc8IEnskSD9XZoZtNjbLnrrFcdOEORGabRxbWMUXr9OxaJGItpGO76GCCGN7Fpyxar26sM7Wgwy6G0I82xlmKyrZuCfrzJQMwNuHEs2FkDisu5M5CXU5bbBN9fPIpyDzDY8DLBfmg8TINKZZLlJe2wf2myLQJABxJooXLyN8FTePu4MfPQHosW+taOM7NahEtne4RZOhdMG0A1WJfrLuku3w+pXZD530IXPh63wAx+bz4PMrM38iR+U3OdV2Cwfk0L0cmswf2OxhZ7ExI89xqFSdS+3KGzWtlyCLUni8NqQU5SipsToQVwEOFtctBMYG/PyfY1QjPYhuXq760SsIHUBEQvMLKLhCxkNF/9tNmRyblhO6B1hOn3jZfm9rCHQc2zZLvHiPcWJAA83GPZYgeo7DAsGEqL9+5fCNWU0TAakTRkvYlH7TBaxxTglSjGYbXHTlYoeiUVKAkSffAVjZdOhHuvn9yrArRCi2StGXenueHZ8tzRwGfi6XFEkejkob0pRaVTNmy3CoK4HFGbkhJALuH3lKMIk7kztDAOSLHGLlQDlpIoOWw== test-rsa@example.com +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPlZqzf0dwAPlQVvrP99hF4R0hWa0DDuNs0eAUxkGrvSiVqUrqjLOLNco5dibDFCYGm7VBq6JBN79jjx6bXGu0A= test-ecdsa_p256@example.com +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKXgoDFrE6RWKm+DkwSey/FvJOfezSqVLQwSx0382+vKMKTZUR7Jh9OgTEuFbb8rTSE7sChlFBD4VNMmuDvMCz7iGa1EGA4hYTkwGAwANKm/34xEqNUze/V9T4U631VPcg== test-ecdsa_p384@example.com +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAB778Lk/xWvjbwI8ABv9XVsVn5Tqc+puiENJQC5RsBbuS7Cvwxns1jXolnZH5nAP+fGMBaL/9EHN0CAfYeRqTxkVAGUeR9NvFoaVFOVlfNnWBWQylRaLw4nvNHqlrSdf7/AopuUHIVyiSN87xMIvizc4PYIU4tK1HTE239mR3hjyFmY1A== test-ecdsa_p521@example.com +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2EhITylf+uZMxtiS2FPufOmp0wDkW22dFFvas6J9vB test-ed25519@example.com diff --git a/git/signature/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt new file mode 100644 index 000000000..5a1008f34 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt @@ -0,0 +1,13 @@ +object 22d258d46f5bc50420db6a5e4c8d60f35a78fa8d +type commit +tag test-tag-ecdsa_p256 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p256 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE +EE+VmrN/R3AA+VBW+s/32EXhHSFZrQMO42zR4BTGQau9KJWpSuqMs4s1yjl2JsMUJgabtU +GrokE3v2OPHptca7QAAAAANnaXQAAAAAAAAABnNoYTUxMgAAAGMAAAATZWNkc2Etc2hhMi +1uaXN0cDI1NgAAAEgAAAAgCxZSTGktMcJiqH9WpFa00o5hcxF8nSBfNIPlgF9Jy4AAAAAg +Z1z3tz/8gqR34z6y7GaNbDNM1Nl7r9bvEE3jFrAZ6OQ= +-----END SSH SIGNATURE----- diff --git a/git/signature/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt new file mode 100644 index 000000000..0cf1c1dfb --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt @@ -0,0 +1,14 @@ +object 35f5804b751475a1856b7efc13ccba249bdcd32d +type commit +tag test-tag-ecdsa_p384 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p384 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG +EEpeCgMWsTpFYqb4OTBJ7L8W8k597NKpUtDBLHTfzb68owpNlRHsmH06BMS4VtvytNITuw +KGUUEPhU0ya4O8wLPuIZrUQYDiFhOTAYDAA0qb/fjESo1TN79X1PhTrfVU9yAAAAA2dpdA +AAAAAAAAAGc2hhNTEyAAAAhQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAagAAADEAtnQb +F4WKv4eNjOlVh+nRn6emQwVp1VeXuGVp2wepXCINVnytAG76n/b5ZJ6gNSIcAAAAMQDkHJ +VlowOSJVMPP4jraIYZ2jNuxO5ABgATi5EHR0tTIFyeuxFx5z8hPfMB4YoDtJk= +-----END SSH SIGNATURE----- diff --git a/git/signature/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt new file mode 100644 index 000000000..a38277f13 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt @@ -0,0 +1,16 @@ +object 18011cecb9ae833a29b75526e801e8c0b7951c3f +type commit +tag test-tag-ecdsa_p521 +tagger Test User 1767225600 +0000 + +Test tag signed with ecdsa_p521 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAKwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAI +UEAHvvwuT/Fa+NvAjwAG/1dWxWflOpz6m6IQ0lALlGwFu5LsK/DGezWNeiWdkfmcA/58Yw +Fov/0Qc3QIB9h5GpPGRUAZR5H028WhpUU5WV82dYFZDKVFovDie80eqWtJ1/v8Cim5QchX +KJI3zvEwi+LNzg9ghTi0rUdMTbf2ZHeGPIWZjUAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAAA +pwAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAAjAAAAEIAupc/dSHO3kpXDI8XQYNX9ovZas +4uedvAnvXmtote/H1WurUtUh57q125yLeh1fxBAhs7mpO/OZiTBjBsdM5K9OcAAABCAQzQ +1xw+lmMhLsFMrzZ1y3EgXI74968Fbchj9APgQIzZG6cyfSZzE+UIkgDPk8j6x1BaMbwWYw +EuzjWGZa2mO8QC +-----END SSH SIGNATURE----- diff --git a/git/signature/testdata/ssh_signatures/tag_ed25519_signed.txt b/git/signature/testdata/ssh_signatures/tag_ed25519_signed.txt new file mode 100644 index 000000000..ea7e50bac --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_ed25519_signed.txt @@ -0,0 +1,12 @@ +object b01ca16de562c561f62427eb0a30cb775d1c1dab +type commit +tag test-tag-ed25519 +tagger Test User 1767225600 +0000 + +Test tag signed with ed25519 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgrYSEhPKV/65kzG2JLYU+586anT +AORbbZ0UW9qzon28EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQOZe8xDqF1oktx3h9p3EQKSx13zFVHQ2YcpF/HfeuQusKA1tvJY+T+ykPH4+zbva93 +KXi2P5xm89dQni0kTeAAY= +-----END SSH SIGNATURE----- diff --git a/git/signature/testdata/ssh_signatures/tag_rsa_signed.txt b/git/signature/testdata/ssh_signatures/tag_rsa_signed.txt new file mode 100644 index 000000000..d4f8fde7b --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_rsa_signed.txt @@ -0,0 +1,30 @@ +object d3a06c9ce3496cc156cdb256efafd82c45ac96d9 +type commit +tag test-tag-rsa +tagger Test User 1767225600 +0000 + +Test tag signed with rsa +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAM/nZRSKToZ1F5uTTRUDNo +MM7VWH75aoyE+RphS7k29LHe3qVGobBXQIzlU8TM9nkjw1MairofggcPymTi041VBVSr1g +gmCz0Cq9ONgVtppJ+IiqUs3Gi3gLJzwgSeyRIP1dmhm02NsueusVx04Q5EZptHFtYxRev0 +7FokYi2kY7voYIIY3sWnLFqvbqwztaDDLobQjzbGWYrKtm4J+vMlAzA24cSzYWQOKy7kzk +JdTltsE3188inIPMNjwMsF+aDxMg0plkuUl7bB/abItAkAHEmihcvI3wVN4+7gx89Aeixb +61o4zs1qES2d7hFk6F0wbQDVYl+su6S7fD6ldkPnfQhc+HrfADH5vPg8yszfyJH5Tc51XY +LB+TQvRyazB/Y7GFnsTEjz3GoVJ1L7cobNa2XIItSeLw2pBTlKKmxOhBXAQ4W1y0Exgb8/ +J9jVCM9iG5ervrRKwgdQERC8wsouELGQ0X/202ZHJuWE7oHWE6feNl+b2sIdBzbNku8eI9 +xYkADzcY9liB6jsMCwYSov37l8I1ZTRMBqRNGS9iUftMFrHFOCVKMZhtcdOVih6JRUoCRJ +98BWNl06Ee6+f3KsCtEKLZK0Zd6e54dny3NHAZ+LpcUSR6OShvSlFpVM2bLcKgrgcUZuSE +kAu4feUowiTuTO0MA5IscYuVAOWkig5bAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAACFAAAAA +xyc2Etc2hhMi01MTIAAAIAAgRV8G/ngPQBftA2zeWyTC9hntqtHkKlIcc5958ti0T/XnY/ +yex2Jt3Ex1BzIoaPUQPIKutYVLgU2K9ZR4tg9NZamey6TCU/KJvojyTi9R9niGtTNmGKMN +tB9qlb459TmAD25RrB3kN07BeJ4V6fgdXReoiRKviQLJLSGSGPfG4MEoA/TViKwBH7tdsT +U5MWBwazejuOMqojD0yoKRVyFbcH7I34Pk3MfOGTxoXz6902RonYDZddAqGCg4knAp0Nfe +EcA07SlXy2B33yF0YW+wH5G+iSuXJ3WctIoZBn0qqYdltDJIug8jBosFtQTnq2jnu+02HN +ufCDPL7UJfQFdEQFCfGY614G+WJS3/clf6cD5qfvb0B9lRbdr4UDrln/JTztFYxrknYbKz +43xV9p2muj1ym707h0BmOc+fWlWxd1GvnYnev2S5MiFdPh3Q5SKT9ejCL7UhnAvhSq75/9 +AE1t5nV89dHl2Asa6D3wq0U4e7GA8fMZK1cjWVVH6JA3PiyhcUZA3sxD2AQlenMgVLqqfW +fyj/p6jihKGfV5zVSoyMp9sL/riBnmzXPhyVRWPgm2vrKs5BTEqSSZEM5PxUTSBMgf1q88 +53fniVIk1lLn6plydVi0Tbwut4wd70w/9pwct2HXeceFVge0NuTaq1JdRzrg8G5/ms4d/I +sBdYQ= +-----END SSH SIGNATURE----- diff --git a/git/signature/testdata/ssh_signatures/tag_unsigned.txt b/git/signature/testdata/ssh_signatures/tag_unsigned.txt new file mode 100644 index 000000000..ec80cabd7 --- /dev/null +++ b/git/signature/testdata/ssh_signatures/tag_unsigned.txt @@ -0,0 +1,6 @@ +object f85d47148d57e658056d9859377ec47ff17d0e98 +type commit +tag test-tag +tagger Test User 1767225600 +0000 + +Test tag diff --git a/git/testutils/fixtures.go b/git/testutils/fixtures.go new file mode 100644 index 000000000..da5327f20 --- /dev/null +++ b/git/testutils/fixtures.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testutils + +import ( + "os" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +// ParseCommitFromFixture parses a git commit object from a fixture file +func ParseCommitFromFixture(fixturePath string) (*object.Commit, error) { + data, err := os.ReadFile(fixturePath) + if err != nil { + return nil, err + } + + // Create a MemoryObject and write the commit data to it + obj := &plumbing.MemoryObject{} + obj.SetType(plumbing.CommitObject) + if _, err := obj.Write(data); err != nil { + return nil, err + } + + // Decode the commit object + commit := &object.Commit{} + if err := commit.Decode(obj); err != nil { + return nil, err + } + + return commit, nil +} + +// ParseTagFromFixture parses a git tag object from a fixture file +func ParseTagFromFixture(fixturePath string) (*object.Tag, error) { + data, err := os.ReadFile(fixturePath) + if err != nil { + return nil, err + } + + // Create a MemoryObject and write the tag data to it + obj := &plumbing.MemoryObject{} + obj.SetType(plumbing.TagObject) + if _, err := obj.Write(data); err != nil { + return nil, err + } + + // Decode the tag object + tag := &object.Tag{} + if err := tag.Decode(obj); err != nil { + return nil, err + } + + return tag, nil +} diff --git a/tests/integration/go.mod b/tests/integration/go.mod index f4318f89a..fa54016ba 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -115,6 +115,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect + github.com/hiddeco/sshsig v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/tests/integration/go.sum b/tests/integration/go.sum index 4e4172e7a..92d4d6998 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -213,6 +213,8 @@ github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5 github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=