diff --git a/git/git.go b/git/git.go index 1bff7cfb..1d53cd33 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/signatures" ) 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 := signatures.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 := signatures.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 := signatures.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 := signatures.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 signatures.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 signatures.IsSSHSignature(c.Signature) +} + +// SignatureType returns the type of the commit signature as a string. +// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// and "unknown" for unrecognized or empty signatures. +func (c *Commit) SignatureType() string { + return signatures.GetSignatureType(c.Signature) +} + +// IsPGPSigned returns true if the tag has a PGP signature. +func (t *Tag) IsPGPSigned() bool { + return signatures.IsPGPSignature(t.Signature) +} + +// IsSSHSigned returns true if the tag has an SSH signature. +func (t *Tag) IsSSHSigned() bool { + return signatures.IsSSHSignature(t.Signature) +} + +// SignatureType returns the type of the tag signature as a string. +// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// and "unknown" for unrecognized or empty signatures. +func (t *Tag) SignatureType() string { + return signatures.GetSignatureType(t.Signature) } diff --git a/git/git_test.go b/git/git_test.go index 1c18d61f..ea2347ca 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("signatures", "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("signatures", "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: "commit_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("signatures", "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("signatures", "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: "commit_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 93442a85..11f0e875 100644 --- a/git/go.mod +++ b/git/go.mod @@ -20,6 +20,7 @@ require ( github.com/fluxcd/pkg/version v0.14.0 github.com/go-git/go-billy/v5 v5.7.0 github.com/go-git/go-git/v5 v5.16.5 + github.com/hiddeco/sshsig v0.2.0 github.com/onsi/gomega v1.39.0 golang.org/x/crypto v0.47.0 ) diff --git a/git/go.sum b/git/go.sum index 53245b0a..0f791452 100644 --- a/git/go.sum +++ b/git/go.sum @@ -48,6 +48,8 @@ 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/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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 59204450..8b78c3cb 100644 --- a/git/gogit/clone.go +++ b/git/gogit/clone.go @@ -136,7 +136,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 BuildCommitWithRef(cc, nil, ref) } func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -236,7 +236,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 BuildCommitWithRef(cc, tagObj, ref) } func (g *Client) cloneCommit(ctx context.Context, url, commit string, opts repository.CloneConfig) (*git.Commit, error) { @@ -305,7 +305,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 BuildCommitWithRef(cc, nil, cloneOpts.ReferenceName) } func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -439,7 +439,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 BuildCommitWithRef(cc, tagObj, tagRef.Name()) } func (g *Client) cloneRefName(ctx context.Context, url string, refName string, cloneOpts repository.CloneConfig) (*git.Commit, error) { @@ -582,7 +582,7 @@ func buildSignature(s object.Signature) git.Signature { } } -func buildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { +func BuildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { if t == nil { return &git.Tag{ Name: ref.Short(), @@ -612,7 +612,7 @@ func buildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { }, nil } -func buildCommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceName) (*git.Commit, error) { +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") } @@ -641,7 +641,7 @@ func buildCommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceN } if ref.IsTag() { - tt, err := buildTag(t, ref) + tt, err := BuildTag(t, ref) if err != nil { return nil, err } diff --git a/git/internal/e2e/go.mod b/git/internal/e2e/go.mod index b7654ae2..53d7d65b 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/git/internal/e2e/go.sum b/git/internal/e2e/go.sum index 77c54a49..6baa341a 100644 --- a/git/internal/e2e/go.sum +++ b/git/internal/e2e/go.sum @@ -81,6 +81,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/signatures/gpg_signature.go b/git/signatures/gpg_signature.go new file mode 100644 index 00000000..94c2ae2a --- /dev/null +++ b/git/signatures/gpg_signature.go @@ -0,0 +1,62 @@ +/* +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 signatures + +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 fingerprint 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)) + } + + 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(signature), nil) + if err == nil { + return signer.PrimaryKey.KeyIdString(), nil + } + } + return "", fmt.Errorf("unable to verify payload with any of the given key rings") +} diff --git a/git/signatures/gpg_signature_test.go b/git/signatures/gpg_signature_test.go new file mode 100644 index 00000000..40c7c672 --- /dev/null +++ b/git/signatures/gpg_signature_test.go @@ -0,0 +1,334 @@ +/* +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 signatures_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/signatures" + "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", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := signatures.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}, + + // ed448 test fails because the key was created with OpenPGP version 5, + // which is not supported by github.com/ProtonMail/go-crypto (only version 4 is supported). + // The error occurs when trying to read the armored key ring: + // "unable to read armored key ring: openpgp: invalid data: first packet was not a public/private key" + {"ed448 valid signature", "commit_ed448_signed.txt", "tag_ed448_signed.txt", "key_ed448.pub", true}, + } + + 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 BuildTag + gitTag, err := gogit.BuildTag(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 := signatures.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 = signatures.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 BuildCommitWithRef + gitCommit, err := gogit.BuildCommitWithRef(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 := signatures.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 = signatures.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) + + // Parse the unsigned commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) + g.Expect(err).ToNot(HaveOccurred()) + + // Build a git.Commit using BuildCommitWithRef + gitCommit, err := gogit.BuildCommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + g.Expect(err).ToNot(HaveOccurred()) + + // Read a public key + publicKey, err := os.ReadFile(filepath.Join(testDataDir, "key_rsa_2048.pub")) + g.Expect(err).ToNot(HaveOccurred()) + + // Verify the signature - should fail as the commit is unsigned + fingerprint, err := signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, string(publicKey)) + g.Expect(err).To(HaveOccurred()) + g.Expect(fingerprint).To(BeEmpty()) + }) +} diff --git a/git/signatures/signature.go b/git/signatures/signature.go new file mode 100644 index 00000000..5b7ba3ed --- /dev/null +++ b/git/signatures/signature.go @@ -0,0 +1,101 @@ +/* +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 signatures + +import ( + "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-----"} + +func startsWithStrings(signature string, prefixList []string) bool { + if signature == "" { + return false + } + + for _, prefix := range prefixList { + if strings.HasPrefix(strings.TrimSpace(signature), prefix) { + return true + } + } + + return false +} + +// 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 startsWithStrings(signature, PGPSignaturePrefix) +} + +// 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 startsWithStrings(signature, SSHSignaturePrefix) +} + +// 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 startsWithStrings(signature, X509SignaturePrefix) +} + +// 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 "pgp" 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/signatures/signature_test.go b/git/signatures/signature_test.go new file mode 100644 index 00000000..b079b878 --- /dev/null +++ b/git/signatures/signature_test.go @@ -0,0 +1,241 @@ +/* +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 signatures_test + +import ( + "testing" + + . "github.com/fluxcd/pkg/git/signatures" +) + +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, + }, + } + + 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, + }, + } + + 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), + }, + } + + 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/signatures/ssh_signature.go b/git/signatures/ssh_signature.go new file mode 100644 index 00000000..f9c80719 --- /dev/null +++ b/git/signatures/ssh_signature.go @@ -0,0 +1,109 @@ +/* +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 signatures + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "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.Split(authorizedKeys, "\n") { + 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) + } + + // Try to verify with each set of authorized keys + for _, keys := range authorizedKeys { + publicKeys, err := ParseAuthorizedKeys(keys) + if err != nil { + return "", fmt.Errorf("unable to parse authorized keys: %w", err) + } + + // 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 getPublicKeyFingerprint(pubKey), nil + } + } + } + + return "", fmt.Errorf("unable to verify payload with any of the given authorized keys") +} + +// getPublicKeyFingerprint returns the SHA256 fingerprint of the public key +// in the format used by SSH (e.g., "SHA256:abc123..."). +func getPublicKeyFingerprint(pubKey gossh.PublicKey) string { + hash := sha256.Sum256(pubKey.Marshal()) + return "SHA256:" + base64.RawStdEncoding.EncodeToString(hash[:]) +} diff --git a/git/signatures/ssh_signature_keys_test.go b/git/signatures/ssh_signature_keys_test.go new file mode 100644 index 00000000..3761d469 --- /dev/null +++ b/git/signatures/ssh_signature_keys_test.go @@ -0,0 +1,294 @@ +/* +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 signatures + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// these tests are in the same package to test private getPublicKeyFingerprint function + +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 := 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 := getPublicKeyFingerprint(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 := 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 := getPublicKeyFingerprint(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 := 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 := getPublicKeyFingerprint(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/signatures/ssh_signature_test.go b/git/signatures/ssh_signature_test.go new file mode 100644 index 00000000..80cacb12 --- /dev/null +++ b/git/signatures/ssh_signature_test.go @@ -0,0 +1,400 @@ +/* +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 signatures_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/signatures" + "github.com/fluxcd/pkg/git/testutils" + "github.com/go-git/go-git/v5/plumbing" +) + +// these tests are in a different package to avoid circular dependencies with gogit.BuildCommitWithRef and gogit.BuildTag + +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 BuildCommitWithRef + gitCommit, err := gogit.BuildCommitWithRef(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 BuildCommitWithRef + gitTag, err := gogit.BuildTag(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 := signatures.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 = signatures.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 = signatures.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 = signatures.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) + } + + }) + } +} + +func TestSSHSignatureValidationCases(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + key_type := "ed25519" + + pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_"+key_type+".pub")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the commit from the fixture file + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_"+key_type+"_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse commit from fixture: %v", err) + } + + // Parse the tag from the fixture file + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_"+key_type+"_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %v", err) + } + + // Build a git.Commit using BuildCommitWithRef + gitCommit, err := gogit.BuildCommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) + if err != nil { + t.Fatalf("Failed to build commit: %v", err) + } + + // Build a git.Tag using BuildTag + gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + if err != nil { + t.Fatalf("Failed to build tag: %v", err) + } + + // Test error cases + t.Run("empty signature", func(t *testing.T) { + + fingerprint, err := signatures.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty signature: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload as the provided signature is empty" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload as the provided signature is empty'", err) + } + + fingerprint, err = signatures.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty signature: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload as the provided signature is empty" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload as the provided signature is empty'", err) + } + + }) + + t.Run("empty payload", func(t *testing.T) { + + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, []byte{}, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty payload: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload as the provided payload is empty" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload as the provided payload is empty'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, []byte{}, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty payload: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload as the provided payload is empty" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload as the provided payload is empty'", err) + } + + }) + + t.Run("wrong authorized keys", func(t *testing.T) { + // Use a different key that won't match + wrongKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEyM97VxLgOCuB9Eg5cDtTc8ogkdM1xAyJhzODB9cK1 wrong@example.com" + + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, wrongKey) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for wrong authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for wrong authorized keys: %s", fingerprint) + } + // The error can be either a parsing error or a verification error + if err != nil && !strings.Contains(err.Error(), "unable to verify payload with any of the given authorized keys") && !strings.Contains(err.Error(), "unable to parse authorized key") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to verify payload with any of the given authorized keys' or 'unable to parse authorized key'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, wrongKey) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for wrong authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for wrong authorized keys: %s", fingerprint) + } + // The error can be either a parsing error or a verification error + if err != nil && !strings.Contains(err.Error(), "unable to verify payload with any of the given authorized keys") && !strings.Contains(err.Error(), "unable to parse authorized key") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to verify payload with any of the given authorized keys' or 'unable to parse authorized key'", err) + } + }) + + t.Run("empty authorized keys", func(t *testing.T) { + // Use empty authorized keys + emptyAuthKeys := "" + + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, emptyAuthKeys) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty authorized keys: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload with any of the given authorized keys" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload with any of the given authorized keys'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, emptyAuthKeys) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty authorized keys: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify payload with any of the given authorized keys" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify payload with any of the given authorized keys'", err) + } + }) + + t.Run("invalid signature", func(t *testing.T) { + invalidSig := "-----BEGIN SSH SIGNATURE-----\n invalid\n -----END SSH SIGNATURE-----" + + fingerprint, err := signatures.VerifySSHSignature(invalidSig, gitCommit.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for invalid signature: %s", fingerprint) + } + if err != nil && !strings.Contains(err.Error(), "unable to unarmor SSH signature") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to unarmor SSH signature'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(invalidSig, gitTag.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for invalid signature: %s", fingerprint) + } + if err != nil && !strings.Contains(err.Error(), "unable to unarmor SSH signature") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to unarmor SSH signature'", err) + } + + }) + + t.Run("non-SSH signature", func(t *testing.T) { + // Use a PGP signature instead of SSH signature + pgpSig := "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----" + + fingerprint, err := signatures.VerifySSHSignature(pgpSig, gitCommit.Encoded, "") + if err == nil { + t.Errorf("VerifySSHSignature() expected error for non-SSH signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for non-SSH signature: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify SSH signature, detected signature format: openpgp" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify SSH signature, detected signature format: openpgp'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(pgpSig, gitTag.Encoded, "") + if err == nil { + t.Errorf("VerifySSHSignature() expected error for non-SSH signature, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for non-SSH signature: %s", fingerprint) + } + if err != nil && err.Error() != "unable to verify SSH signature, detected signature format: openpgp" { + t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify SSH signature, detected signature format: openpgp'", err) + } + }) + + t.Run("invalid authorized keys", func(t *testing.T) { + // Use invalid authorized keys + invalidAuthKeys := "invalid-key-data" + + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, invalidAuthKeys) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for invalid authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for invalid authorized keys: %s", fingerprint) + } + if err != nil && !strings.Contains(err.Error(), "unable to parse authorized key") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to parse authorized key'", err) + } + + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, invalidAuthKeys) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for invalid authorized keys, got nil") + } + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for invalid authorized keys: %s", fingerprint) + } + if err != nil && !strings.Contains(err.Error(), "unable to parse authorized key") { + t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to parse authorized key'", err) + } + }) +} diff --git a/git/signatures/testdata/gpg_signatures/README.md b/git/signatures/testdata/gpg_signatures/README.md new file mode 100644 index 00000000..e7e8187e --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/README.md @@ -0,0 +1,378 @@ +# 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, Ed448) + + **Note:** Some key types (like Ed448) require GnuPG 2.3 or higher. The script will report any failures and continue with successfully generated keys. + +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 Commit**: + - One unsigned commit 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 < batch_ed448.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 +gpg --armor --export test-ed448@example.com > key_ed448.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 +- `key_ed448.pub` - Ed448 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 +- `commit_ed448_signed.txt` - Ed448 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 +- `tag_ed448_signed.txt` - Ed448 signed tag + +### Unsigned Commit +- `commit_unsigned.txt` - Unsigned commit 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 +- **Ed448**: Higher security variant +- 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 +Some key types (like Ed448) require GnuPG 2.3 or higher. If you encounter errors, check your GPG version: + +```bash +gpg --version +``` + +### Key generation failures +The script now includes comprehensive error handling: +- Each key generation attempt is logged +- Failed keys are reported with detailed error messages +- The script continues with successfully generated keys +- An error log is created in the temporary directory + +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 key types: +- `generate_rsa_dsa_key()` - For RSA keys with key length validation +- `generate_ecc_key()` - For ECC/ECDSA/EdDSA keys with curve validation +- `create_signed_object()` - For creating signed commits and tags +- `create_unsigned_commit()` - For creating unsigned test commits + +Each function includes parameter validation and proper error handling. + +### 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/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt b/git/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt new file mode 100644 index 00000000..8e2e958c --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt @@ -0,0 +1,12 @@ +tree 1673f4226b68c3c29e8d038052698fd10706eb7e +author Test User 1772188964 +0100 +committer Test User 1772188964 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iHUEABMIAB0WIQSHtLFiUpKKegTi4RlOd7ceLHgABgUCaaF1JAAKCRBOd7ceLHgA + BpOTAP9KFSViLeUSJMzw9I2nW/kMJRWIXUE2XE+wuj/A2PTxYgD/ef3PLdiDr0l+ + CzdrXSQRdiNkD6avr8KEyy/Q0vz+03Y= + =IHuS + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p256 diff --git a/git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt b/git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt new file mode 100644 index 00000000..9feb7d2f --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff5f115ae071fc5b5984c3cf8a2e14fb86e54596 +author Test User 1772188964 +0100 +committer Test User 1772188964 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iJUEABMJAB0WIQQ/Ad7FJfKxg1LGHLMZ238vxsg1ZQUCaaF1JAAKCRAZ238vxsg1 + ZRMFAX9PF5KWQcYJla4N0RPc/EwrYkmNVH7yJeKUiJA1H6efE99/0tejkP+oNLAr + RUH4HngBf0E/aFFzZD1T/D+mZgpwptGWL+3m41vo92byaUdeEcOfGZWGPzVceAsY + uesfSeUOAA== + =u9GB + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p384 diff --git a/git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt b/git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt new file mode 100644 index 00000000..76c1efc1 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt @@ -0,0 +1,13 @@ +tree a9ac3b19ae895b654fadecbf65d68b6b904e9015 +author Test User 1772188964 +0100 +committer Test User 1772188964 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iLUEABMKAB0WIQRFqHbkH9cuZyIgGbcl0p71vcaJEQUCaaF1JAAKCRAl0p71vcaJ + EbsGAf0UpYkwRuLxUfV19hj31s8CFTrqe4e8DgKhZxv1cNX/0FUE8n/u15GePsQQ + /I0Omw7bXSKo8wh0VeUD17GjiDOeAf9WBNDV9qQh3Z1Vc01DHQrzp0RKzoeTquxe + ivA0N6jknF9V6smfTbL0I6SLu3dtrA+1dh3CDeQCROdhH3aA7ZaG + =aUDv + -----END PGP SIGNATURE----- + +Test commit signed with brainpool_p512 diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt new file mode 100644 index 00000000..a95b90f9 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt @@ -0,0 +1,12 @@ +tree 2f0fa5393a2120151c5446eb34b99d1f3713ff12 +author Test User 1772188965 +0100 +committer Test User 1772188965 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iHUEABMIAB0WIQQZYVspROx35dYcOV/9NQRFrxVHiwUCaaF1JQAKCRD9NQRFrxVH + i7V+AQCBE5nzpuGEjw8dTsdQ7o53ec1fN/O8IoRreC98vr2/9AD9E6Yu6b0t+ahp + j90zFJCPdc+cAxk4mVXh4piVbJ8tPvQ= + =dI7Q + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p256 diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt new file mode 100644 index 00000000..596f9951 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff58328bd5797f45f6f300c6c39d2cd357b9f3cd +author Test User 1772188965 +0100 +committer Test User 1772188965 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iJUEABMJAB0WIQScyLxivLVGKonynyhueVMDKL7ECgUCaaF1JQAKCRBueVMDKL7E + CvZZAYC3WouUxsPpDyK3rwkhe9/tLEeSq+Z2nIUNTK3CYjw2MbyqKqMav4dZiYun + C78+910BgMF8yGkEhzSVnl5ZtNe6CXP4ZTrtdeo8WsOwvJaiey9YA/HYLLsSW/67 + uhz/ua8xtQ== + =SeMA + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p384 diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt new file mode 100644 index 00000000..f0fe269b --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt @@ -0,0 +1,13 @@ +tree 63af4f62a108a6c684181a4488b4bd3a5b51dc8e +author Test User 1772188966 +0100 +committer Test User 1772188966 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iLkEABMKAB0WIQTsiCaQTNePc9nPIlaMg3KiIK8bzwUCaaF1JgAKCRCMg3KiIK8b + z1sOAgkB1oCZKDZ9JVg8VASnxGOr9DBtMuPD3W0afvfjH41UDoSPERuiMvws+AkT + 2NmaqcADWIvTnKWUWmZbVTnypr76mCcCCQFHVhFbQ4BohfHZvEDoMctt07xHVfQg + Hzfjh1JagDgevjnOh1ekzluDamEzPNMCmaRM0gFbtMqamIOAED9U70R7yA== + =oq6z + -----END PGP SIGNATURE----- + +Test commit signed with ecdsa_p521 diff --git a/git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt b/git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt new file mode 100644 index 00000000..25d01d2f --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt @@ -0,0 +1,12 @@ +tree 7c5bd8f246ab8e8c6a5749c3d2f44018aa029fb8 +author Test User 1772188966 +0100 +committer Test User 1772188966 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iHUEABYKAB0WIQRnYqhdVzl5MeAXXPFaeBMjEbdVcgUCaaF1JgAKCRBaeBMjEbdV + cvFBAP9oqFkZXb3J8tGe8wcYoWBCtj1bIEnkOxdWJHqA7KHuiwD/Xe18Vu+IGMSV + xJUkStADGVvF+jlPQshn7C+cak6zWAQ= + =A7mH + -----END PGP SIGNATURE----- + +Test commit signed with ed25519 diff --git a/git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt b/git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt new file mode 100644 index 00000000..6080f343 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt @@ -0,0 +1,13 @@ +tree d49a4c033c2a0d7c2d5882461a0e70f61e021959 +author Test User 1772188966 +0100 +committer Test User 1772188966 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iKkFABYKACkiIQWoMJZGKPzTpyuUAJVTaO7/Ty5E4I2nu8xGwMU1m96nfAUCaaF1 + JgAA0yIByPwhpDW6dmJddCS/TsB2z2Wu30Vjd3wGLCp3J6N8FHsVi6jcmbPM2JXF + /uA7DZWryLM1Rgtsbcv9AAHGMTePYjyduBDw/uK7K3kgL0NnLHGHEQ1545mSmk4Z + Q8ltXviJqCQ4Ut549BoZxM8YbIieNmVWtiwA + =cJtf + -----END PGP SIGNATURE----- + +Test commit signed with ed448 diff --git a/git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt new file mode 100644 index 00000000..d696d92b --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt @@ -0,0 +1,16 @@ +tree e3ca2325bfa8013dca224a2f62f0582d70c07b12 +author Test User 1772188967 +0100 +committer Test User 1772188967 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iQEzBAABCAAdFiEEjxo8CPOWlAXK18ap+GK+ySN6ocgFAmmhdScACgkQ+GK+ySN6 + ocgm8wf7BOC9Jxv3QYTz+v9zztniu5phXYIF3Q1v7UuhVIK1uUj0F6OzIsdj7CHm + ryy4pVPHcOPq3Q6bPU7JlTHHfVdk+jzpv/K+SgjAqEdJHiH0FrSnNkXiA7+5jSxP + pJUPcnaeBr7I1jj+RM5uvAlHt7fTjrq6FZYqQuxrK80ICQ+YBz+5CHDm6OCSJGsR + xppNnGd3WkKkRJKInlXvd2eSStX4lffUihpo01JmN6XX9WfY1e3VDWokEpvIzyvJ + 269Kg4EEtmj5FBaAsMjalwF2ZmnfIClwo/zOCrir0QPQCX49F1CBwESTArOtI0/P + tUHIQ9zTWogzEQ0Ob2SyiEnRpEX8Ww== + =CK6f + -----END PGP SIGNATURE----- + +Test commit signed with rsa_2048 diff --git a/git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt b/git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt new file mode 100644 index 00000000..a983592e --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt @@ -0,0 +1,21 @@ +tree 596e4c43898dcf2a6aa08cb9c0f3e0bbb8ecc26d +author Test User 1772188967 +0100 +committer Test User 1772188967 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iQIzBAABCAAdFiEEXu1Q4zHdzEIbmc8LmBNHwX31dl4FAmmhdScACgkQmBNHwX31 + dl6pzA//RS+ffLDBShXdWCry8pmfIs4/Wkcz0oMlhetcpuErjCjOMI1ZEHao5J5G + +8l7UcMFq1JtpjQ156mFboJ1ZaAPmAAOAoGB+uJ20ncr/TXprOlc5pP6ssSIDsoU + n+zk+bONfIkdMQKdEcrAyOJPVuIFs7OvDY017n2kOTytCsWqxIWLgj/OrZCIyemd + EaumIoHCMkwAdfklWqba0v9OG7fw/knLFg8kvrjTZFmsi8GJcfdrCsqveS/sE3z5 + 2hEsleDavQ1FHTw0zOuN1y7E2CUXbMphQe+OxR6ypk53JQE4f0TsIYGItr9UQn7Y + tY1bYDiyJlTm6v/BRRl5J4qMgnNNsttjrl8cVihacYi1Gq6Mbl/vDYbZBLtWl9/7 + Bx8hPruqeZkix2nmA1lsFXAUDpumSERpjab3GjzzLW2hqIButodToD+3Jais01a/ + +JXsmZRvco3MjoLEKiSsM6BKp/FeWsH72A06/7JJ4i6LjFcJT8t1ljaSmNEZsQm2 + d10mHLQ34+9sgA35IaNFnF56XwZ9mX+NkLM9nTrtbaF/FHlzAd1k1HoNIT2NQ2tH + 5xydmyKJOkUEiaZXUIgsINI8RB5ERSCSJCXHk2G/N4ShT62jKqj3GmywWgKyGCpP + IQOUSxv6TZlZR2r5J1OIGzjZsFEWJyvq2u1vBG71uXnUOExt1k4= + =39uJ + -----END PGP SIGNATURE----- + +Test commit signed with rsa_4096 diff --git a/git/signatures/testdata/gpg_signatures/commit_unsigned.txt b/git/signatures/testdata/gpg_signatures/commit_unsigned.txt new file mode 100644 index 00000000..491a1441 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_unsigned.txt @@ -0,0 +1,5 @@ +tree 4650a2cda631bc795fc254fe20b598135b265036 +author Test User 1772188971 +0100 +committer Test User 1772188971 +0100 + +Test commit unsigned diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh new file mode 100755 index 00000000..896881f9 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -0,0 +1,244 @@ +#!/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 -e + +# Configuration variables +TEST_USER_NAME="Test User" +TEST_USER_EMAIL="sign-user@example.com" + +# Directory for temporary files +TEMP_DIR=$(mktemp -d) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +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" + +# 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 + git config user.name "$TEST_USER_NAME" + git config user.email "$TEST_USER_EMAIL" + 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 +create_unsigned_commit() { + echo "Creating unsigned commit..." + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_unsigned" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init + git config user.name "$TEST_USER_NAME" + git config user.email "$TEST_USER_EMAIL" + + # 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" + + cd "$SCRIPT_DIR" + echo " ✓ commit_unsigned.txt created" +} + +# Main program +main() { + 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" + + # Ed448 (less common) + generate_key "eddsa" "Ed448" "ed448" + + 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..." + echo "------------------------------------------" + + create_unsigned_commit + + echo "" + echo "=== Cleanup ===" + rm -rf "$TEMP_DIR" + echo "Temporary directory removed" + + 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 ")"}' +} + +main diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub b/git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub new file mode 100644 index 00000000..b08e1ba5 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mFMEaaF1IxMJKyQDAwIIAQEHAgMEUwknda08hsRC4Npdfcm+1YqDOomET8eB+jJ7 +42mryjwct/lIPxW9lNCcTsu+zw4inUSFie+ppyaUvs2Zn7NcR7QrVGVzdCBVc2Vy +IDx0ZXN0LWJyYWlucG9vbF9wMjU2QGV4YW1wbGUuY29tPoiTBBMTCAA7FiEEh7Sx +YlKSinoE4uEZTne3Hix4AAYFAmmhdSMCGyMFCwkIBwICIgIGFQoJCAsCBBYCAwEC +HgcCF4AACgkQTne3Hix4AAbAdgEApp8sXO9KUkVJBccanhxGOWM1V1u6wMSU4qP9 +maYLTl8A/22K8pAdmUEJNeFPnplgQL8If89hcOulaz9X7IXuX9R9 +=pSV1 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub b/git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub new file mode 100644 index 00000000..00344946 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub @@ -0,0 +1,12 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mHMEaaF1IxMJKyQDAwIIAQELAwMEVfXzvz+2tDtNqnttIWwaC2ErDVVrEY3GZZSr +BGnrvj+sy65ZzlrwuvnNTMAS1KbSPweRF90aZVkiyesNHtjIj//JoJETS2UYUJfP +D4vbhcVlhjUwuAIRA9Tv6UqXwdNVtCtUZXN0IFVzZXIgPHRlc3QtYnJhaW5wb29s +X3AzODRAZXhhbXBsZS5jb20+iLMEExMJADsWIQQ/Ad7FJfKxg1LGHLMZ238vxsg1 +ZQUCaaF1IwIbIwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRAZ238vxsg1 +ZZupAX9XXBzWAIUIax+3FzDyiaX52s9I7mReCvOhRUvR14JYMc/f5/CsebPZRw/4 +BFe0taoBfjJqSo0Y+qE/832yB/IuOEsLmSKeXvu8oncwSYQeRoOFBHKmsa+NFh35 +lvl/j9z8ng== +=0Q9w +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub b/git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub new file mode 100644 index 00000000..b4916c41 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mJMEaaF1IxMJKyQDAwIIAQENBAMEgOA+Jee2aD4ihETrDyd6nIeLNMi5/OoW8ChU +abrNn0A/JtViY0GIwSs8ZZbCWpbktU2cvi81yUOyPuXQNylxAVB+VJTLl2WG6/hm +iJytSnow5mx8jlMrjHralTHgmZ6vGA7113eBaw98uyQaTpW9L7/EnJZmIaWsOc7z +c0CTuNS0K1Rlc3QgVXNlciA8dGVzdC1icmFpbnBvb2xfcDUxMkBleGFtcGxlLmNv +bT6I0wQTEwoAOxYhBEWoduQf1y5nIiAZtyXSnvW9xokRBQJpoXUjAhsjBQsJCAcC +AiICBhUKCQgLAgQWAgMBAh4HAheAAAoJECXSnvW9xokRbhAB/3zbCx9UGG50fbqp +B1kSsRZTJXedRrBVb28l2WCD2M1RnNCEZsQiSbMzMCpjCUomlAHdcekSyIaQUQT2 +bsAnhfEB/j/xcqmLq+uYVlARylj3FdFNRPFMBk31VbmM4MmPGmKEK/Y2wfBA4t1Y +AsElpiiqqjE4h066r0Br0zyGmSH90aI= +=bACz +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub b/git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub new file mode 100644 index 00000000..3d692bd2 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mFIEaaF1IhMIKoZIzj0DAQcCAwQoQUw24pNLURN7niophK1FO8jxlaS8zIyXHrdk +v57m6jAzbdRsOgZ6q2RQ+mkzGpk+5W+Yv7oWit1On2NI5otNtCdUZXN0IFVzZXIg +PHRlc3QtZWNkc2FfcDI1NkBleGFtcGxlLmNvbT6IkwQTEwgAOxYhBBlhWylE7Hfl +1hw5X/01BEWvFUeLBQJpoXUiAhsjBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA +AAoJEP01BEWvFUeLX50BAO8aO89RwPhvh9AwK9d5p6JrAB1sMQifQa4qWLCxSoCc +AP9RhNEUOygsIPqEKUyZ+yhEcEMQP/5kd7ln52zaVmCIqw== +=SrKK +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub b/git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub new file mode 100644 index 00000000..bdd907f0 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub @@ -0,0 +1,11 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mG8EaaF1IhMFK4EEACIDAwSR0zvO7tXWhXmxDppSEiokEWqRZEy0wuRHJ+7P0o6F +8FDpuip3FkcBFaR47I7dwHIuQhg60pG/OMsuh72ZO0CndiPb4bpVK02ppY7QoE4A +JZNnETMeWEvn7nWdKsLbAvu0J1Rlc3QgVXNlciA8dGVzdC1lY2RzYV9wMzg0QGV4 +YW1wbGUuY29tPoizBBMTCQA7FiEEnMi8Yry1RiqJ8p8obnlTAyi+xAoFAmmhdSIC +GyMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQbnlTAyi+xAprwgGA2MZ2 +fe0jcm780LHpNFn+6skaR9eGKKVXg0gRu5169yLln6DHiXex3h0YNc6RPTveAX9i +cEo2z0sLtILQKIomGZqfqkXLgJPiT8qDZLZkElhM1CkmRWXPGgC96Twwuy/LGig= +=zVPo +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub b/git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub new file mode 100644 index 00000000..db963c3c --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mJMEaaF1IxMFK4EEACMEIwQABsj6GXczdoIybwVeCD4H1Bm4/kRA2oSJ8Q0eI8eI +eji8bwafKdEX+oqmW199cfJtwwM9NNe9vvfGvnANmfvhWeEAG9wz7UlhE4VUxgAo +hRYTwnZgBztiXGEjp/flr4y34Lz2IG33arxePBpzza72JyroVcfstYu7jY0KOa5s +NO7tDEO0J1Rlc3QgVXNlciA8dGVzdC1lY2RzYV9wNTIxQGV4YW1wbGUuY29tPojV +BBMTCgA7FiEE7IgmkEzXj3PZzyJWjINyoiCvG88FAmmhdSMCGyMFCwkIBwICIgIG +FQoJCAsCBBYCAwECHgcCF4AACgkQjINyoiCvG8/+7AIHRdZR45qP/DLcLR7BN9Mk +sjoDjUvd2swiVFXO5ZAhxu4/R/URkaSSTDW+a1QJjzSiwdKvVDeVBNNbNU9s2YVF +RFICB3ylAKmuOhs+upo5GqHJpdVgVI7AonTbnD7mlhhlvU5gbtGGO+ftCuZgCdsQ +ERV4BYsGGNM6FB3COlpKH8g+Jx0N +=ISNS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_ed25519.pub b/git/signatures/testdata/gpg_signatures/key_ed25519.pub new file mode 100644 index 00000000..6ba0bb53 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_ed25519.pub @@ -0,0 +1,9 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaaF1IxYJKwYBBAHaRw8BAQdARB9dMt7IgHVlZ1LKknKIc18Mp9P0ky1S5oAE +y+Ipvq60JFRlc3QgVXNlciA8dGVzdC1lZDI1NTE5QGV4YW1wbGUuY29tPoiTBBMW +CgA7FiEEZ2KoXVc5eTHgF1zxWngTIxG3VXIFAmmhdSMCGyMFCwkIBwICIgIGFQoJ +CAsCBBYCAwECHgcCF4AACgkQWngTIxG3VXKhNgD8DaeYgQWZUanENgua9f1sveQ5 +ceXJYo5wHKlNN5n0OpYBALLAg5Gg0Z2RzcSU3JKWh+F5KpJx9Xx+xA4GfuIZYgYG +=7VIr +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_ed448.pub b/git/signatures/testdata/gpg_signatures/key_ed448.pub new file mode 100644 index 00000000..1a008282 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_ed448.pub @@ -0,0 +1,11 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mEkFaaF1IxYAAAA/AytlcQHHVaag9xPUoWmV1JEqTCsKYnFQWm6PiaoTmLlDSIja +hH8gjdxTMzX7K+s9pI3Vxxdx1IdJ5kSumz0AtCJUZXN0IFVzZXIgPHRlc3QtZWQ0 +NDhAZXhhbXBsZS5jb20+iMcFExYKAEciIQWoMJZGKPzTpyuUAJVTaO7/Ty5E4I2n +u8xGwMU1m96nfAUCaaF1IwIbIwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAA +dbgByKYQcf0u88/60iuHN0mrkEQ1DenGhOmizKcrBpxLhHjEk+xQnuvA/tlEJVfZ +4lfWQO/sDJZMV013gAHIleJVkxDqXi+6UlXetODZRu3+kAGunWyzyU1XEjXbRCPh +l4jDOm9PF/GDfeqXWfzChIJZPQt/Uy8A +=VpqO +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_rsa_2048.pub b/git/signatures/testdata/gpg_signatures/key_rsa_2048.pub new file mode 100644 index 00000000..35309851 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_rsa_2048.pub @@ -0,0 +1,18 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGmhdR0BCADdMRJ9iHzeJanSzOhTqhONdUlgICL0+0FgOxTXIo7nhf3tCcfb +n9AhSVkiDX5ItDZzjHjeiZ66Frs4O4TP04x5Z8Ayxssx4J6ST/YeXm7vkTquigDs +Qes9uzIKp4aFTuGG9MXzuPtKQeWixebhtS217EUb4rZbSitafmuV/zeIR+4l5+g4 +H2YGsF9m1ElK1EiJuUozBZVjcJYQJ5elWJeWdqHr9oCjeFrnZRMJ/WaFrF0OpFXw +kZVseh50MZ0SZ43JzmlokZqZuMyhY2rq0rTsvD4IH+yV6sS4Gefc0jhijZcRzWpX +QIb/7WrAqPSMOfQeukapw90Ke1sKYEfwLmR5ABEBAAG0JVRlc3QgVXNlciA8dGVz +dC1yc2FfMjA0OEBleGFtcGxlLmNvbT6JAVIEEwEIADwWIQSPGjwI85aUBcrXxqn4 +Yr7JI3qhyAUCaaF1HQMbLwQFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ ++GK+ySN6ochAmwgAw7MwUF0mXbRQPTQNXp5tgOjBDSloQVoUw4f4Rs1N8XOW6Vvy +CnXwfCX8YHCtAMMs10mELY+iOG3GMdCqvRrImjJyh38JylLf/HQDigzL95tOy3cF +hZ3ZHm8m/H3w/zFDegI2QNMM4dCAdwGwUuxo42CoVMp5PzYtNy8l8WMkVXYLkJfm +wV6rM1rJazCAkY1Fk1FCW/LW8eenPr4rQa36VgmpT4hz+j9mi5mUM5RUdZGLXdPT +uuMcCpm2sfU1Lozx+6AeHng4LHTdQDWazXWLG2Ob1o0coG6zj2iVry04VnGFd/do +mvV/nK4AdBJ4Al/KKT1At/KmP5zVpnpJQdZq8w== +=f9Oy +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/key_rsa_4096.pub b/git/signatures/testdata/gpg_signatures/key_rsa_4096.pub new file mode 100644 index 00000000..d2356537 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_rsa_4096.pub @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGmhdR4BEADDKCJbXPeXZIU4l6NBJaLDL5Po4IGB+3+CbCPk90DFCXrowrBb +BT1SwCU7+bmYKINQprFgG6WqhhQ8rGwkxlTjKlTdKYpHF3I13ONuPYcs1KZiPtXA +6puY9ma5lOH6VSBnK+k+EnuvHBw7NmWtMQbMwSqnPO+PFnG1yaOxRQR641aKw7wI +ciR8hJdlakIy11Z+inWw0RzeK/54837ws05CBDaqRNiO4FQfJ+Q9bBPpYpVR1G7g +4GVtidWpwprJYALmR3ejkn0QAiSGOlR7tin+8x4FXufxaiVwrPcXJCEOXLdLSndV +4purALokhhm0wP3D9fOFU9nqvkhlENLL3pLlPLswRq158RbaMBKgRZ318RRX11yH +qKlO6s4NAoYlhBeEY/gXQ36trALtu3YTB/eZlqoaEFFgXfEoS02W2F0sqYyK/ISG +fvUuxWZWjATNmfNLr2L15aM/GmfpacN8JO2omyKWGJQ3WBcGRdxfBkJ03vOQIFDE +WvJ+XmKpY+XC/N0q16Sz8rIF5LzDxwAMHdG66uSbYHGGlKxbq5YnUw1ZMafRhvcp +epEFRhLHUMGmJrHqfkSKkcDclMFlKG+wm9F/8a8V8zINQ3J1ohaQblT1OkwioSyT +GnIk92sVD28dS9mnoJbEKHEPjcTp2B1VMntHidFE+v4zwb1TPRE5rtFOdQARAQAB +tCVUZXN0IFVzZXIgPHRlc3QtcnNhXzQwOTZAZXhhbXBsZS5jb20+iQJSBBMBCAA8 +FiEEXu1Q4zHdzEIbmc8LmBNHwX31dl4FAmmhdR4DGy8EBQsJCAcCAiICBhUKCQgL +AgQWAgMBAh4HAheAAAoJEJgTR8F99XZe6WYP/2ubM+Mc+cC61MZv755k82xL4t7i +qQWplqjsX4DYXyZmRjqaNp0vKr0A0C11hoosTIS213yoXt0To0grXTP15btu+Dfs +vo8R7oeUDG70UFhArP5vLAwcZRf5+ZV+HKKr4KuxlW2KKbHO5UQtIiv8Lf6NcU5v +K1lDRfQUxhauTb8lOEkt0eFbsobu4GU/M8c2uDDj9Z187Nvm/UrxiB0akrB95iDW +S8ol6+AwHCfrZALbwP1Lsd1hI1RRfT+OUysrK4//K3k4r/8nT0deIulxV1oZezPg +yRXrEHvsDbhV4ZQiSKDx+hwayeKO70ag5Ijl8I4m4Wuz7e5xn9bx2QGZEUsil6ff +oNLnn1p0gXkbKl18+cnla4tQqjRYV50s8FtocZ/ULXU/EOSsuTuvwC45Fd6XUiSx ++awz55iYaYrIQdyir4Ltedt+IvvIDfDZM53r/Xc5H1kixIHYzZ/xse2qremlhuro +fZePkcNQemhdzM5llTpq8AP1TfuT1BtPkrfohoWJGNjqmIm6rcPGFHmWLfvo8I5f +JyRvwz6ljovBywaxXojvrHdGa13ylsIZTUDgDMAG/6noUR80L8JmXz1lTZcbT+zh +5A++/Dg1u1p4TFzqkQmopVXe/ccns5YtBMW3EV85ctsg+dGnSh3jwW2QIAlwgfiA +XZlV0oFIlwpVXcBq +=QDQJ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt b/git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt new file mode 100644 index 00000000..f5429018 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt @@ -0,0 +1,13 @@ +object 9b70151dee2f47896dc875733450d2b81d22b5bd +type commit +tag test-tag-brainpool_p256 +tagger Test User 1772188967 +0100 + +Test tag signed with brainpool_p256 +-----BEGIN PGP SIGNATURE----- + +iHUEABMIAB0WIQSHtLFiUpKKegTi4RlOd7ceLHgABgUCaaF1JwAKCRBOd7ceLHgA +BqiIAQCT5NXXc2q8B5zF9qZMcuRxbV9sXzZnZcerDddzIyw3JAD/TQKfIbKZNGdv +lYE+mLhclLxPs6fzlFnr/PUUP+W28q8= +=VWLG +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt b/git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt new file mode 100644 index 00000000..90d96b40 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt @@ -0,0 +1,14 @@ +object 68211368f80d9087df5e0d9ec5e9f0f01d0f9251 +type commit +tag test-tag-brainpool_p384 +tagger Test User 1772188968 +0100 + +Test tag signed with brainpool_p384 +-----BEGIN PGP SIGNATURE----- + +iJUEABMJAB0WIQQ/Ad7FJfKxg1LGHLMZ238vxsg1ZQUCaaF1KAAKCRAZ238vxsg1 +ZUxdAX9Ymfjm35gtB0+cEXryF+10W2EBt8xYtw11BSfhwZ43qiHzw6GeNgGqGWf+ +Q+6aq/wBf0/1JmwcKWR6kko5TXcvU6SIjxg8JJxEzjFvUNKuhAu29QmK+bv+oW2I +kqg3pbWh9A== +=Yavx +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt b/git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt new file mode 100644 index 00000000..60815c75 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt @@ -0,0 +1,14 @@ +object b3156f0627e50dfa726e48dbf8e94adc6bdebf03 +type commit +tag test-tag-brainpool_p512 +tagger Test User 1772188968 +0100 + +Test tag signed with brainpool_p512 +-----BEGIN PGP SIGNATURE----- + +iLUEABMKAB0WIQRFqHbkH9cuZyIgGbcl0p71vcaJEQUCaaF1KAAKCRAl0p71vcaJ +ESzuAfkBoKFp7ZeomqTWBgHSkMRgzSup5vhlit8+RcH9b4pEy+kXCq8OjWEh45S6 +ACSbOwUGXPOb3azuUqDEaNu/RDEPAf0aJQv16PdYHKayxyV64UNn+dZvoTbmOVtr +cAOWxHe2rfix9yob9Rt497/hCUWFjxy3LLeIIsSEAARLXrmSokTE +=ZE3W +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt new file mode 100644 index 00000000..c21ef5bf --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt @@ -0,0 +1,13 @@ +object 9b386e46cb3c08a84860225689ebb0696874a288 +type commit +tag test-tag-ecdsa_p256 +tagger Test User 1772188969 +0100 + +Test tag signed with ecdsa_p256 +-----BEGIN PGP SIGNATURE----- + +iHUEABMIAB0WIQQZYVspROx35dYcOV/9NQRFrxVHiwUCaaF1KQAKCRD9NQRFrxVH +i55VAP97X6IxOp3ZxAvdof4h8weHE66FzmqdseCsvUeWHatRWgEAgt7H/Eg2kQUH +PRHHy4l+joi9tAAg9KClfvq/lA+VcxI= +=dPQQ +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt new file mode 100644 index 00000000..0340f25c --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt @@ -0,0 +1,14 @@ +object c87dc41b03d89ae26c1ebcc5ae34b816e915d76c +type commit +tag test-tag-ecdsa_p384 +tagger Test User 1772188969 +0100 + +Test tag signed with ecdsa_p384 +-----BEGIN PGP SIGNATURE----- + +iJUEABMJAB0WIQScyLxivLVGKonynyhueVMDKL7ECgUCaaF1KQAKCRBueVMDKL7E +ClbhAYCKIE4pMka3pHBjX4XmSvsq0El0DctONYNZgE15uRyIF/P+Oeonm3t9tF51 +XAkMS98BgMO27cmy6TMl1cnYBW34yrBmpLeHpctSk5pkxSddfhKAxj1aOLJHp6eu +/nFMr2HSow== +=l/X+ +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt new file mode 100644 index 00000000..c43adb5c --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt @@ -0,0 +1,14 @@ +object 08dadb22fa537a9efa99d565ff01fc5d5854e802 +type commit +tag test-tag-ecdsa_p521 +tagger Test User 1772188969 +0100 + +Test tag signed with ecdsa_p521 +-----BEGIN PGP SIGNATURE----- + +iLkEABMKAB0WIQTsiCaQTNePc9nPIlaMg3KiIK8bzwUCaaF1KQAKCRCMg3KiIK8b +z+HcAgkB8d27ZgMvPQ0ueTNeVnUtxJwu1zyXfVnoC9/cdeAU+D5yE/nEugwysds+ +/9aKjsLMV5v7gxTa6lg1dvGN2CdGEf4CCQEgnjuQkSgfaLmRmpsKPbGJoUDA1RJT +0zrv56m//eCOHFYJtcKFy95mNn5+9IiBWXrY3Ilz48jaQSg9CntzaITmCA== +=w0Ur +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt b/git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt new file mode 100644 index 00000000..3ab00c63 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt @@ -0,0 +1,13 @@ +object 35e58b202d6ba7a15f33b4e893b6da021c7132b7 +type commit +tag test-tag-ed25519 +tagger Test User 1772188970 +0100 + +Test tag signed with ed25519 +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQRnYqhdVzl5MeAXXPFaeBMjEbdVcgUCaaF1KgAKCRBaeBMjEbdV +cgc0AQDdONxRMTofNPtHP+BDEWsGFcDdyBGb9xxp5D5Xa3rYyQD/VLvlPmxl3jk5 +JUczWsHgXxcLWXP6e/N42Mf6ddU4lwg= +=Dt+S +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt b/git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt new file mode 100644 index 00000000..6b27d5dd --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt @@ -0,0 +1,14 @@ +object 594f42b8ecaad08227805a281ca4b053ff7fdae4 +type commit +tag test-tag-ed448 +tagger Test User 1772188970 +0100 + +Test tag signed with ed448 +-----BEGIN PGP SIGNATURE----- + +iKkFABYKACkiIQWoMJZGKPzTpyuUAJVTaO7/Ty5E4I2nu8xGwMU1m96nfAUCaaF1 +KgAAe94Bx0QXkRdhxyHoybrUWYIcs0ZFMhZRQa823NLlMtlPIVjUAieWGSJrVJyD +ZJbDprNIyLFRvEFYoYdEgAHDB+NuVFZQ+wvqwrQ6DryI4Azh6AorZCCxeHQ3dpm/ +o3KzUg0YxLeBptuESwPGyTqnTqublmc4xAMA +=axq3 +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt new file mode 100644 index 00000000..b2967d82 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt @@ -0,0 +1,17 @@ +object 2aa79d2bc04180cb05948619bcd3edb60703c214 +type commit +tag test-tag-rsa_2048 +tagger Test User 1772188970 +0100 + +Test tag signed with rsa_2048 +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEEjxo8CPOWlAXK18ap+GK+ySN6ocgFAmmhdSoACgkQ+GK+ySN6 +ocib9gf/ewYsCH6QEx6L3MAT5sJFlN2USLRCSeLTE9/l6Bm/h/DITK2xlkbADQOC +3Ct4IXjrXaWMJ+G2vTdvmdxDAvNkga/RpbkEPapedwVoYMRqVWgC4pF6+aZH6EF2 +omd7p7+er/HCmRfe+5NFwUOSsYAxt0yB2lZC5Mq3Vz99KLi0daUoHY+ymkzFE1kk +Hdu94PG/g4YLHFY7PP7EtOq3NH0HrCxombcU+n8rkqjquwH7rJk5ZYMSI5HcDD2l +qB6R0zRGDpwH9IiMmSpNNWpcRKqUmORLGCaeJdfhh++ZaEJdF7AkBQkGy4WV/A2k +Te0lLC3zVqsMB/9T3nzbklyWpweZsA== +=xjd+ +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt b/git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt new file mode 100644 index 00000000..4c5ae500 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt @@ -0,0 +1,22 @@ +object f75808dabeb766be8c47519fdea37ae4a0a6a613 +type commit +tag test-tag-rsa_4096 +tagger Test User 1772188971 +0100 + +Test tag signed with rsa_4096 +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEXu1Q4zHdzEIbmc8LmBNHwX31dl4FAmmhdSsACgkQmBNHwX31 +dl4gqxAAi/EifC0soQ6F5tj2EkKn4j9w7I5B505X2c2KUKWPUtGAMevwbeFFNLgn +S+kx/cl0xjkrrfv8mEWts9OPr2YRqMOojVKa5kBfqfSaZVEXJpic8Ocs2FhYbic+ +h7FQggtMNagkMKtqSw6qbXg9E3ZnZ/9iaF+EEHGNLdp6OSJEtpulidyOB1zPS5A3 +K7D1Y1Q+Z47v+x2ljwlAGjabZzkokwSIDScM1PyHCwoRmGeolzGjgyZFg/ROg8he +HpmxnDqS3uIzfjqFvusfYOO8aMJh9cir2KSsqzyc+basbciwwm/ChwXg93rpE7kc +sQWaWCBRCq4Z3VHL19Grl+BeqoSl2aeSgJn1hG2pYEDxbFe3ci6l8frgppcUXlhL +rMo5NaAZainHMge0lin3aZBenqH0GUzbaf4VtwzKVpnwWF/TGLcjNemnRn0Slfui +9w4tYQTiv6zNTwNBUG7YXgWs4jMgvLor5bbsTcZX6Zm3zvKDOGWPHX9UlQGFFBpB +W8zifKGES0KykcpJsGximwamoc5tjnuBSIUiFJVnGOT3uSONQRsSjX+CLiLrym/1 +k9V1OH92mW/1R8uW8ZjndOCmjNwKsLzU9hBg6MVaV+9gIbc37OTGMohLyEAn4mbk +8MuhIkSW8FsDedJCBhxbjMdCBV97cgffyHFu9FirchSAjbfQBZA= +=xzA8 +-----END PGP SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/README.md b/git/signatures/testdata/ssh_signatures/README.md new file mode 100644 index 00000000..1e57a520 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/README.md @@ -0,0 +1,195 @@ +# 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, authorized_keys files, verified signers 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. **Authorized Keys Files**: + - Individual files for each key type + - Combined file with all keys + +3. **Verified Signers Files** (with git namespace): + - Individual files for each key type + - Combined file with all keys + +4. **Signed Git Commits**: + - One signed commit for each key type + - All commits are verified using `git verify-commit` + +5. **Signed Git Tags**: + - One signed tag for each key type + - All tags are verified using `git verify-tag` + +6. **Unsigned Commit**: + - One unsigned commit 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 + +### Verified Signers Files +- `verified_signers_rsa` - RSA public key with git namespace +- `verified_signers_ecdsa_p256` - ECDSA P-256 public key with git namespace +- `verified_signers_ecdsa_p384` - ECDSA P-384 public key with git namespace +- `verified_signers_ecdsa_p521` - ECDSA P-521 public key with git namespace +- `verified_signers_ed25519` - ED25519 public key with git namespace +- `verified_signers_all` - All public keys with git namespace + +### 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 Commit +- `commit_unsigned.txt` - Unsigned commit 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/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt new file mode 100644 index 00000000..12ed29b5 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt @@ -0,0 +1,12 @@ +tree 2f0fa5393a2120151c5446eb34b99d1f3713ff12 +author Test User 1772153087 +0100 +committer Test User 1772153087 +0100 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE + EEvteyl/kGZEPuKkajhI0J+2PN66evLXOeZTvxGFxU5jAs0JHkxWbbY31zVphpwjEeaL9P + GQ1N1B0QHx13iZ8DhAAAAANnaXQAAAAAAAAABnNoYTUxMgAAAGUAAAATZWNkc2Etc2hhMi + 1uaXN0cDI1NgAAAEoAAAAhAPQhsSXLRif71JKQ1QN9z79VfPHTOeKKAhpplCh5VY5/AAAA + IQDZBEQLxlx8YuKNFC3c2pZ6oS0Ry8MkkkpgZio9gsDl3w== + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p256 diff --git a/git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt new file mode 100644 index 00000000..860fd0f2 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt @@ -0,0 +1,13 @@ +tree ff58328bd5797f45f6f300c6c39d2cd357b9f3cd +author Test User 1772153088 +0100 +committer Test User 1772153088 +0100 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG + EEpD1Slvc9rtvk1ZujObbQ+qkVzlZkIIIGVf354UQsCMp0HN7YRtNMq/H1iyQonw9YsTwP + 3DbSyMOK83B9SOiJkaBslBwkpwo+u2i85g+/QkqmjJnQ+4umr2SNJFNGdKETAAAAA2dpdA + AAAAAAAAAGc2hhNTEyAAAAhQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAagAAADEA1fuA + 0MeTI0m7DUxVP/EIRljC3Y6L9ElAU7Sqv5HXcOKVCxPYnZYuOrWgbnk+IhD4AAAAMQC+qA + zQUSgM0KFWFRPoxWUYo2gODfyizXdJqWIazjri9IlFZE/1eDZH8M32Ron3UII= + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p384 diff --git a/git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt new file mode 100644 index 00000000..fcc8de7e --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt @@ -0,0 +1,15 @@ +tree 63af4f62a108a6c684181a4488b4bd3a5b51dc8e +author Test User 1772153088 +0100 +committer Test User 1772153088 +0100 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAKwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAI + UEAZJj44ARus1InhAPo2AkglBXySaOqL4GF94AC2ES/R4KrUIAOsKoq3SmjEJqFg0JMwuU + y+pbvEHDrAMHSRXT/gJPAFf0dF+0VSlplqc+1+8w2E9P8IMytOw1LOD8ffYe79+68vDI9D + QnNFeB/6qKrc5nirRWMRFTsvXdQOjPgWAckh5VAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAAA + pgAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAAiwAAAEIAqSn31cfI4XZEhgnOPL5BJ42jbD + G9nC/F0n94PJPLL1Y2aq9uFT69diEuTTYYFEzuJkk0CZdTCCDSi7Lbg2l3g4IAAABBDYLv + jKD5wuPhyt1tvLaTPNBIElMbkOULaLgespZHEbrgEh0KYNQXphnTgyF3lnuMBiPGqgDUW7 + 7TkSxoBDsI4D0= + -----END SSH SIGNATURE----- + +Test commit signed with ecdsa_p521 diff --git a/git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt b/git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt new file mode 100644 index 00000000..4f6da87f --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt @@ -0,0 +1,11 @@ +tree 7c5bd8f246ab8e8c6a5749c3d2f44018aa029fb8 +author Test User 1772153088 +0100 +committer Test User 1772153088 +0100 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgHRfgHA3PrVXK+vIj9qrm9Rz19k + rWdqNjpYJJ3HOkstYAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQFOFBI8DavCfBEiobPbMvmFO5gcAzy1BLwKfo4djvxbhDYi74cg7Bejqqcv7NakDNL + rKJYnzrfnNIIk6GDmC7QY= + -----END SSH SIGNATURE----- + +Test commit signed with ed25519 diff --git a/git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt b/git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt new file mode 100644 index 00000000..ca23e48e --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt @@ -0,0 +1,29 @@ +tree 1207106d0fef65cd05d7a8428fc871886a36fa78 +author Test User 1772153087 +0100 +committer Test User 1772153087 +0100 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAKmt6I77xewHjFcY2bC47j + xrtY+CFvmKEIk0/JBmmdo9+rq+E1VZKCrwAiMGYk4lijgdnKIeqlLg4FzzqCWTqoy/xgdG + 3hVFUE/4OM8sMiw5Hv7YcGU48cybyVMOL6Iw8cEPGoXLuZIMHj6/ufvTT7j29iFaNkml6y + ecTomiK3FJWGaqnvmN41E1To8PTTP6AxRy+K/xQ6z8CmDULDl/7hP3I6eOU4doUf8G629n + S3ZUXRyzby18K4sCi6aOKd7kabq0JFCVk6hqk0nO+dhP7zp/88RT/iQNh/fBPtL42dQN8K + wikNWU5c++OdD8O52GoSede99yH54EIjuu0kEcgY8oV2YLhxRE5rRQMZHqj8nEu3HhlNuQ + amroxXB2tvuon46JvVTzFWKZYV9quSbt15VdYPfAHlZrwqj9r9a5h8TeBhGFvyJc9h3vUW + wTLgXjUkIxkNwioyJBF7d7aC9j7Pax/TaQc4V5YmasBj0UWM8vzlUPQOD76OsTGQYWNwFP + D2BQbk48anCpD1Yc7wTwM/Nr6Lkn/C7gM4PIusvG5cc95JhSNy+HmWfw/vSov4ivCfWaCf + Y+QhlbRcI8G3ojKWDnm3mx/LLTc/QqZvhTdOumkx6KDsuv0sNTgOiHsWPqgtgMYVRX4XnZ + JK4+zlJ+tZ3oCmLP5U+OwCHlu088k2XNAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAACFAAAAA + xyc2Etc2hhMi01MTIAAAIAnpVTEIaHs0ngRHSOk3oxEBHmZd/A0uMCznRjHNHDgHW8qa09 + qvII0n1RQI8Q0Wi8XvZsQqxXJ9/8nzfsrss1qDg8w4UnggnBYVnH/mgUIjw0tWxdoAv5Ga + BLfMOu+6gOp7YaqFYHe4RwtR/M2nCXbtnsEVrzLWKSBUaRI+TZHzExLJ4o6NpgJLMRhwpp + d8sGT6LuH/P08psOu9jCASksODcbWerAx+LfLcDIXje+WLzqu4Mn/HqZncMyf28bXJHcoq + X2ZWPHjZuRbcr9EeLdkHCDyD1kb7wAzR2Mpma9W99ZtpIXkugDSlbNQOyDGqB/b7t+I6Er + Sm/FL+1m3+pBnOxORpaxSkqFMlbWou4SNmYjSVU0XltxTpTV27svt0Lapmu3CpAptp3kx+ + 0Gd1y4QWyc2f38NPpConekGFKS/4O16zyGtFAUY5p4UCa/YUmC/H5QDskgv/MtZ/N+3RAr + EAlPOpTKM7876puPPd9tyEj9Tax5uNT7C039gyER/+B/eGWGcK08bq/YLfdgbmi51hrehd + DK3Z5wDfvIXci2rO0A/MB/HC1c75urX95uiHQV9pglQ+8zkrYeL/fD9+COaxqPJru+hdT0 + qlUJGIil/VBUTvu9PbsyyZA8UvPpFJRyGreNByyBxfhu33o2jx08OB9AgoctJ1tEgWJrty + fY/nQ= + -----END SSH SIGNATURE----- + +Test commit signed with rsa diff --git a/git/signatures/testdata/ssh_signatures/commit_unsigned.txt b/git/signatures/testdata/ssh_signatures/commit_unsigned.txt new file mode 100644 index 00000000..84dc4222 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/commit_unsigned.txt @@ -0,0 +1,5 @@ +tree 4650a2cda631bc795fc254fe20b598135b265036 +author Test User 1772153090 +0100 +committer Test User 1772153090 +0100 + +Test commit unsigned diff --git a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh new file mode 100755 index 00000000..b14574bd --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -0,0 +1,271 @@ +#!/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 -e + +# Configuration variables +TEST_USER_NAME="Test User" +TEST_USER_EMAIL="sign-user@example.com" + +# Directory for temporary files +TEMP_DIR=$(mktemp -d) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== SSH Signature Test Fixtures Generator ===" +echo "Temporary directory: $TEMP_DIR" +echo "Output directory: $SCRIPT_DIR" +echo "" + +# 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="$SCRIPT_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 combined verified signers file +create_combined_verified_signers() { + local output_file="$SCRIPT_DIR/verified_signers_all" + + echo "Creating combined verified signers..." + + # Combine all public keys with git namespace + { + echo "$TEST_USER_EMAIL namespaces=\"git\" $(cat "$TEMP_DIR/rsa.pub")" + echo "$TEST_USER_EMAIL namespaces=\"git\" $(cat "$TEMP_DIR/ecdsa_p256.pub")" + echo "$TEST_USER_EMAIL namespaces=\"git\" $(cat "$TEMP_DIR/ecdsa_p384.pub")" + echo "$TEST_USER_EMAIL namespaces=\"git\" $(cat "$TEMP_DIR/ecdsa_p521.pub")" + echo "$TEST_USER_EMAIL namespaces=\"git\" $(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 key_type=$3 + local verified_signers_file="$SCRIPT_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 + git config user.name "$TEST_USER_NAME" + git config user.email "$TEST_USER_EMAIL" + 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 +create_unsigned_commit() { + local commit_file="$SCRIPT_DIR/commit_unsigned.txt" + + echo "Creating unsigned commit..." + + # Create temporary Git repository + local repo_dir="$TEMP_DIR/repo_unsigned" + mkdir -p "$repo_dir" + cd "$repo_dir" + + git init + git config user.name "$TEST_USER_NAME" + git config user.email "$TEST_USER_EMAIL" + + # 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" + + cd "$SCRIPT_DIR" + echo " ✓ $commit_file created" +} + +# Main program +main() { + 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" + + # Combined verified signers file + create_combined_verified_signers + + echo "" + echo "Step 4: Create signed commits..." + echo "----------------------------------------" + + # Signed commits for each key type + create_signed_object "commit" "rsa" "rsa" + create_signed_object "commit" "ecdsa_p256" "ecdsa" + create_signed_object "commit" "ecdsa_p384" "ecdsa" + create_signed_object "commit" "ecdsa_p521" "ecdsa" + create_signed_object "commit" "ed25519" "ed25519" + + echo "" + echo "Step 5: Create signed tags..." + echo "-------------------------------------" + + # Signed tags for each key type + create_signed_object "tag" "rsa" "rsa" + create_signed_object "tag" "ecdsa_p256" "ecdsa" + create_signed_object "tag" "ecdsa_p384" "ecdsa" + create_signed_object "tag" "ecdsa_p521" "ecdsa" + create_signed_object "tag" "ed25519" "ed25519" + + echo "" + echo "Step 6: Create unsigned commit..." + echo "------------------------------------------" + + create_unsigned_commit + + echo "" + echo "=== Cleanup ===" + rm -rf "$TEMP_DIR" + echo "Temporary directory removed" + + 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 ")"}' +} + +# Run script +main "$@" \ No newline at end of file diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub b/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub new file mode 100644 index 00000000..7364a9a2 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint b/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint new file mode 100644 index 00000000..f62198c0 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint @@ -0,0 +1 @@ +SHA256:oU8IT7UOnJlOTOvr/W1cYf1SkdocFm5F7SAXOwuo8Kc diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub new file mode 100644 index 00000000..aabefb80 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint new file mode 100644 index 00000000..ee5243a3 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint @@ -0,0 +1 @@ +SHA256:+vwrYGpHfAAWIzT2x+uV+duJG7ZnSvCbRKwdPApx7JA diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub new file mode 100644 index 00000000..82d92898 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint new file mode 100644 index 00000000..34f471ec --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint @@ -0,0 +1 @@ +SHA256:3FcWgX5RsACruglrcBJP/hefUZcYHJGnrk07U6yKin8 diff --git a/git/signatures/testdata/ssh_signatures/key_ed25519.pub b/git/signatures/testdata/ssh_signatures/key_ed25519.pub new file mode 100644 index 00000000..8f745c47 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint b/git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint new file mode 100644 index 00000000..1ccdda31 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint @@ -0,0 +1 @@ +SHA256:eNi885YLo10DYWUdJOAs+CeXcDLX7X+Aqg2PprKFE3A diff --git a/git/signatures/testdata/ssh_signatures/key_rsa.pub b/git/signatures/testdata/ssh_signatures/key_rsa.pub new file mode 100644 index 00000000..b02a4d38 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_rsa.pub @@ -0,0 +1 @@ +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 diff --git a/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint b/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint new file mode 100644 index 00000000..060a2c80 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint @@ -0,0 +1 @@ +SHA256:TxoYgaeIj5A7Md4rHNfxPdqawooc4NIGjIMbcQ7YKbw diff --git a/git/signatures/testdata/ssh_signatures/keys_all.pub b/git/signatures/testdata/ssh_signatures/keys_all.pub new file mode 100644 index 00000000..2a587db7 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/keys_all.pub @@ -0,0 +1,5 @@ +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 +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt new file mode 100644 index 00000000..a20933c0 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt @@ -0,0 +1,13 @@ +object a9ce559c0acfc9268bdd854dec51d77ead112ab5 +type commit +tag test-tag-ecdsa_p256 +tagger Test User 1772153089 +0100 + +Test tag signed with ecdsa_p256 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE +EEvteyl/kGZEPuKkajhI0J+2PN66evLXOeZTvxGFxU5jAs0JHkxWbbY31zVphpwjEeaL9P +GQ1N1B0QHx13iZ8DhAAAAANnaXQAAAAAAAAABnNoYTUxMgAAAGUAAAATZWNkc2Etc2hhMi +1uaXN0cDI1NgAAAEoAAAAhAOQrMY08WBF4tTiUz3vq48VoKjvjOR9y75YzhMShbmGEAAAA +IQCF2ZvBxS6o/sZuRRw6HrFNryg2PU4ambnsRlC2cqOgfA== +-----END SSH SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt new file mode 100644 index 00000000..00218038 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt @@ -0,0 +1,14 @@ +object 095e9cde03a267af2c9ef62cf4868b126994714a +type commit +tag test-tag-ecdsa_p384 +tagger Test User 1772153089 +0100 + +Test tag signed with ecdsa_p384 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG +EEpD1Slvc9rtvk1ZujObbQ+qkVzlZkIIIGVf354UQsCMp0HN7YRtNMq/H1iyQonw9YsTwP +3DbSyMOK83B9SOiJkaBslBwkpwo+u2i85g+/QkqmjJnQ+4umr2SNJFNGdKETAAAAA2dpdA +AAAAAAAAAGc2hhNTEyAAAAgwAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAaAAAADA35l3E +HiF5ajYffjQRjxx37o8DG0eZIwDGtM2suBElqRKPrv2lNXUAZIFOt60X7EgAAAAwSE8BAK +DzSrdmwWwGIdsURzNrb0ziNQG5TJUI6oexNNGqP+JvZeGSJpSsS/PtRJyq +-----END SSH SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt new file mode 100644 index 00000000..48690d84 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt @@ -0,0 +1,16 @@ +object f98d104240f097f9912d3dd654710a4ea9710a0d +type commit +tag test-tag-ecdsa_p521 +tagger Test User 1772153090 +0100 + +Test tag signed with ecdsa_p521 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAKwAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQAAAI +UEAZJj44ARus1InhAPo2AkglBXySaOqL4GF94AC2ES/R4KrUIAOsKoq3SmjEJqFg0JMwuU +y+pbvEHDrAMHSRXT/gJPAFf0dF+0VSlplqc+1+8w2E9P8IMytOw1LOD8ffYe79+68vDI9D +QnNFeB/6qKrc5nirRWMRFTsvXdQOjPgWAckh5VAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAAA +pwAAABNlY2RzYS1zaGEyLW5pc3RwNTIxAAAAjAAAAEIBZnxQJm8pVQbZLRVgFzBa6mKgyo +Ndyi4pEsccUjrIVxkHV+choqQaLBv0hiLNx9pj7a4ZXCNxxTO0XO4LY5OMP40AAABCAROG +/LBErKEWKIFHOMYwPdaCEPUtimfYwAH6rBUhAFJdeDwm9WHoU2XcXO2Ca6+LCNQGTRBZSu +UxOfXY4xBKbaf2 +-----END SSH SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt b/git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt new file mode 100644 index 00000000..4b811e55 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt @@ -0,0 +1,12 @@ +object 04285f60c0dcb310174dccae49f08475981aba2c +type commit +tag test-tag-ed25519 +tagger Test User 1772153090 +0100 + +Test tag signed with ed25519 +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgHRfgHA3PrVXK+vIj9qrm9Rz19k +rWdqNjpYJJ3HOkstYAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQH9s7JZFHLzLGztuessbqdQwofdi/4WLeBnaRXdxy0g5WTLOUxENnJtLYcdKKowJBs +xS/FE43Cfu3YGmXAsSWwk= +-----END SSH SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt b/git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt new file mode 100644 index 00000000..3882a4fa --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt @@ -0,0 +1,30 @@ +object 76abbeedee42f812d7fa2cdf0545f9cc13ae0463 +type commit +tag test-tag-rsa +tagger Test User 1772153089 +0100 + +Test tag signed with rsa +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAKmt6I77xewHjFcY2bC47j +xrtY+CFvmKEIk0/JBmmdo9+rq+E1VZKCrwAiMGYk4lijgdnKIeqlLg4FzzqCWTqoy/xgdG +3hVFUE/4OM8sMiw5Hv7YcGU48cybyVMOL6Iw8cEPGoXLuZIMHj6/ufvTT7j29iFaNkml6y +ecTomiK3FJWGaqnvmN41E1To8PTTP6AxRy+K/xQ6z8CmDULDl/7hP3I6eOU4doUf8G629n +S3ZUXRyzby18K4sCi6aOKd7kabq0JFCVk6hqk0nO+dhP7zp/88RT/iQNh/fBPtL42dQN8K +wikNWU5c++OdD8O52GoSede99yH54EIjuu0kEcgY8oV2YLhxRE5rRQMZHqj8nEu3HhlNuQ +amroxXB2tvuon46JvVTzFWKZYV9quSbt15VdYPfAHlZrwqj9r9a5h8TeBhGFvyJc9h3vUW +wTLgXjUkIxkNwioyJBF7d7aC9j7Pax/TaQc4V5YmasBj0UWM8vzlUPQOD76OsTGQYWNwFP +D2BQbk48anCpD1Yc7wTwM/Nr6Lkn/C7gM4PIusvG5cc95JhSNy+HmWfw/vSov4ivCfWaCf +Y+QhlbRcI8G3ojKWDnm3mx/LLTc/QqZvhTdOumkx6KDsuv0sNTgOiHsWPqgtgMYVRX4XnZ +JK4+zlJ+tZ3oCmLP5U+OwCHlu088k2XNAAAAA2dpdAAAAAAAAAAGc2hhNTEyAAACFAAAAA +xyc2Etc2hhMi01MTIAAAIAUtTbcgmerQKpLoDxALJWACnkNDtKFagkZFMmFUo8vpAp5Zyz +jbC9uo6Oql/JwVNoAI+pm4/gxRDAKRGU1abki5Ge998m/FSkKi6ka0E4qwuNZkd9dOHAqt +kKeFhwIp0xiWCDF8s3iPpraaJbEfuGkGsAyAVNgfR0W8hu0wOOj8uwHuVZj7LeNLS3/jEu +bHwWhmzWCT0IPhFdkegDJMJ4XXgjxfsgGCXUahUfNZgOCXBfEBQkhHNoTq55+8DVqZ47hK +nRGjAZTVTnIxZhJqvaCHErse5A2jBJs2QfzmAIJhNAlDKmeHdWGDUADGxk5U7gD5IK5j/A +lBWp/ruXVqc7gwRKwQc7muu0Kzwa0yw8pBGi+8Y089a0M8Ti0cci57koXD8tPBLgz66710 +zLe9xkAZFvwxurHcgf01POvlCf6KGamCTNRsncnaUKfTvZVSOXHPeurNVlEpsDPZAsJ6wI +hFc0Y/RvLbTMlCxA6/brvr+peYSKmnCXJO+SXgZkN0QoKrq27RvPwB/2j1sNGgKpftfxUK +ymhPPKlzXGgrSDkBLhcaqGI1+5J3qjN0qLRGjwpgvkuM2JFLUaFVk1w8EvU19yMjmYkddn +AdZEB0xiAu1vZEEw9jhaTWW2R1qQ3ftf1+D8iXm+t1I3HlrSOBcVGzDi0x66a62K0bmXXy +AIRCY= +-----END SSH SIGNATURE----- diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_all b/git/signatures/testdata/ssh_signatures/verified_signers_all new file mode 100644 index 00000000..40c5d87a --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_all @@ -0,0 +1,5 @@ +sign-user@example.com namespaces="git" 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 +sign-user@example.com namespaces="git" ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com +sign-user@example.com namespaces="git" ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com +sign-user@example.com namespaces="git" ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com +sign-user@example.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 new file mode 100644 index 00000000..c776e628 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 @@ -0,0 +1 @@ +sign-user@example.com namespaces="git" ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 new file mode 100644 index 00000000..ef4a2016 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 @@ -0,0 +1 @@ +sign-user@example.com namespaces="git" ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 new file mode 100644 index 00000000..91f0e557 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 @@ -0,0 +1 @@ +sign-user@example.com namespaces="git" ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ed25519 b/git/signatures/testdata/ssh_signatures/verified_signers_ed25519 new file mode 100644 index 00000000..ce518692 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_ed25519 @@ -0,0 +1 @@ +sign-user@example.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_rsa b/git/signatures/testdata/ssh_signatures/verified_signers_rsa new file mode 100644 index 00000000..0e845f6f --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/verified_signers_rsa @@ -0,0 +1 @@ +sign-user@example.com namespaces="git" 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 diff --git a/git/testutils/fixtures.go b/git/testutils/fixtures.go new file mode 100644 index 00000000..da5327f2 --- /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 38c86b79..d8d727d0 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -113,6 +113,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 205eb8f6..93acdd01 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -209,6 +209,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=