From 71afb88d64bba94d9238d30f0823cdb558e82aef Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Thu, 26 Feb 2026 22:59:11 +0100 Subject: [PATCH 01/16] adds SSH signature validation for git commits - adds new package git/signatures - adds validation of SSH signed commits to ssh_signature.go - moves GPG signature validation to gpg_signature.go - adds text fixtures for all SSH and GPG key types including commits and signatures - adds tests for all key/signature combinations - adds wrapper for "Verify(keyRings ...string)" function Signed-off-by: Ricardo Bartels --- git/git.go | 94 +- git/git_test.go | 397 +++-- git/go.mod | 8 + git/go.sum | 4 + git/gogit/clone.go | 14 +- git/signatures/gpg_signature.go | 50 + git/signatures/gpg_signature_test.go | 403 ++++++ git/signatures/signature.go | 64 + git/signatures/signature_test.go | 169 +++ git/signatures/ssh_signature.go | 106 ++ git/signatures/ssh_signature_test.go | 1286 +++++++++++++++++ .../testdata/gpg_signatures/README.md | 399 +++++ .../commit_brainpool_p256_signed.txt | 12 + .../commit_brainpool_p384_signed.txt | 13 + .../commit_brainpool_p512_signed.txt | 13 + .../gpg_signatures/commit_dsa_2048_signed.txt | 12 + .../commit_ecdsa_p256_signed.txt | 12 + .../commit_ecdsa_p384_signed.txt | 13 + .../commit_ecdsa_p521_signed.txt | 13 + .../gpg_signatures/commit_ed25519_signed.txt | 12 + .../gpg_signatures/commit_ed448_signed.txt | 13 + .../gpg_signatures/commit_rsa_2048_signed.txt | 16 + .../gpg_signatures/commit_rsa_4096_signed.txt | 21 + .../gpg_signatures/commit_unsigned.txt | 5 + .../gpg_signatures/generate_gpg_fixtures.sh | 245 ++++ .../gpg_signatures/key_brainpool_p256.pub | 10 + .../gpg_signatures/key_brainpool_p384.pub | 12 + .../gpg_signatures/key_brainpool_p512.pub | 13 + .../testdata/gpg_signatures/key_dsa_2048.pub | 25 + .../gpg_signatures/key_ecdsa_p256.pub | 10 + .../gpg_signatures/key_ecdsa_p384.pub | 11 + .../gpg_signatures/key_ecdsa_p521.pub | 13 + .../testdata/gpg_signatures/key_ed25519.pub | 9 + .../testdata/gpg_signatures/key_ed448.pub | 11 + .../testdata/gpg_signatures/key_rsa_2048.pub | 18 + .../testdata/gpg_signatures/key_rsa_4096.pub | 29 + .../tag_brainpool_p256_signed.txt | 13 + .../tag_brainpool_p384_signed.txt | 14 + .../tag_brainpool_p512_signed.txt | 14 + .../gpg_signatures/tag_dsa_2048_signed.txt | 13 + .../gpg_signatures/tag_ecdsa_p256_signed.txt | 13 + .../gpg_signatures/tag_ecdsa_p384_signed.txt | 14 + .../gpg_signatures/tag_ecdsa_p521_signed.txt | 14 + .../gpg_signatures/tag_ed25519_signed.txt | 13 + .../gpg_signatures/tag_ed448_signed.txt | 14 + .../gpg_signatures/tag_rsa_2048_signed.txt | 17 + .../gpg_signatures/tag_rsa_4096_signed.txt | 22 + .../testdata/ssh_signatures/README.md | 202 +++ .../ssh_signatures/authorized_keys_all | 5 + .../ssh_signatures/authorized_keys_ecdsa_p256 | 1 + .../ssh_signatures/authorized_keys_ecdsa_p384 | 1 + .../ssh_signatures/authorized_keys_ecdsa_p521 | 1 + .../ssh_signatures/authorized_keys_ed25519 | 1 + .../ssh_signatures/authorized_keys_rsa | 1 + .../commit_ecdsa_p256_signed.txt | 12 + .../commit_ecdsa_p384_signed.txt | 13 + .../commit_ecdsa_p521_signed.txt | 15 + .../ssh_signatures/commit_ed25519_signed.txt | 11 + .../ssh_signatures/commit_rsa_signed.txt | 29 + .../ssh_signatures/commit_unsigned.txt | 5 + .../ssh_signatures/generate_ssh_fixtures.sh | 286 ++++ .../ssh_signatures/key_ecdsa_p256.pub | 1 + .../ssh_signatures/key_ecdsa_p384.pub | 1 + .../ssh_signatures/key_ecdsa_p521.pub | 1 + .../testdata/ssh_signatures/key_ed25519.pub | 1 + .../testdata/ssh_signatures/key_rsa.pub | 1 + .../ssh_signatures/tag_ecdsa_p256_signed.txt | 13 + .../ssh_signatures/tag_ecdsa_p384_signed.txt | 14 + .../ssh_signatures/tag_ecdsa_p521_signed.txt | 16 + .../ssh_signatures/tag_ed25519_signed.txt | 12 + .../ssh_signatures/tag_rsa_signed.txt | 30 + .../ssh_signatures/verified_signers_all | 5 + .../verified_signers_ecdsa_p256 | 1 + .../verified_signers_ecdsa_p384 | 1 + .../verified_signers_ecdsa_p521 | 1 + .../ssh_signatures/verified_signers_ed25519 | 1 + .../ssh_signatures/verified_signers_rsa | 1 + git/signatures/testutils_test.go | 70 + 78 files changed, 4289 insertions(+), 180 deletions(-) create mode 100644 git/signatures/gpg_signature.go create mode 100644 git/signatures/gpg_signature_test.go create mode 100644 git/signatures/signature.go create mode 100644 git/signatures/signature_test.go create mode 100644 git/signatures/ssh_signature.go create mode 100644 git/signatures/ssh_signature_test.go create mode 100644 git/signatures/testdata/gpg_signatures/README.md create mode 100644 git/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/commit_unsigned.txt create mode 100755 git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh create mode 100644 git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_dsa_2048.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_ed25519.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_ed448.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_rsa_2048.pub create mode 100644 git/signatures/testdata/gpg_signatures/key_rsa_4096.pub create mode 100644 git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt create mode 100644 git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/README.md create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_all create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 create mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_rsa create mode 100644 git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/commit_unsigned.txt create mode 100755 git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub create mode 100644 git/signatures/testdata/ssh_signatures/key_ed25519.pub create mode 100644 git/signatures/testdata/ssh_signatures/key_rsa.pub create mode 100644 git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_all create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_ed25519 create mode 100644 git/signatures/testdata/ssh_signatures/verified_signers_rsa create mode 100644 git/signatures/testutils_test.go diff --git a/git/git.go b/git/git.go index 1bff7cfb2..d6344eaa8 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,38 @@ func (c *Commit) AbsoluteReference() string { return c.Hash.Digest() } +// 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) { + // The Encoded field already contains the commit data without the signature + // (it was encoded using EncodeWithoutSignature in BuildCommitWithRef) + 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 +170,34 @@ type Tag struct { Message string } +// 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) { + // The Encoded field already contains the tag data without the signature + // (it was encoded using EncodeWithoutSignature in BuildCommitWithRef) + 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 +245,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 1c18d61f5..8fb93fd06 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -23,102 +23,6 @@ import ( . "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 TestHash_Algorithm(t *testing.T) { tests := []struct { name string @@ -155,61 +59,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 +252,249 @@ func TestIsConcreteCommit(t *testing.T) { }) } } + +func TestCommit_IsPGPSigned(t *testing.T) { + tests := []struct { + name string + commit *Commit + want bool + }{ + { + name: "PGP signed commit", + commit: &Commit{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: true, + }, + { + name: "SSH signed commit", + commit: &Commit{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: false, + }, + { + name: "unsigned commit", + commit: &Commit{}, + want: false, + }, + { + name: "PGP signed commit with leading whitespace", + commit: &Commit{ + Signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.commit.IsPGPSigned()).To(Equal(tt.want)) + }) + } +} + +func TestCommit_IsSSHSigned(t *testing.T) { + tests := []struct { + name string + commit *Commit + want bool + }{ + { + name: "SSH signed commit", + commit: &Commit{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: true, + }, + { + name: "PGP signed commit", + commit: &Commit{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: false, + }, + { + name: "unsigned commit", + commit: &Commit{}, + want: false, + }, + { + name: "SSH signed commit with leading whitespace", + commit: &Commit{ + Signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.commit.IsSSHSigned()).To(Equal(tt.want)) + }) + } +} + +func TestCommit_SignatureType(t *testing.T) { + tests := []struct { + name string + commit *Commit + want string + }{ + { + name: "PGP signed commit", + commit: &Commit{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: "pgp", + }, + { + name: "SSH signed commit", + commit: &Commit{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: "ssh", + }, + { + name: "unsigned commit", + commit: &Commit{}, + want: "unknown", + }, + { + name: "unknown signature type", + commit: &Commit{ + Signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + }, + 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)) + }) + } +} + +func TestTag_IsPGPSigned(t *testing.T) { + tests := []struct { + name string + tag *Tag + want bool + }{ + { + name: "PGP signed tag", + tag: &Tag{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: true, + }, + { + name: "SSH signed tag", + tag: &Tag{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: false, + }, + { + name: "unsigned tag", + tag: &Tag{}, + want: false, + }, + { + name: "PGP signed tag with leading whitespace", + tag: &Tag{ + Signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.tag.IsPGPSigned()).To(Equal(tt.want)) + }) + } +} + +func TestTag_IsSSHSigned(t *testing.T) { + tests := []struct { + name string + tag *Tag + want bool + }{ + { + name: "SSH signed tag", + tag: &Tag{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: true, + }, + { + name: "PGP signed tag", + tag: &Tag{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: false, + }, + { + name: "unsigned tag", + tag: &Tag{}, + want: false, + }, + { + name: "SSH signed tag with leading whitespace", + tag: &Tag{ + Signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.tag.IsSSHSigned()).To(Equal(tt.want)) + }) + } +} + +func TestTag_SignatureType(t *testing.T) { + tests := []struct { + name string + tag *Tag + want string + }{ + { + name: "PGP signed tag", + tag: &Tag{ + Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + }, + want: "pgp", + }, + { + name: "SSH signed tag", + tag: &Tag{ + Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + }, + want: "ssh", + }, + { + name: "unsigned tag", + tag: &Tag{}, + want: "unknown", + }, + { + name: "unknown signature type", + tag: &Tag{ + Signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + }, + want: "unknown", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(tt.tag.SignatureType()).To(Equal(tt.want)) + }) + } +} diff --git a/git/go.mod b/git/go.mod index 229207e82..fd5a8a5ff 100644 --- a/git/go.mod +++ b/git/go.mod @@ -22,6 +22,14 @@ require ( github.com/go-git/go-git/v5 v5.19.0 github.com/onsi/gomega v1.40.0 golang.org/x/crypto v0.50.0 + github.com/fluxcd/pkg/gittestserver v0.28.0 + github.com/fluxcd/pkg/ssh v0.25.0 + github.com/fluxcd/pkg/version v0.15.0 + github.com/go-git/go-billy/v5 v5.9.0 + github.com/go-git/go-git/v5 v5.19.0 + github.com/hiddeco/sshsig v0.2.0 + github.com/onsi/gomega v1.40.0 + golang.org/x/crypto v0.50.0 ) require ( diff --git a/git/go.sum b/git/go.sum index 14f7a8d2e..e51d7f654 100644 --- a/git/go.sum +++ b/git/go.sum @@ -42,6 +42,10 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/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 592044500..8b78c3cbb 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/signatures/gpg_signature.go b/git/signatures/gpg_signature.go new file mode 100644 index 000000000..0abe02d8a --- /dev/null +++ b/git/signatures/gpg_signature.go @@ -0,0 +1,50 @@ +/* +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. +const PGPSignaturePrefix = "-----BEGIN PGP SIGNATURE-----" + +// 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") + } + + 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 000000000..9fa2996a5 --- /dev/null +++ b/git/signatures/gpg_signature_test.go @@ -0,0 +1,403 @@ +/* +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/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", + }, + } + 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 TestVerifyPGPSignatureWithFixturesForTags(t *testing.T) { + testDataDir := filepath.Join("testdata", "gpg_signatures") + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + sigFile string + keyFile string + wantErr bool + }{ + {"rsa_2048 valid tag signature", "tag_rsa_2048_signed.txt", "key_rsa_2048.pub", false}, + {"rsa_4096 valid tag signature", "tag_rsa_4096_signed.txt", "key_rsa_4096.pub", false}, + {"ed25519 valid tag signature", "tag_ed25519_signed.txt", "key_ed25519.pub", false}, + {"ecdsa_p256 valid tag signature", "tag_ecdsa_p256_signed.txt", "key_ecdsa_p256.pub", false}, + {"ecdsa_p384 valid tag signature", "tag_ecdsa_p384_signed.txt", "key_ecdsa_p384.pub", false}, + {"ecdsa_p521 valid tag signature", "tag_ecdsa_p521_signed.txt", "key_ecdsa_p521.pub", false}, + {"dsa_2048 valid tag signature", "tag_dsa_2048_signed.txt", "key_dsa_2048.pub", false}, + {"brainpool_p256 valid tag signature", "tag_brainpool_p256_signed.txt", "key_brainpool_p256.pub", false}, + {"brainpool_p384 valid tag signature", "tag_brainpool_p384_signed.txt", "key_brainpool_p384.pub", false}, + {"brainpool_p512 valid tag signature", "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 tag signature", "tag_ed448_signed.txt", "key_ed448.pub", true}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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()) + }) + } +} + +func TestVerifyPGPSignatureWithFixtures(t *testing.T) { + testDataDir := filepath.Join("testdata", "gpg_signatures") + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + sigFile string + keyFile string + wantErr bool + }{ + {"rsa_2048 valid signature", "commit_rsa_2048_signed.txt", "key_rsa_2048.pub", false}, + {"rsa_4096 valid signature", "commit_rsa_4096_signed.txt", "key_rsa_4096.pub", false}, + {"ed25519 valid signature", "commit_ed25519_signed.txt", "key_ed25519.pub", false}, + {"ecdsa_p256 valid signature", "commit_ecdsa_p256_signed.txt", "key_ecdsa_p256.pub", false}, + {"ecdsa_p384 valid signature", "commit_ecdsa_p384_signed.txt", "key_ecdsa_p384.pub", false}, + {"ecdsa_p521 valid signature", "commit_ecdsa_p521_signed.txt", "key_ecdsa_p521.pub", false}, + {"dsa_2048 valid signature", "commit_dsa_2048_signed.txt", "key_dsa_2048.pub", false}, + {"brainpool_p256 valid signature", "commit_brainpool_p256_signed.txt", "key_brainpool_p256.pub", false}, + {"brainpool_p384 valid signature", "commit_brainpool_p384_signed.txt", "key_brainpool_p384.pub", false}, + {"brainpool_p512 valid signature", "commit_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", "key_ed448.pub", true}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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()) + }) + } + + // Test error cases + t.Run("unsigned commit", func(t *testing.T) { + g := NewWithT(t) + + // Parse the unsigned commit from the fixture file + commitObj, err := 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()) + }) +} + +func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { + testDataDir := filepath.Join("testdata", "gpg_signatures") + + // Read multiple public keys to create a multi-key keyring + keyFiles := []string{ + "key_rsa_2048.pub", + "key_rsa_4096.pub", + "key_ed25519.pub", + "key_ecdsa_p256.pub", + "key_ecdsa_p384.pub", + "key_ecdsa_p521.pub", + "key_dsa_2048.pub", + "key_brainpool_p256.pub", + "key_brainpool_p384.pub", + "key_brainpool_p512.pub", + } + + var keyRings []string + for _, keyFile := range keyFiles { + publicKey, err := os.ReadFile(filepath.Join(testDataDir, keyFile)) + if err != nil { + t.Fatalf("failed to read public key file %s: %v", keyFile, err) + } + keyRings = append(keyRings, string(publicKey)) + } + + // Test cases for each key type using the multi-key keyring + keyTypes := []struct { + name string + sigFile string + wantErr bool + }{ + {"rsa_2048 valid signature with multi-key keyring", "commit_rsa_2048_signed.txt", false}, + {"rsa_4096 valid signature with multi-key keyring", "commit_rsa_4096_signed.txt", false}, + {"ed25519 valid signature with multi-key keyring", "commit_ed25519_signed.txt", false}, + {"ecdsa_p256 valid signature with multi-key keyring", "commit_ecdsa_p256_signed.txt", false}, + {"ecdsa_p384 valid signature with multi-key keyring", "commit_ecdsa_p384_signed.txt", false}, + {"ecdsa_p521 valid signature with multi-key keyring", "commit_ecdsa_p521_signed.txt", false}, + {"dsa_2048 valid signature with multi-key keyring", "commit_dsa_2048_signed.txt", false}, + {"brainpool_p256 valid signature with multi-key keyring", "commit_brainpool_p256_signed.txt", false}, + {"brainpool_p384 valid signature with multi-key keyring", "commit_brainpool_p384_signed.txt", false}, + {"brainpool_p512 valid signature with multi-key keyring", "commit_brainpool_p512_signed.txt", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + g := NewWithT(t) + + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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()) + + // Verify the signature using the multi-key keyring + fingerprint, err := signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, keyRings...) + 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 that an unsigned commit fails with multi-key keyring + t.Run("unsigned commit with multi-key keyring", func(t *testing.T) { + g := NewWithT(t) + + // Parse the unsigned commit from the fixture file + commitObj, err := 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()) + + // Verify the signature - should fail as the commit is unsigned + fingerprint, err := signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, keyRings...) + 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 000000000..04c487b6f --- /dev/null +++ b/git/signatures/signature.go @@ -0,0 +1,64 @@ +/* +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 PGP signature. + SignatureTypePGP SignatureType = "pgp" + // SignatureTypeSSH represents an SSH signature. + SignatureTypeSSH SignatureType = "ssh" + // SignatureTypeUnknown represents an unknown signature type. + SignatureTypeUnknown SignatureType = "unknown" +) + +// 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 { + if signature == "" { + return false + } + return strings.HasPrefix(strings.TrimSpace(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 { + if signature == "" { + return false + } + return strings.HasPrefix(strings.TrimSpace(signature), SSHSignaturePrefix) +} + +// GetSignatureType returns the type of the signature as a string. +// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// and "unknown" for unrecognized or empty signatures. +func GetSignatureType(signature string) string { + if IsPGPSignature(signature) { + return string(SignatureTypePGP) + } + if IsSSHSignature(signature) { + return string(SignatureTypeSSH) + } + return string(SignatureTypeUnknown) +} diff --git a/git/signatures/signature_test.go b/git/signatures/signature_test.go new file mode 100644 index 000000000..baa48c5b4 --- /dev/null +++ b/git/signatures/signature_test.go @@ -0,0 +1,169 @@ +/* +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: "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 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: "empty signature", + signature: "", + want: string(SignatureTypeUnknown), + }, + { + 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 000000000..e298e65ad --- /dev/null +++ b/git/signatures/ssh_signature.go @@ -0,0 +1,106 @@ +/* +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" +) + +// SSHSignaturePrefix is the prefix used by Git to identify SSH signatures. +const SSHSignaturePrefix = "-----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") + } + + // 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 + // The namespace for Git is "git" + // Git supports both SHA256 and SHA512, so we try both + for _, hashAlgo := range []sshsig.HashAlgorithm{sshsig.HashSHA256, sshsig.HashSHA512} { + err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, hashAlgo, "git") + 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.StdEncoding.EncodeToString(hash[:]) +} diff --git a/git/signatures/ssh_signature_test.go b/git/signatures/ssh_signature_test.go new file mode 100644 index 000000000..df9521511 --- /dev/null +++ b/git/signatures/ssh_signature_test.go @@ -0,0 +1,1286 @@ +/* +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/go-git/go-git/v5/plumbing" + "github.com/hiddeco/sshsig" +) + +func TestParseAuthorizedKeys(t *testing.T) { + tests := []struct { + name string + authorizedKeys string + wantCount int + wantErr bool + }{ + { + name: "single key", + authorizedKeys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com", + wantCount: 1, + wantErr: false, + }, + { + name: "multiple keys", + authorizedKeys: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test1@example.com +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test2@example.com`, + wantCount: 2, + wantErr: false, + }, + { + name: "with comments", + authorizedKeys: `# This is a comment +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com +# Another comment`, + wantCount: 1, + wantErr: false, + }, + { + name: "with empty lines", + authorizedKeys: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com + +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test2@example.com`, + wantCount: 2, + wantErr: false, + }, + { + name: "empty", + authorizedKeys: "", + wantCount: 0, + wantErr: false, + }, + { + name: "invalid key", + authorizedKeys: "invalid-key-data", + wantCount: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keys, err := signatures.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) + } + }) + } +} + +func TestParseAuthorizedKeysFromFixtures(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + wantCount int + wantErr bool + }{ + { + name: "ed25519 key", + fixture: "authorized_keys_ed25519", + wantCount: 1, + wantErr: false, + }, + { + name: "rsa key", + fixture: "authorized_keys_rsa", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p256 key", + fixture: "authorized_keys_ecdsa_p256", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p384 key", + fixture: "authorized_keys_ecdsa_p384", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p521 key", + fixture: "authorized_keys_ecdsa_p521", + wantCount: 1, + wantErr: false, + }, + { + name: "all key types combined", + fixture: "authorized_keys_all", + 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 := signatures.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) + } + + // Verify that each key has a valid fingerprint + for i, key := range keys { + fingerprint := signatures.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) + } + } + }) + } +} + +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{"authorized_keys_ed25519", "authorized_keys_rsa"}, + wantCount: 2, + wantErr: false, + }, + { + name: "ed25519 + ecdsa p256", + fixtures: []string{"authorized_keys_ed25519", "authorized_keys_ecdsa_p256"}, + wantCount: 2, + wantErr: false, + }, + { + name: "rsa + ecdsa p384 + ecdsa p521", + fixtures: []string{"authorized_keys_rsa", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, + wantCount: 3, + wantErr: false, + }, + { + name: "all ecdsa variants", + fixtures: []string{"authorized_keys_ecdsa_p256", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, + wantCount: 3, + wantErr: false, + }, + { + name: "ed25519 + rsa + all ecdsa", + fixtures: []string{"authorized_keys_ed25519", "authorized_keys_rsa", "authorized_keys_ecdsa_p256", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, + 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 := signatures.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 := signatures.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) + } + } + }) + } +} + +func TestParseSSHSignature(t *testing.T) { + tests := []struct { + name string + sig string + wantErr bool + }{ + { + name: "valid signature with PEM armor", + sig: `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg9uahUwBLlO2Dvuz0MtIA5/iBcK +JCmB1F6QUeXNtccscAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQFb88f1ZXOK1BByC4QQOthH9bZP0/hMcPl62h4oIuEny6W5xd/oOpDv7dmj9A6DiMS +o6RLdWlvb81l/UyYhGEwE= +-----END SSH SIGNATURE-----`, + wantErr: false, + }, + { + name: "valid signature without PEM armor", + sig: "U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg9uahUwBLlO2Dvuz0MtIA5/iBcKJCmB1F6QUeXNtccscAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5AAAAQFb88f1ZXOK1BByC4QQOthH9bZP0/hMcPl62h4oIuEny6W5xd/oOpDv7dmj9A6DiMSo6RLdWlvb81l/UyYhGEwE=", + wantErr: true, // sshsig.Unarmor() requires PEM armor + }, + { + name: "empty signature", + sig: "", + wantErr: true, + }, + { + name: "invalid base64", + sig: "-----BEGIN SSH SIGNATURE-----\ninvalid-base64!!!\n-----END SSH SIGNATURE-----", + wantErr: true, + }, + { + name: "invalid format", + sig: "invalid-signature-format", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sig, err := sshsig.Unarmor([]byte(tt.sig)) + if (err != nil) != tt.wantErr { + t.Errorf("sshsig.Unarmor() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && sig == nil { + t.Errorf("sshsig.Unarmor() returned nil signature") + } + }) + } +} + +func TestGetPublicKeyFingerprint(t *testing.T) { + // Test with a known public key + pubKeyStr := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com" + keys, err := signatures.ParseAuthorizedKeys(pubKeyStr) + if err != nil { + t.Fatalf("Failed to parse test public key: %v", err) + } + if len(keys) == 0 { + t.Fatal("No keys parsed") + } + + fingerprint := signatures.GetPublicKeyFingerprint(keys[0]) + if fingerprint == "" { + t.Error("GetPublicKeyFingerprint() returned empty string") + } + if !strings.HasPrefix(fingerprint, "SHA256:") { + t.Errorf("GetPublicKeyFingerprint() = %s, want prefix SHA256:", fingerprint) + } +} + +func TestVerifySSHSignature(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + sigFile string + authFile string + wantErr bool + }{ + {"ed25519 valid signature", "commit_ed25519_signed.txt", "authorized_keys_ed25519", false}, + {"rsa valid signature", "commit_rsa_signed.txt", "authorized_keys_rsa", false}, + {"ecdsa_p256 valid signature", "commit_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, + {"ecdsa_p384 valid signature", "commit_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, + {"ecdsa_p521 valid signature", "commit_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the signature using the git.Commit's Signature and Encoded fields + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } + + // Test error cases + t.Run("empty signature", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + fingerprint, err := signatures.VerifySSHSignature("", gitCommit.Encoded, string(authorizedKeys)) + 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) + } + }) + + t.Run("empty payload", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, []byte{}, string(authorizedKeys)) + 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) + } + }) + + t.Run("wrong authorized keys", func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + // 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) + } + }) + + t.Run("invalid signature", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + invalidSig := "-----BEGIN SSH SIGNATURE-----\n invalid\n -----END SSH SIGNATURE-----" + + fingerprint, err := signatures.VerifySSHSignature(invalidSig, gitCommit.Encoded, string(authorizedKeys)) + 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) + } + }) +} + +func TestVerifySSHSignatureAllKeyTypes(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Test cases for each key type + keyTypes := []struct { + name string + sigFile string + authFile string + wantErr bool + }{ + {"ed25519", "commit_ed25519_signed.txt", "authorized_keys_ed25519", false}, + {"rsa", "commit_rsa_signed.txt", "authorized_keys_rsa", false}, + {"ecdsa_p256", "commit_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, + {"ecdsa_p384", "commit_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, + {"ecdsa_p521", "commit_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the signature using the git.Commit's Signature and Encoded fields + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} + +func TestVerifySSHSignatureCombinedKeys(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Read the combined authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_all")) + if err != nil { + t.Fatalf("Failed to read combined authorized keys: %v", err) + } + + // Test each key type against the combined authorized keys + keyTypes := []struct { + name string + sigFile string + wantErr bool + }{ + {"ed25519", "commit_ed25519_signed.txt", false}, + {"rsa", "commit_rsa_signed.txt", false}, + {"ecdsa_p256", "commit_ecdsa_p256_signed.txt", false}, + {"ecdsa_p384", "commit_ecdsa_p384_signed.txt", false}, + {"ecdsa_p521", "commit_ecdsa_p521_signed.txt", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + 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) + } + + // Verify the signature with combined authorized keys + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} + +func TestBuildCommitWithRefFromFixture(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + wantErr bool + wantSig bool + }{ + { + name: "ed25519 signed commit", + fixture: "commit_ed25519_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "rsa signed commit", + fixture: "commit_rsa_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p256 signed commit", + fixture: "commit_ecdsa_p256_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p384 signed commit", + fixture: "commit_ecdsa_p384_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p521 signed commit", + fixture: "commit_ecdsa_p521_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "unsigned commit", + fixture: "commit_unsigned.txt", + wantErr: false, + wantSig: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) + 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) != tt.wantErr { + t.Errorf("BuildCommitWithRef() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the commit was built correctly + if gitCommit == nil { + t.Fatal("BuildCommitWithRef() returned nil commit") + } + + // Check if signature is present as expected + hasSig := gitCommit.Signature != "" + if hasSig != tt.wantSig { + t.Errorf("BuildCommitWithRef() has signature = %v, want %v", hasSig, tt.wantSig) + } + + // Verify the encoded data is present + if len(gitCommit.Encoded) == 0 { + t.Error("BuildCommitWithRef() returned commit with empty Encoded field") + } + + // Verify the reference is set correctly + if gitCommit.Reference != "refs/heads/main" { + t.Errorf("BuildCommitWithRef() reference = %q, want %q", gitCommit.Reference, "refs/heads/main") + } + + // Verify the hash is set + if len(gitCommit.Hash) == 0 { + t.Error("BuildCommitWithRef() returned commit with empty Hash") + } + + // Verify author and committer are set + if gitCommit.Author.Name == "" { + t.Error("BuildCommitWithRef() returned commit with empty Author.Name") + } + if gitCommit.Committer.Name == "" { + t.Error("BuildCommitWithRef() returned commit with empty Committer.Name") + } + + // If the commit has a signature, verify it can be extracted + if tt.wantSig { + // The signature is stored in gitCommit.Signature, not in gitCommit.Encoded + // gitCommit.Encoded contains the encoded commit without the signature + if gitCommit.Signature == "" { + t.Error("BuildCommitWithRef() returned commit with empty Signature field") + } + // Verify the signature contains the expected SSH signature markers + if !strings.Contains(gitCommit.Signature, "-----BEGIN SSH SIGNATURE-----") { + t.Error("BuildCommitWithRef() signature does not contain SSH signature start marker") + } + if !strings.Contains(gitCommit.Signature, "-----END SSH SIGNATURE-----") { + t.Error("BuildCommitWithRef() signature does not contain SSH signature end marker") + } + } + } + }) + } +} + +func TestBuildCommitWithRefAndVerifySSH(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + authFile string + wantErr bool + }{ + { + name: "ed25519 signed commit", + fixture: "commit_ed25519_signed.txt", + authFile: "authorized_keys_ed25519", + wantErr: false, + }, + { + name: "rsa signed commit", + fixture: "commit_rsa_signed.txt", + authFile: "authorized_keys_rsa", + wantErr: false, + }, + { + name: "ecdsa p256 signed commit", + fixture: "commit_ecdsa_p256_signed.txt", + authFile: "authorized_keys_ecdsa_p256", + wantErr: false, + }, + { + name: "ecdsa p384 signed commit", + fixture: "commit_ecdsa_p384_signed.txt", + authFile: "authorized_keys_ecdsa_p384", + wantErr: false, + }, + { + name: "ecdsa p521 signed commit", + fixture: "commit_ecdsa_p521_signed.txt", + authFile: "authorized_keys_ecdsa_p521", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) + 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("BuildCommitWithRef() error = %v", err) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the SSH signature using the git.Commit's VerifySSH method + fingerprint, err := gitCommit.VerifySSH(string(authorizedKeys)) + if (err != nil) != tt.wantErr { + t.Errorf("git.Commit.VerifySSH() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if fingerprint == "" { + t.Error("git.Commit.VerifySSH() returned empty fingerprint") + } + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} + +func TestBuildCommitWithRefWithDifferentRefs(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Parse a signed commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse commit from fixture: %v", err) + } + + tests := []struct { + name string + ref plumbing.ReferenceName + wantRef string + }{ + { + name: "branch reference", + ref: plumbing.ReferenceName("refs/heads/main"), + wantRef: "refs/heads/main", + }, + { + name: "tag reference", + ref: plumbing.ReferenceName("refs/tags/v1.0.0"), + wantRef: "refs/tags/v1.0.0", + }, + { + name: "remote branch reference", + ref: plumbing.ReferenceName("refs/remotes/origin/main"), + wantRef: "refs/remotes/origin/main", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build a git.Commit using BuildCommitWithRef with different references + gitCommit, err := gogit.BuildCommitWithRef(commitObj, nil, tt.ref) + if err != nil { + t.Fatalf("BuildCommitWithRef() error = %v", err) + } + + // Verify the reference is set correctly + if gitCommit.Reference != tt.wantRef { + t.Errorf("BuildCommitWithRef() reference = %q, want %q", gitCommit.Reference, tt.wantRef) + } + + // Verify other fields are still set correctly + if len(gitCommit.Hash) == 0 { + t.Error("BuildCommitWithRef() returned commit with empty Hash") + } + if gitCommit.Signature == "" { + t.Error("BuildCommitWithRef() returned commit with empty Signature") + } + }) + } +} + +func TestBuildTagFromFixture(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + wantErr bool + wantSig bool + }{ + { + name: "ed25519 signed tag", + fixture: "tag_ed25519_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "rsa signed tag", + fixture: "tag_rsa_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p256 signed tag", + fixture: "tag_ecdsa_p256_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p384 signed tag", + fixture: "tag_ecdsa_p384_signed.txt", + wantErr: false, + wantSig: true, + }, + { + name: "ecdsa p521 signed tag", + fixture: "tag_ecdsa_p521_signed.txt", + wantErr: false, + wantSig: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %v", err) + } + + // Build a git.Tag using BuildTag + gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + if (err != nil) != tt.wantErr { + t.Errorf("BuildTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + // Verify the tag was built correctly + if gitTag == nil { + t.Fatal("BuildTag() returned nil tag") + } + + // Check if signature is present as expected + hasSig := gitTag.Signature != "" + if hasSig != tt.wantSig { + t.Errorf("BuildTag() has signature = %v, want %v", hasSig, tt.wantSig) + } + + // Verify the encoded data is present + if len(gitTag.Encoded) == 0 { + t.Error("BuildTag() returned tag with empty Encoded field") + } + + // Verify the name is set correctly + if gitTag.Name == "" { + t.Error("BuildTag() returned tag with empty Name") + } + + // Verify the hash is set + if len(gitTag.Hash) == 0 { + t.Error("BuildTag() returned tag with empty Hash") + } + + // Verify author is set + if gitTag.Author.Name == "" { + t.Error("BuildTag() returned tag with empty Author.Name") + } + + // If the tag has a signature, verify it can be extracted + if tt.wantSig { + if gitTag.Signature == "" { + t.Error("BuildTag() returned tag with empty Signature field") + } + // Verify the signature contains the expected SSH signature markers + if !strings.Contains(gitTag.Signature, "-----BEGIN SSH SIGNATURE-----") { + t.Error("BuildTag() signature does not contain SSH signature start marker") + } + if !strings.Contains(gitTag.Signature, "-----END SSH SIGNATURE-----") { + t.Error("BuildTag() signature does not contain SSH signature end marker") + } + } + } + }) + } +} + +func TestVerifySSHSignatureForTags(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + sigFile string + authFile string + wantErr bool + }{ + {"ed25519 valid signature", "tag_ed25519_signed.txt", "authorized_keys_ed25519", false}, + {"rsa valid signature", "tag_rsa_signed.txt", "authorized_keys_rsa", false}, + {"ecdsa_p256 valid signature", "tag_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, + {"ecdsa_p384 valid signature", "tag_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, + {"ecdsa_p521 valid signature", "tag_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the signature using the git.Tag's Signature and Encoded fields + fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } + + // Test error cases + t.Run("empty signature", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + fingerprint, err := signatures.VerifySSHSignature("", gitTag.Encoded, string(authorizedKeys)) + 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) + } + }) + + t.Run("empty payload", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, []byte{}, string(authorizedKeys)) + 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) + } + }) + + t.Run("wrong authorized keys", func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + // Use a different key that won't match + wrongKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEyM97VxLgOCuB9Eg5cDtTc8ogkdM1xAyJhzODB9cK1 wrong@example.com" + + 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) + } + }) + + t.Run("invalid signature", func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + invalidSig := "-----BEGIN SSH SIGNATURE-----\n invalid\n -----END SSH SIGNATURE-----" + + fingerprint, err := signatures.VerifySSHSignature(invalidSig, gitTag.Encoded, string(authorizedKeys)) + 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) + } + }) +} + +func TestVerifySSHSignatureForTagsAllKeyTypes(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Test cases for each key type + keyTypes := []struct { + name string + sigFile string + authFile string + wantErr bool + }{ + {"ed25519", "tag_ed25519_signed.txt", "authorized_keys_ed25519", false}, + {"rsa", "tag_rsa_signed.txt", "authorized_keys_rsa", false}, + {"ecdsa_p256", "tag_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, + {"ecdsa_p384", "tag_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, + {"ecdsa_p521", "tag_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the signature using the git.Tag's Signature and Encoded fields + fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} + +func TestVerifySSHSignatureForTagsCombinedKeys(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + // Read the combined authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_all")) + if err != nil { + t.Fatalf("Failed to read combined authorized keys: %v", err) + } + + // Test each key type against the combined authorized keys + keyTypes := []struct { + name string + sigFile string + wantErr bool + }{ + {"ed25519", "tag_ed25519_signed.txt", false}, + {"rsa", "tag_rsa_signed.txt", false}, + {"ecdsa_p256", "tag_ecdsa_p256_signed.txt", false}, + {"ecdsa_p384", "tag_ecdsa_p384_signed.txt", false}, + {"ecdsa_p521", "tag_ecdsa_p521_signed.txt", false}, + } + + for _, kt := range keyTypes { + t.Run(kt.name, func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %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) + } + + // Verify the signature with combined authorized keys + fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) + if (err != nil) != kt.wantErr { + t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) + return + } + if !kt.wantErr && fingerprint == "" { + t.Errorf("VerifySSHSignature() returned empty fingerprint") + } + if !kt.wantErr { + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} + +func TestBuildTagAndVerifySSH(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + authFile string + wantErr bool + }{ + { + name: "ed25519 signed tag", + fixture: "tag_ed25519_signed.txt", + authFile: "authorized_keys_ed25519", + wantErr: false, + }, + { + name: "rsa signed tag", + fixture: "tag_rsa_signed.txt", + authFile: "authorized_keys_rsa", + wantErr: false, + }, + { + name: "ecdsa p256 signed tag", + fixture: "tag_ecdsa_p256_signed.txt", + authFile: "authorized_keys_ecdsa_p256", + wantErr: false, + }, + { + name: "ecdsa p384 signed tag", + fixture: "tag_ecdsa_p384_signed.txt", + authFile: "authorized_keys_ecdsa_p384", + wantErr: false, + }, + { + name: "ecdsa p521 signed tag", + fixture: "tag_ecdsa_p521_signed.txt", + authFile: "authorized_keys_ecdsa_p521", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the tag from the fixture file + tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) + if err != nil { + t.Fatalf("Failed to parse tag from fixture: %v", err) + } + + // Build a git.Tag using BuildTag + gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + if err != nil { + t.Fatalf("BuildTag() error = %v", err) + } + + // Read the authorized keys + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.authFile)) + if err != nil { + t.Fatalf("Failed to read authorized keys: %v", err) + } + + // Verify the SSH signature using the git.Tag's VerifySSH method + fingerprint, err := gitTag.VerifySSH(string(authorizedKeys)) + if (err != nil) != tt.wantErr { + t.Errorf("git.Tag.VerifySSH() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if fingerprint == "" { + t.Error("git.Tag.VerifySSH() returned empty fingerprint") + } + t.Logf("Verified with fingerprint: %s", fingerprint) + } + }) + } +} diff --git a/git/signatures/testdata/gpg_signatures/README.md b/git/signatures/testdata/gpg_signatures/README.md new file mode 100644 index 000000000..29fd5fc70 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/README.md @@ -0,0 +1,399 @@ +# 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) + - DSA (2048 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_dsa_2048.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-dsa-2048@example.com > key_dsa_2048.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_dsa_2048.pub` - DSA 2048-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_dsa_2048_signed.txt` - DSA 2048-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_dsa_2048_signed.txt` - DSA 2048-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 + +### DSA (Digital Signature Algorithm) +- **DSA 2048**: Legacy algorithm, 2048-bit key +- Less secure than modern alternatives, included for compatibility testing + +### 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 and DSA 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 000000000..8e2e958c0 --- /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 000000000..9feb7d2fa --- /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 000000000..76c1efc1e --- /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_dsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt new file mode 100644 index 000000000..1132feef8 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt @@ -0,0 +1,12 @@ +tree 94e5cb8fdb0551092fe394328dd9de2dbd8394f3 +author Test User 1772188965 +0100 +committer Test User 1772188965 +0100 +gpgsig -----BEGIN PGP SIGNATURE----- + + iHUEABEIAB0WIQQ3p1oEVydAtN6w28QIntqNADkiBwUCaaF1JQAKCRAIntqNADki + B3brAP9bhBteRaxkRDN2rXbAxFdBLACqgqTH10Zv4if3gxZxKQD/ZoAiBYUyWq3C + HKyihQ+PCD2wMv6tyzkC5RI5mumh5Fw= + =C3Wn + -----END PGP SIGNATURE----- + +Test commit signed with dsa_2048 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 000000000..a95b90f90 --- /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 000000000..596f99517 --- /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 000000000..f0fe269bc --- /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 000000000..25d01d2f6 --- /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 000000000..6080f3434 --- /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 000000000..d696d92b4 --- /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 000000000..a983592e2 --- /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 000000000..491a14418 --- /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 000000000..05eaa6a56 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -0,0 +1,245 @@ +#!/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=$(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=$(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/DSA keys..." + echo "-----------------------------------" + + # RSA keys (different key lengths) + generate_key "RSA" "2048" "rsa_2048" + generate_key "RSA" "4096" "rsa_4096" + + # DSA key (legacy, but still supported) + generate_key "DSA" "2048" "dsa_2048" + + 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=() + for key_file in "$TEMP_DIR"/*_id.txt; do + if [[ -f "$key_file" ]]; then + local 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 \ No newline at end of file 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 000000000..b08e1ba5c --- /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 000000000..003449460 --- /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 000000000..b4916c41a --- /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_dsa_2048.pub b/git/signatures/testdata/gpg_signatures/key_dsa_2048.pub new file mode 100644 index 000000000..908d2c05b --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/key_dsa_2048.pub @@ -0,0 +1,25 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQMuBGmhdSIRCACoIRoP5Lvi8g2dE7Nn9/AI6O3SEnCotRNnT10nmELXwqYonn/9 +3LIhiMMqdMPgtIQZLuvoUlZzMG/mufeHZlezhfUqbFOHY09Czcuvm0zkTBwIDq+a +CA729ICuPAgYAq53iPab5WqGO9H/LAX/6yYGLhQ0GHFdpnyxWnO/OtSBlLxqL+97 +oz6lhGSC9JSNxazSr/3qwvaytzOMI8ptDIVlpydv8WTghXBkjrIkXR8vR++2aEhK +voVCS9HSC19kg9B2fybGu4M5foP9ZIL62O+6rvopGSA1tmWctR2oIoi3Bi7x5vzA +f3NyOYZT8F+nnxwotdxzeoYxh/rVld4i321jAQCYLj2IVmgisitLwHKxXVT/aX3O +CAaEI/ESOcBm/arO9Qf9HLfKlc2wVtXL1g0KjaMZVvh9nvqzchBxmTtlLGmU5gIU +r7ZqDQ2pqavmZJ1YRBlGPRLnL8n1NXZMj8OHPRHyUJQ4oph8FFRoBOwspEw+i67j +jl42mc20IhOU28QPmtsmlEHJwdhZsYmCImtWFilHS8ThPewY+Qn2S0L+4nnBkTy4 +1y3ZGRSzQQhH1jOJtBdNBadQcrYppMWgxHNIe0V3s+7FCc89jJRECj608ZrlLYT1 +7KGPZDPqDtR668Br7sP6PjPJD6mnycsrQSNu1rFU3fsuClVlLeT9mWpwQspfQEYa +vmuRh48uuGUQFBanDM5EPTG7c4aB6Gz1k8J/HtXRpwf/Rc8eHZIVGrpc/7CuChGF +fqloBvAz77A3Blr7KaYIViEXj9dcw75Aurtk9lhtUpYe4A66ZdyoZE03xsKmATKX +Ois1YgBaQGEZoOM632pbv3bFrSCjrZnLMnwLIGDhqKnJy7H0mALKL9iILcN7lF0P +WU1YSNgZFU6X70aJvwEmOjeBM5YhGS+e4OPZW/z+b3f/1jE3dGJwz6LsT+M8+xWw +uqN1ZJ+Ijvg8k6HIFx4eXY0zPLElIaWkZExNki/T35jnazb8ZzCeu4/RiJz6YMwd +OAPIZ3I0dZJe5BO32eRbMQFb+OzEcVTNV5Jc/m09b9jEfOvmrHRHoDgGrF6/0S3Y +R7QlVGVzdCBVc2VyIDx0ZXN0LWRzYV8yMDQ4QGV4YW1wbGUuY29tPoiTBBMRCAA7 +FiEEN6daBFcnQLTesNvECJ7ajQA5IgcFAmmhdSICGyMFCwkIBwICIgIGFQoJCAsC +BBYCAwECHgcCF4AACgkQCJ7ajQA5IgcsBAD9F9koK8sIUApNcCFUqCGR9olYimkN +juoedSfOpMV/+j0A+wXU0jUfweGWUv7MPGmh1Sn0oMOBZTIL0LU+x/F3glLl +=lJcF +-----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 000000000..3d692bd2d --- /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 000000000..bdd907f05 --- /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 000000000..db963c3ca --- /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 000000000..6ba0bb532 --- /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 000000000..1a0082821 --- /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 000000000..353098510 --- /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 000000000..d2356537d --- /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 000000000..f5429018a --- /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 000000000..90d96b407 --- /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 000000000..60815c75a --- /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_dsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt new file mode 100644 index 000000000..46578ae13 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt @@ -0,0 +1,13 @@ +object 8ee3b79d5ad1fa463deea7fc9bfcbca311168d01 +type commit +tag test-tag-dsa_2048 +tagger Test User 1772188968 +0100 + +Test tag signed with dsa_2048 +-----BEGIN PGP SIGNATURE----- + +iHUEABEIAB0WIQQ3p1oEVydAtN6w28QIntqNADkiBwUCaaF1KAAKCRAIntqNADki +Byq0AP9rHhQiJKh3rPNYW06C6N9yGnccU8nE5S5EfeH8Gps6SQD/f19dyM5euse9 +vylc3KD1sfdFekiLuW2WpDIw4JbAbMg= +=k9QD +-----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 000000000..c21ef5bff --- /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 000000000..0340f25c6 --- /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 000000000..c43adb5ce --- /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 000000000..3ab00c63f --- /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 000000000..6b27d5dda --- /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 000000000..b2967d822 --- /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 000000000..4c5ae5000 --- /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 000000000..ab402ffec --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/README.md @@ -0,0 +1,202 @@ +# 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 + +### Authorized Keys Files +- `authorized_keys_rsa` - RSA public key +- `authorized_keys_ecdsa_p256` - ECDSA P-256 public key +- `authorized_keys_ecdsa_p384` - ECDSA P-384 public key +- `authorized_keys_ecdsa_p521` - ECDSA P-521 public key +- `authorized_keys_ed25519` - ED25519 public key +- `authorized_keys_all` - All public keys combined + +### 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/authorized_keys_all b/git/signatures/testdata/ssh_signatures/authorized_keys_all new file mode 100644 index 000000000..2a587db72 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_all @@ -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/authorized_keys_ecdsa_p256 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 new file mode 100644 index 000000000..7364a9a27 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 new file mode 100644 index 000000000..aabefb80b --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 new file mode 100644 index 000000000..82d92898f --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 @@ -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/authorized_keys_ed25519 b/git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 new file mode 100644 index 000000000..8f745c471 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_rsa b/git/signatures/testdata/ssh_signatures/authorized_keys_rsa new file mode 100644 index 000000000..b02a4d38f --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/authorized_keys_rsa @@ -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/commit_ecdsa_p256_signed.txt b/git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt new file mode 100644 index 000000000..12ed29b51 --- /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 000000000..860fd0f26 --- /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 000000000..fcc8de7ef --- /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 000000000..4f6da87f3 --- /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 000000000..ca23e48e5 --- /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 000000000..84dc42228 --- /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 000000000..2b5a76183 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -0,0 +1,286 @@ +#!/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" +} + +# Function to create authorized_keys files +create_authorized_keys() { + local key_name=$1 + local output_file="$SCRIPT_DIR/authorized_keys_${key_name}" + + echo "Creating authorized_keys for $key_name..." + + # Copy public key + cp "$TEMP_DIR/${key_name}.pub" "$output_file" + echo " ✓ $output_file 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_authorized_keys() { + local output_file="$SCRIPT_DIR/authorized_keys_all" + + 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 authorized_keys files..." + echo "-----------------------------------------------" + + # Individual authorized_keys files + create_authorized_keys "rsa" + create_authorized_keys "ecdsa_p256" + create_authorized_keys "ecdsa_p384" + create_authorized_keys "ecdsa_p521" + create_authorized_keys "ed25519" + + # Combined authorized_keys file + create_combined_authorized_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 000000000..7364a9a27 --- /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_p384.pub b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub new file mode 100644 index 000000000..aabefb80b --- /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_p521.pub b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub new file mode 100644 index 000000000..82d92898f --- /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_ed25519.pub b/git/signatures/testdata/ssh_signatures/key_ed25519.pub new file mode 100644 index 000000000..8f745c471 --- /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_rsa.pub b/git/signatures/testdata/ssh_signatures/key_rsa.pub new file mode 100644 index 000000000..b02a4d38f --- /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/tag_ecdsa_p256_signed.txt b/git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt new file mode 100644 index 000000000..a20933c0b --- /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 000000000..002180388 --- /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 000000000..48690d844 --- /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 000000000..4b811e555 --- /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 000000000..3882a4fa4 --- /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 000000000..40c5d87a6 --- /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 000000000..c776e628e --- /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 000000000..ef4a20160 --- /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 000000000..91f0e5571 --- /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 000000000..ce5186928 --- /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 000000000..0e845f6f5 --- /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/signatures/testutils_test.go b/git/signatures/testutils_test.go new file mode 100644 index 000000000..7088f741d --- /dev/null +++ b/git/signatures/testutils_test.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 signatures_test + +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 +} From f68d32c2dd653369757e3200eadd9d32edc18fd0 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 27 Feb 2026 18:27:24 +0100 Subject: [PATCH 02/16] adds validation of key fingerprint string Signed-off-by: Ricardo Bartels --- git/signatures/ssh_signature.go | 2 +- git/signatures/ssh_signature_test.go | 154 ++++++++++++------ .../ssh_signatures/calc_fingerprints.go | 35 ++++ .../ssh_signatures/generate_ssh_fixtures.sh | 94 ++++++----- .../key_ecdsa_p256.pub_fingerprint | 1 + .../key_ecdsa_p384.pub_fingerprint | 1 + .../key_ecdsa_p521.pub_fingerprint | 1 + .../key_ed25519.pub_fingerprint | 1 + .../ssh_signatures/key_rsa.pub_fingerprint | 1 + 9 files changed, 193 insertions(+), 97 deletions(-) create mode 100644 git/signatures/testdata/ssh_signatures/calc_fingerprints.go create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint create mode 100644 git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint create mode 100644 git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint create mode 100644 git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint diff --git a/git/signatures/ssh_signature.go b/git/signatures/ssh_signature.go index e298e65ad..c2fd9d2cc 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signatures/ssh_signature.go @@ -102,5 +102,5 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri // in the format used by SSH (e.g., "SHA256:abc123..."). func GetPublicKeyFingerprint(pubKey gossh.PublicKey) string { hash := sha256.Sum256(pubKey.Marshal()) - return "SHA256:" + base64.StdEncoding.EncodeToString(hash[:]) + return "SHA256:" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(hash[:]), "=") } diff --git a/git/signatures/ssh_signature_test.go b/git/signatures/ssh_signature_test.go index df9521511..f364b4b09 100644 --- a/git/signatures/ssh_signature_test.go +++ b/git/signatures/ssh_signature_test.go @@ -30,51 +30,65 @@ import ( func TestParseAuthorizedKeys(t *testing.T) { tests := []struct { - name string - authorizedKeys string - wantCount int - wantErr bool + name string + authorizedKeys string + wantCount int + wantErr bool + wantFingerprints []string }{ { - name: "single key", - authorizedKeys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com", - wantCount: 1, - wantErr: false, + 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 -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test2@example.com`, - wantCount: 2, - wantErr: false, +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 -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com # Another comment`, - wantCount: 1, - wantErr: false, + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:+vwrYGpHfAAWIzT2x+uV+duJG7ZnSvCbRKwdPApx7JA"}, }, { name: "with empty lines", - authorizedKeys: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com + authorizedKeys: `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test2@example.com`, - wantCount: 2, - wantErr: false, +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, + name: "empty", + authorizedKeys: "", + wantCount: 0, + wantErr: false, + wantFingerprints: []string{}, }, { - name: "invalid key", - authorizedKeys: "invalid-key-data", - wantCount: 0, - wantErr: true, + name: "invalid key", + authorizedKeys: "invalid-key-data", + wantCount: 0, + wantErr: true, + wantFingerprints: []string{}, }, } @@ -88,6 +102,21 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH 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 := signatures.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) + } + } + } }) } } @@ -96,40 +125,46 @@ func TestParseAuthorizedKeysFromFixtures(t *testing.T) { testDataDir := filepath.Join("testdata", "ssh_signatures") tests := []struct { - name string - fixture string - wantCount int - wantErr bool + name string + fixture string + fingerprintFile string + wantCount int + wantErr bool }{ { - name: "ed25519 key", - fixture: "authorized_keys_ed25519", - wantCount: 1, - wantErr: false, + name: "ed25519 key", + fixture: "authorized_keys_ed25519", + fingerprintFile: "key_ed25519.pub_fingerprint", + wantCount: 1, + wantErr: false, }, { - name: "rsa key", - fixture: "authorized_keys_rsa", - wantCount: 1, - wantErr: false, + name: "rsa key", + fixture: "authorized_keys_rsa", + fingerprintFile: "key_rsa.pub_fingerprint", + wantCount: 1, + wantErr: false, }, { - name: "ecdsa p256 key", - fixture: "authorized_keys_ecdsa_p256", - wantCount: 1, - wantErr: false, + name: "ecdsa p256 key", + fixture: "authorized_keys_ecdsa_p256", + fingerprintFile: "key_ecdsa_p256.pub_fingerprint", + wantCount: 1, + wantErr: false, }, { - name: "ecdsa p384 key", - fixture: "authorized_keys_ecdsa_p384", - wantCount: 1, - wantErr: false, + name: "ecdsa p384 key", + fixture: "authorized_keys_ecdsa_p384", + fingerprintFile: "key_ecdsa_p384.pub_fingerprint", + wantCount: 1, + wantErr: false, }, { - name: "ecdsa p521 key", - fixture: "authorized_keys_ecdsa_p521", - wantCount: 1, - wantErr: false, + name: "ecdsa p521 key", + fixture: "authorized_keys_ecdsa_p521", + fingerprintFile: "key_ecdsa_p521.pub_fingerprint", + wantCount: 1, + wantErr: false, }, { name: "all key types combined", @@ -155,6 +190,16 @@ func TestParseAuthorizedKeysFromFixtures(t *testing.T) { 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 := signatures.GetPublicKeyFingerprint(key) @@ -164,6 +209,12 @@ func TestParseAuthorizedKeysFromFixtures(t *testing.T) { 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) + } + } } }) } @@ -300,6 +351,7 @@ o6RLdWlvb81l/UyYhGEwE= func TestGetPublicKeyFingerprint(t *testing.T) { // Test with a known public key pubKeyStr := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com" + expectedFingerprint := "SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM" keys, err := signatures.ParseAuthorizedKeys(pubKeyStr) if err != nil { t.Fatalf("Failed to parse test public key: %v", err) @@ -312,7 +364,7 @@ func TestGetPublicKeyFingerprint(t *testing.T) { if fingerprint == "" { t.Error("GetPublicKeyFingerprint() returned empty string") } - if !strings.HasPrefix(fingerprint, "SHA256:") { + if !strings.HasPrefix(fingerprint, expectedFingerprint) { t.Errorf("GetPublicKeyFingerprint() = %s, want prefix SHA256:", fingerprint) } } diff --git a/git/signatures/testdata/ssh_signatures/calc_fingerprints.go b/git/signatures/testdata/ssh_signatures/calc_fingerprints.go new file mode 100644 index 000000000..2cd8bf8d6 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/calc_fingerprints.go @@ -0,0 +1,35 @@ +package main + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + + gossh "golang.org/x/crypto/ssh" +) + +func main() { + keys := []struct { + name string + key string + }{ + {"test_key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com"}, + {"ed25519", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com"}, + {"rsa", "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_p256", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com"}, + {"ecdsa_p384", "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com"}, + {"ecdsa_p521", "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com"}, + } + + for _, k := range keys { + pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(k.key)) + if err != nil { + fmt.Printf("Error parsing %s: %v\n", k.name, err) + continue + } + hash := sha256.Sum256(pubKey.Marshal()) + fingerprint := "SHA256:" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(hash[:]), "=") + fmt.Printf("%s: %s\n", k.name, fingerprint) + } +} diff --git a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh index 2b5a76183..ab7cba060 100755 --- a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh +++ b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -22,9 +22,9 @@ 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" @@ -36,19 +36,23 @@ generate_ssh_key() { 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 authorized_keys files create_authorized_keys() { local key_name=$1 local output_file="$SCRIPT_DIR/authorized_keys_${key_name}" - + echo "Creating authorized_keys for $key_name..." - + # Copy public key cp "$TEMP_DIR/${key_name}.pub" "$output_file" echo " ✓ $output_file created" @@ -58,9 +62,9 @@ create_authorized_keys() { 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" @@ -69,9 +73,9 @@ create_verified_signers() { # Function to create combined authorized_keys file create_combined_authorized_keys() { local output_file="$SCRIPT_DIR/authorized_keys_all" - + echo "Creating combined authorized_keys..." - + # Combine all public keys { cat "$TEMP_DIR/rsa.pub" @@ -80,16 +84,16 @@ create_combined_authorized_keys() { 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")" @@ -98,7 +102,7 @@ create_combined_verified_signers() { 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" } @@ -108,30 +112,30 @@ create_signed_object() { 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 @@ -140,7 +144,7 @@ create_signed_object() { 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" @@ -150,7 +154,7 @@ create_signed_object() { 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 @@ -159,7 +163,7 @@ create_signed_object() { 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" @@ -173,26 +177,26 @@ create_signed_object() { # 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" } @@ -201,79 +205,79 @@ create_unsigned_commit() { 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 authorized_keys files..." echo "-----------------------------------------------" - + # Individual authorized_keys files create_authorized_keys "rsa" create_authorized_keys "ecdsa_p256" create_authorized_keys "ecdsa_p384" create_authorized_keys "ecdsa_p521" create_authorized_keys "ed25519" - + # Combined authorized_keys file create_combined_authorized_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." 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 000000000..f62198c01 --- /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_fingerprint b/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint new file mode 100644 index 000000000..ee5243a33 --- /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_fingerprint b/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint new file mode 100644 index 000000000..34f471eca --- /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_fingerprint b/git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint new file mode 100644 index 000000000..1ccdda317 --- /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_fingerprint b/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint new file mode 100644 index 000000000..060a2c804 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint @@ -0,0 +1 @@ +SHA256:TxoYgaeIj5A7Md4rHNfxPdqawooc4NIGjIMbcQ7YKbw From 9613fbe384e9dd2b8243c0b35f3c5c04ff1d843e Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 27 Feb 2026 23:18:06 +0100 Subject: [PATCH 03/16] adds better git signature detection format Signed-off-by: Ricardo Bartels --- git/git.go | 4 - git/signatures/gpg_signature.go | 14 +++- git/signatures/gpg_signature_test.go | 14 ++++ git/signatures/signature.go | 43 ++++++++--- git/signatures/signature_test.go | 72 ++++++++++++++++++ git/signatures/ssh_signature.go | 7 +- git/signatures/ssh_signature_test.go | 110 +++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 16 deletions(-) diff --git a/git/git.go b/git/git.go index d6344eaa8..ce9b1a2c7 100644 --- a/git/git.go +++ b/git/git.go @@ -135,8 +135,6 @@ func (c *Commit) VerifyGPG(keyRings ...string) (string, 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) { - // The Encoded field already contains the commit data without the signature - // (it was encoded using EncodeWithoutSignature in BuildCommitWithRef) fingerprint, err := signatures.VerifySSHSignature(c.Signature, c.Encoded, authorizedKeys...) if err != nil { return "", fmt.Errorf("unable to verify Git commit SSH signature: %w", err) @@ -189,8 +187,6 @@ func (t *Tag) VerifyGPG(keyRings ...string) (string, error) { // 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) { - // The Encoded field already contains the tag data without the signature - // (it was encoded using EncodeWithoutSignature in BuildCommitWithRef) fingerprint, err := signatures.VerifySSHSignature(t.Signature, t.Encoded, authorizedKeys...) if err != nil { return "", fmt.Errorf("unable to verify Git tag SSH signature: %w", err) diff --git a/git/signatures/gpg_signature.go b/git/signatures/gpg_signature.go index 0abe02d8a..94c2ae2ac 100644 --- a/git/signatures/gpg_signature.go +++ b/git/signatures/gpg_signature.go @@ -25,7 +25,11 @@ import ( ) // PGPSignaturePrefix is the prefix used by Git to identify PGP signatures. -const PGPSignaturePrefix = "-----BEGIN PGP SIGNATURE-----" +// 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 @@ -35,6 +39,14 @@ func VerifyPGPSignature(signature string, payload []byte, keyRings ...string) (s 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) diff --git a/git/signatures/gpg_signature_test.go b/git/signatures/gpg_signature_test.go index 9fa2996a5..d9fe97f99 100644 --- a/git/signatures/gpg_signature_test.go +++ b/git/signatures/gpg_signature_test.go @@ -157,6 +157,20 @@ func TestVerifyPGPSignature(t *testing.T) { 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) { diff --git a/git/signatures/signature.go b/git/signatures/signature.go index 04c487b6f..0c48f73c8 100644 --- a/git/signatures/signature.go +++ b/git/signatures/signature.go @@ -24,30 +24,50 @@ import ( type SignatureType string const ( - // SignatureTypePGP represents a PGP signature. - SignatureTypePGP SignatureType = "pgp" + // 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" ) -// 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 { +// 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 } - return strings.HasPrefix(strings.TrimSpace(signature), PGPSignaturePrefix) + + 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 { - if signature == "" { - return false - } - return strings.HasPrefix(strings.TrimSpace(signature), SSHSignaturePrefix) + 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. +func Isx509Signature(signature string) bool { + return startsWithStrings(signature, X509SignaturePrefix) } // GetSignatureType returns the type of the signature as a string. @@ -60,5 +80,8 @@ func GetSignatureType(signature string) string { if IsSSHSignature(signature) { return string(SignatureTypeSSH) } + if Isx509Signature(signature) { + return string(SignatureTypeX509) + } return string(SignatureTypeUnknown) } diff --git a/git/signatures/signature_test.go b/git/signatures/signature_test.go index baa48c5b4..a649960dd 100644 --- a/git/signatures/signature_test.go +++ b/git/signatures/signature_test.go @@ -38,6 +38,16 @@ func TestIsPGPSignature(t *testing.T) { 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: "", @@ -116,6 +126,58 @@ func TestIsSSHSignature(t *testing.T) { } } +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 @@ -142,6 +204,16 @@ func TestGetSignatureType(t *testing.T) { 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: "", diff --git a/git/signatures/ssh_signature.go b/git/signatures/ssh_signature.go index c2fd9d2cc..e61f19ecb 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signatures/ssh_signature.go @@ -28,7 +28,8 @@ import ( ) // SSHSignaturePrefix is the prefix used by Git to identify SSH signatures. -const SSHSignaturePrefix = "-----BEGIN SSH SIGNATURE-----" +// 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. @@ -67,6 +68,10 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri 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 { diff --git a/git/signatures/ssh_signature_test.go b/git/signatures/ssh_signature_test.go index f364b4b09..326f2a83a 100644 --- a/git/signatures/ssh_signature_test.go +++ b/git/signatures/ssh_signature_test.go @@ -447,6 +447,9 @@ func TestVerifySSHSignature(t *testing.T) { 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) { @@ -474,6 +477,9 @@ func TestVerifySSHSignature(t *testing.T) { 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) { @@ -499,6 +505,38 @@ func TestVerifySSHSignature(t *testing.T) { 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) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + // 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) + } }) t.Run("invalid signature", func(t *testing.T) { @@ -528,6 +566,65 @@ func TestVerifySSHSignature(t *testing.T) { 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) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + // 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) + } + }) + + t.Run("invalid authorized keys", func(t *testing.T) { + // Parse the commit from the fixture file + commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + 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) + } + + // 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) + } }) } @@ -1070,6 +1167,9 @@ func TestVerifySSHSignatureForTags(t *testing.T) { 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) { @@ -1097,6 +1197,9 @@ func TestVerifySSHSignatureForTags(t *testing.T) { 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) { @@ -1122,6 +1225,10 @@ func TestVerifySSHSignatureForTags(t *testing.T) { 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("invalid signature", func(t *testing.T) { @@ -1151,6 +1258,9 @@ func TestVerifySSHSignatureForTags(t *testing.T) { 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) + } }) } From d249a1fac917015fc443c23c2477731c8836faeb Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 27 Feb 2026 23:31:11 +0100 Subject: [PATCH 04/16] adds detection for added signature prefixes Signed-off-by: Ricardo Bartels --- git/git_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/git/git_test.go b/git/git_test.go index 8fb93fd06..ba9a412e5 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -260,12 +260,19 @@ func TestCommit_IsPGPSigned(t *testing.T) { want bool }{ { - name: "PGP signed commit", + name: "PGP signed commit with SIGNATURE prefix", commit: &Commit{ Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", }, want: true, }, + { + name: "PGP signed commit with MESSAGE prefix", + commit: &Commit{ + Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + }, + want: true, + }, { name: "SSH signed commit", commit: &Commit{ @@ -273,6 +280,13 @@ func TestCommit_IsPGPSigned(t *testing.T) { }, want: false, }, + { + name: "X509 signed commit", + commit: &Commit{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: false, + }, { name: "unsigned commit", commit: &Commit{}, @@ -314,6 +328,13 @@ func TestCommit_IsSSHSigned(t *testing.T) { }, want: false, }, + { + name: "X509 signed commit", + commit: &Commit{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: false, + }, { name: "unsigned commit", commit: &Commit{}, @@ -342,11 +363,18 @@ func TestCommit_SignatureType(t *testing.T) { want string }{ { - name: "PGP signed commit", + name: "PGP signed commit with SIGNATURE prefix", commit: &Commit{ Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", }, - want: "pgp", + want: "openpgp", + }, + { + name: "PGP signed commit with MESSAGE prefix", + commit: &Commit{ + Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + }, + want: "openpgp", }, { name: "SSH signed commit", @@ -355,6 +383,13 @@ func TestCommit_SignatureType(t *testing.T) { }, want: "ssh", }, + { + name: "X509 signed commit", + commit: &Commit{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: "x509", + }, { name: "unsigned commit", commit: &Commit{}, @@ -383,12 +418,19 @@ func TestTag_IsPGPSigned(t *testing.T) { want bool }{ { - name: "PGP signed tag", + name: "PGP signed tag with SIGNATURE prefix", tag: &Tag{ Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", }, want: true, }, + { + name: "PGP signed tag with MESSAGE prefix", + tag: &Tag{ + Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + }, + want: true, + }, { name: "SSH signed tag", tag: &Tag{ @@ -396,6 +438,13 @@ func TestTag_IsPGPSigned(t *testing.T) { }, want: false, }, + { + name: "X509 signed tag", + tag: &Tag{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: false, + }, { name: "unsigned tag", tag: &Tag{}, @@ -437,6 +486,13 @@ func TestTag_IsSSHSigned(t *testing.T) { }, want: false, }, + { + name: "X509 signed tag", + tag: &Tag{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: false, + }, { name: "unsigned tag", tag: &Tag{}, @@ -465,11 +521,18 @@ func TestTag_SignatureType(t *testing.T) { want string }{ { - name: "PGP signed tag", + name: "PGP signed tag with SIGNATURE prefix", tag: &Tag{ Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", }, - want: "pgp", + want: "openpgp", + }, + { + name: "PGP signed tag with MESSAGE prefix", + tag: &Tag{ + Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + }, + want: "openpgp", }, { name: "SSH signed tag", @@ -478,6 +541,13 @@ func TestTag_SignatureType(t *testing.T) { }, want: "ssh", }, + { + name: "X509 signed tag", + tag: &Tag{ + Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + }, + want: "x509", + }, { name: "unsigned tag", tag: &Tag{}, From 79b31e95f035b837e7e52a074a27cd9d674ec1fb Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 27 Feb 2026 23:54:26 +0100 Subject: [PATCH 05/16] adds tests for git.go Signed-off-by: Ricardo Bartels --- git/git.go | 2 + git/git_test.go | 680 ++++++++++++++---- git/internal/e2e/go.mod | 1 + git/internal/e2e/go.sum | 2 + git/signatures/gpg_signature_test.go | 11 +- git/signatures/ssh_signature_test.go | 45 +- .../gpg_signatures/generate_gpg_fixtures.sh | 10 +- .../ssh_signatures/calc_fingerprints.go | 35 - .../fixtures.go} | 10 +- tests/integration/go.mod | 1 + tests/integration/go.sum | 2 + 11 files changed, 576 insertions(+), 223 deletions(-) delete mode 100644 git/signatures/testdata/ssh_signatures/calc_fingerprints.go rename git/{signatures/testutils_test.go => testutils/fixtures.go} (84%) diff --git a/git/git.go b/git/git.go index ce9b1a2c7..1d53cd33d 100644 --- a/git/git.go +++ b/git/git.go @@ -112,6 +112,7 @@ 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...) @@ -168,6 +169,7 @@ 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...) diff --git a/git/git_test.go b/git/git_test.go index ba9a412e5..d1869b520 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -17,12 +17,27 @@ 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 ( + 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) { tests := []struct { name string @@ -253,152 +268,297 @@ func TestIsConcreteCommit(t *testing.T) { } } -func TestCommit_IsPGPSigned(t *testing.T) { +func TestIsAnnotatedTag(t *testing.T) { tests := []struct { name string - commit *Commit - want bool + tag Tag + result bool }{ { - name: "PGP signed commit with SIGNATURE prefix", - commit: &Commit{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + name: "annotated tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Encoded: []byte("tag-content"), }, - want: true, + result: true, }, { - name: "PGP signed commit with MESSAGE prefix", - commit: &Commit{ - Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + name: "lightweight tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", }, - want: true, + result: false, }, { - name: "SSH signed commit", - commit: &Commit{ - Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + name: "empty encoded", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Encoded: []byte{}, }, - want: false, + 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: "X509 signed commit", - commit: &Commit{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + name: "signed tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Signature: signaturePGPSignature, }, - want: false, + result: true, }, { - name: "unsigned commit", - commit: &Commit{}, - want: false, + name: "unsigned tag", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + }, + result: false, }, { - name: "PGP signed commit with leading whitespace", - commit: &Commit{ - Signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + name: "empty signature", + tag: Tag{ + Hash: Hash("foo"), + Name: "v1.0.0", + Signature: "", }, - want: true, + result: false, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - g.Expect(tt.commit.IsPGPSigned()).To(Equal(tt.want)) + g.Expect(IsSignedTag(tt.tag)).To(Equal(tt.result)) }) } } -func TestCommit_IsSSHSigned(t *testing.T) { +func TestTag_String(t *testing.T) { tests := []struct { - name string - commit *Commit - want bool + name string + tag *Tag + want string }{ { - name: "SSH signed commit", + 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: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + Signature: signaturePGPMessage, + }, + tag: &Tag{ + Signature: signaturePGPMessage, }, - want: true, + wantPGPCommit: true, + wantSSHCommit: false, + wantPGPTag: true, + wantSSHTag: false, }, { - name: "PGP signed commit", + name: "SSH signed", commit: &Commit{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + Signature: signatureSSH, }, - want: false, + tag: &Tag{ + Signature: signatureSSH, + }, + wantPGPCommit: false, + wantSSHCommit: true, + wantPGPTag: false, + wantSSHTag: true, }, { - name: "X509 signed commit", + name: "X509 signed", commit: &Commit{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + Signature: signatureX509, }, - want: false, + tag: &Tag{ + Signature: signatureX509, + }, + wantPGPCommit: false, + wantSSHCommit: false, + wantPGPTag: false, + wantSSHTag: false, }, { - name: "unsigned commit", - commit: &Commit{}, - want: false, + name: "unsigned", + commit: &Commit{}, + tag: &Tag{}, + wantPGPCommit: false, + wantSSHCommit: false, + wantPGPTag: false, + wantSSHTag: false, }, { - name: "SSH signed commit with leading whitespace", + name: "PGP signed with leading whitespace", commit: &Commit{ - Signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + Signature: signaturePGPSignatureWithLeadingWhitespace, + }, + tag: &Tag{ + Signature: signaturePGPSignatureWithLeadingWhitespace, }, - want: true, + 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.IsSSHSigned()).To(Equal(tt.want)) + 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 TestCommit_SignatureType(t *testing.T) { +func TestSignatureType(t *testing.T) { tests := []struct { name string commit *Commit + tag *Tag want string }{ { - name: "PGP signed commit with SIGNATURE prefix", + name: "PGP signed with SIGNATURE prefix", commit: &Commit{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", + Signature: signaturePGPSignature, + }, + tag: &Tag{ + Signature: signaturePGPSignature, }, want: "openpgp", }, { - name: "PGP signed commit with MESSAGE prefix", + name: "PGP signed with MESSAGE prefix", commit: &Commit{ - Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", + Signature: signaturePGPMessage, + }, + tag: &Tag{ + Signature: signaturePGPMessage, }, want: "openpgp", }, { - name: "SSH signed commit", + name: "SSH signed", commit: &Commit{ - Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", + Signature: signatureSSH, + }, + tag: &Tag{ + Signature: signatureSSH, }, want: "ssh", }, { - name: "X509 signed commit", + name: "X509 signed", commit: &Commit{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + Signature: signatureX509, + }, + tag: &Tag{ + Signature: signatureX509, }, want: "x509", }, { - name: "unsigned commit", + name: "unsigned", commit: &Commit{}, + tag: &Tag{}, want: "unknown", }, { name: "unknown signature type", commit: &Commit{ - Signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + Signature: signatureUnknown, + }, + tag: &Tag{ + Signature: signatureUnknown, }, want: "unknown", }, @@ -407,164 +567,380 @@ func TestCommit_SignatureType(t *testing.T) { 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 TestTag_IsPGPSigned(t *testing.T) { +func TestCommit_VerifyGPG(t *testing.T) { + testDataDir := filepath.Join("signatures", "testdata", "gpg_signatures") + tests := []struct { - name string - tag *Tag - want bool + name string + sigFile string + keyFile string + wantErr string }{ { - name: "PGP signed tag with SIGNATURE prefix", - tag: &Tag{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - }, - want: true, + name: "valid PGP signature", + sigFile: "commit_rsa_2048_signed.txt", + keyFile: "key_rsa_2048.pub", }, { - name: "PGP signed tag with MESSAGE prefix", - tag: &Tag{ - Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", - }, - want: true, + 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: "SSH signed tag", - tag: &Tag{ - Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - }, - want: false, + 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: "X509 signed tag", - tag: &Tag{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", - }, - want: false, + 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: "unsigned tag", - tag: &Tag{}, - want: false, + name: "valid PGP signature", + sigFile: "tag_rsa_2048_signed.txt", + keyFile: "key_rsa_2048.pub", }, { - name: "PGP signed tag with leading whitespace", - tag: &Tag{ - Signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - }, - want: true, + 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) - g.Expect(tt.tag.IsPGPSigned()).To(Equal(tt.want)) + + // 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 TestTag_IsSSHSigned(t *testing.T) { +func TestCommit_VerifySSH(t *testing.T) { + testDataDir := filepath.Join("signatures", "testdata", "ssh_signatures") + tests := []struct { - name string - tag *Tag - want bool + name string + sigFile string + authorizedKeys string + wantErr string }{ { - name: "SSH signed tag", - tag: &Tag{ - Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - }, - want: true, + name: "valid SSH signature", + sigFile: "commit_rsa_signed.txt", + authorizedKeys: "authorized_keys_rsa", }, { - name: "PGP signed tag", - tag: &Tag{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - }, - want: false, + name: "missing signature", + sigFile: "commit_unsigned.txt", + authorizedKeys: "authorized_keys_rsa", + wantErr: "unable to verify Git commit SSH signature: unable to verify payload as the provided signature is empty", }, { - name: "X509 signed tag", - tag: &Tag{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", - }, - want: false, + name: "invalid signature", + sigFile: "commit_rsa_signed.txt", + authorizedKeys: "authorized_keys_ed25519", + wantErr: "unable to verify Git commit SSH signature: unable to verify payload with any of the given authorized keys", }, { - name: "unsigned tag", - tag: &Tag{}, - want: false, - }, - { - name: "SSH signed tag with leading whitespace", - tag: &Tag{ - Signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - }, - want: true, + 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) - g.Expect(tt.tag.IsSSHSigned()).To(Equal(tt.want)) + + // 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_SignatureType(t *testing.T) { +func TestTag_VerifySSH(t *testing.T) { + testDataDir := filepath.Join("signatures", "testdata", "ssh_signatures") + tests := []struct { - name string - tag *Tag - want string + name string + sigFile string + authorizedKeys string + wantErr string }{ { - name: "PGP signed tag with SIGNATURE prefix", - tag: &Tag{ - Signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - }, - want: "openpgp", + name: "valid SSH signature", + sigFile: "tag_rsa_signed.txt", + authorizedKeys: "authorized_keys_rsa", }, { - name: "PGP signed tag with MESSAGE prefix", - tag: &Tag{ - Signature: "-----BEGIN PGP MESSAGE-----\n-----END PGP MESSAGE-----", - }, - want: "openpgp", + name: "missing signature", + sigFile: "commit_unsigned.txt", + authorizedKeys: "authorized_keys_rsa", + wantErr: "unable to verify Git tag SSH signature: unable to verify payload as the provided signature is empty", }, { - name: "SSH signed tag", - tag: &Tag{ - Signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - }, - want: "ssh", + name: "invalid signature", + sigFile: "tag_rsa_signed.txt", + authorizedKeys: "authorized_keys_ed25519", + wantErr: "unable to verify Git tag SSH signature: unable to verify payload with any of the given authorized keys", }, { - name: "X509 signed tag", - tag: &Tag{ - Signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", + 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: "x509", + want: "repository not found: git repository: 'https://github.com/example/repo.git'", }, { - name: "unsigned tag", - tag: &Tag{}, - want: "unknown", + name: "with empty message", + err: ErrRepositoryNotFound{ + Message: "", + URL: "https://github.com/example/repo.git", + }, + want: ": git repository: 'https://github.com/example/repo.git'", }, { - name: "unknown signature type", - tag: &Tag{ - Signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", + name: "with empty URL", + err: ErrRepositoryNotFound{ + Message: "repository not found", + URL: "", }, - want: "unknown", + want: "repository not found: git repository: ''", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - g.Expect(tt.tag.SignatureType()).To(Equal(tt.want)) + g.Expect(tt.err.Error()).To(Equal(tt.want)) }) } } diff --git a/git/internal/e2e/go.mod b/git/internal/e2e/go.mod index 9a7104448..4af9bafd3 100644 --- a/git/internal/e2e/go.mod +++ b/git/internal/e2e/go.mod @@ -49,6 +49,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hiddeco/sshsig v0.2.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect diff --git a/git/internal/e2e/go.sum b/git/internal/e2e/go.sum index 6e222880c..2dc636fb8 100644 --- a/git/internal/e2e/go.sum +++ b/git/internal/e2e/go.sum @@ -77,6 +77,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= diff --git a/git/signatures/gpg_signature_test.go b/git/signatures/gpg_signature_test.go index d9fe97f99..b5a86617e 100644 --- a/git/signatures/gpg_signature_test.go +++ b/git/signatures/gpg_signature_test.go @@ -23,6 +23,7 @@ import ( "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" ) @@ -223,7 +224,7 @@ func TestVerifyPGPSignatureWithFixturesForTags(t *testing.T) { g := NewWithT(t) // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Tag using BuildTag @@ -281,7 +282,7 @@ func TestVerifyPGPSignatureWithFixtures(t *testing.T) { g := NewWithT(t) // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Commit using BuildCommitWithRef @@ -310,7 +311,7 @@ func TestVerifyPGPSignatureWithFixtures(t *testing.T) { g := NewWithT(t) // Parse the unsigned commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Commit using BuildCommitWithRef @@ -377,7 +378,7 @@ func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { g := NewWithT(t) // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Commit using BuildCommitWithRef @@ -402,7 +403,7 @@ func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { g := NewWithT(t) // Parse the unsigned commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_unsigned.txt")) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Commit using BuildCommitWithRef diff --git a/git/signatures/ssh_signature_test.go b/git/signatures/ssh_signature_test.go index 326f2a83a..82c2f1320 100644 --- a/git/signatures/ssh_signature_test.go +++ b/git/signatures/ssh_signature_test.go @@ -24,6 +24,7 @@ import ( "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/hiddeco/sshsig" ) @@ -389,7 +390,7 @@ func TestVerifySSHSignature(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -429,7 +430,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -459,7 +460,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -484,7 +485,7 @@ func TestVerifySSHSignature(t *testing.T) { t.Run("wrong authorized keys", func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -513,7 +514,7 @@ func TestVerifySSHSignature(t *testing.T) { t.Run("empty authorized keys", func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -546,7 +547,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -573,7 +574,7 @@ func TestVerifySSHSignature(t *testing.T) { t.Run("non-SSH signature", func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -601,7 +602,7 @@ func TestVerifySSHSignature(t *testing.T) { t.Run("invalid authorized keys", func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -648,7 +649,7 @@ func TestVerifySSHSignatureAllKeyTypes(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -706,7 +707,7 @@ func TestVerifySSHSignatureCombinedKeys(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -783,7 +784,7 @@ func TestBuildCommitWithRefFromFixture(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -894,7 +895,7 @@ func TestBuildCommitWithRefAndVerifySSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse the commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -932,7 +933,7 @@ func TestBuildCommitWithRefWithDifferentRefs(t *testing.T) { testDataDir := filepath.Join("testdata", "ssh_signatures") // Parse a signed commit from the fixture file - commitObj, err := parseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse commit from fixture: %v", err) } @@ -1027,7 +1028,7 @@ func TestBuildTagFromFixture(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1109,7 +1110,7 @@ func TestVerifySSHSignatureForTags(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1149,7 +1150,7 @@ func TestVerifySSHSignatureForTags(t *testing.T) { } // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1179,7 +1180,7 @@ func TestVerifySSHSignatureForTags(t *testing.T) { } // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1204,7 +1205,7 @@ func TestVerifySSHSignatureForTags(t *testing.T) { t.Run("wrong authorized keys", func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1238,7 +1239,7 @@ func TestVerifySSHSignatureForTags(t *testing.T) { } // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1284,7 +1285,7 @@ func TestVerifySSHSignatureForTagsAllKeyTypes(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1342,7 +1343,7 @@ func TestVerifySSHSignatureForTagsCombinedKeys(t *testing.T) { for _, kt := range keyTypes { t.Run(kt.name, func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -1413,7 +1414,7 @@ func TestBuildTagAndVerifySSH(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse the tag from the fixture file - tagObj, err := parseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh index 05eaa6a56..66de6958e 100755 --- a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -62,7 +62,8 @@ EOF gpg --batch --generate-key "$batch_file" 2>&1 # Get the key ID - local key_id=$(gpg --list-keys --with-colons "test-${key_name}@example.com" | grep '^fpr' | head -1 | cut -d: -f10) + 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" @@ -88,7 +89,8 @@ create_signed_object() { echo "Creating signed $object_type for $key_name..." # Get key ID - local key_id=$(cat "$TEMP_DIR/${key_name}_id.txt") + 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}" @@ -201,10 +203,10 @@ main() { echo "----------------------------------------" # Get list of successfully generated keys - local keys=() + local keys=() key_name="" for key_file in "$TEMP_DIR"/*_id.txt; do if [[ -f "$key_file" ]]; then - local key_name=$(basename "$key_file" "_id.txt") + key_name=$(basename "$key_file" "_id.txt") keys+=("$key_name") fi done diff --git a/git/signatures/testdata/ssh_signatures/calc_fingerprints.go b/git/signatures/testdata/ssh_signatures/calc_fingerprints.go deleted file mode 100644 index 2cd8bf8d6..000000000 --- a/git/signatures/testdata/ssh_signatures/calc_fingerprints.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/base64" - "fmt" - "strings" - - gossh "golang.org/x/crypto/ssh" -) - -func main() { - keys := []struct { - name string - key string - }{ - {"test_key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com"}, - {"ed25519", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com"}, - {"rsa", "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_p256", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com"}, - {"ecdsa_p384", "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com"}, - {"ecdsa_p521", "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com"}, - } - - for _, k := range keys { - pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(k.key)) - if err != nil { - fmt.Printf("Error parsing %s: %v\n", k.name, err) - continue - } - hash := sha256.Sum256(pubKey.Marshal()) - fingerprint := "SHA256:" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(hash[:]), "=") - fmt.Printf("%s: %s\n", k.name, fingerprint) - } -} diff --git a/git/signatures/testutils_test.go b/git/testutils/fixtures.go similarity index 84% rename from git/signatures/testutils_test.go rename to git/testutils/fixtures.go index 7088f741d..da5327f20 100644 --- a/git/signatures/testutils_test.go +++ b/git/testutils/fixtures.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures_test +package testutils import ( "os" @@ -23,8 +23,8 @@ import ( "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) { +// 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 @@ -46,8 +46,8 @@ func parseCommitFromFixture(fixturePath string) (*object.Commit, error) { return commit, nil } -// parseTagFromFixture parses a git tag object from a fixture file -func parseTagFromFixture(fixturePath string) (*object.Tag, error) { +// 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 diff --git a/tests/integration/go.mod b/tests/integration/go.mod index cbd4f75a6..a02f511c2 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -115,6 +115,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect + github.com/hiddeco/sshsig v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/tests/integration/go.sum b/tests/integration/go.sum index bf58da861..69e3bf74e 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -213,6 +213,8 @@ github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5 github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= From e2adc4aeef7efcf1463f9356270609d4d0ce6313 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Sat, 28 Feb 2026 12:55:13 +0100 Subject: [PATCH 06/16] adds requested changes regarding naming Signed-off-by: Ricardo Bartels --- git/signatures/signature.go | 8 ++++---- git/signatures/signature_test.go | 6 +++--- git/signatures/ssh_signature.go | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/git/signatures/signature.go b/git/signatures/signature.go index 0c48f73c8..3e4842a69 100644 --- a/git/signatures/signature.go +++ b/git/signatures/signature.go @@ -34,7 +34,7 @@ const ( SignatureTypeUnknown SignatureType = "unknown" ) -// Isx509Signature is the prefix used by Git to identify x509 signatures. +// 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-----"} @@ -64,9 +64,9 @@ func IsSSHSignature(signature string) bool { return startsWithStrings(signature, SSHSignaturePrefix) } -// Isx509Signature tests if the given signature is of type x509. +// IsX509Signature tests if the given signature is of type x509. // It returns true if the signature starts with the x509 signature prefix. -func Isx509Signature(signature string) bool { +func IsX509Signature(signature string) bool { return startsWithStrings(signature, X509SignaturePrefix) } @@ -80,7 +80,7 @@ func GetSignatureType(signature string) string { if IsSSHSignature(signature) { return string(SignatureTypeSSH) } - if Isx509Signature(signature) { + if IsX509Signature(signature) { return string(SignatureTypeX509) } return string(SignatureTypeUnknown) diff --git a/git/signatures/signature_test.go b/git/signatures/signature_test.go index a649960dd..4350421a4 100644 --- a/git/signatures/signature_test.go +++ b/git/signatures/signature_test.go @@ -126,7 +126,7 @@ func TestIsSSHSignature(t *testing.T) { } } -func TestIsx509Signature(t *testing.T) { +func TestIsX509Signature(t *testing.T) { tests := []struct { name string signature string @@ -171,8 +171,8 @@ func TestIsx509Signature(t *testing.T) { 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) + if got := IsX509Signature(tt.signature); got != tt.want { + t.Errorf("IsX509Signature() = %v, want %v", got, tt.want) } }) } diff --git a/git/signatures/ssh_signature.go b/git/signatures/ssh_signature.go index e61f19ecb..2561fff10 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signatures/ssh_signature.go @@ -103,9 +103,9 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri return "", fmt.Errorf("unable to verify payload with any of the given authorized keys") } -// getPublicKeyFingerprint returns the SHA256 fingerprint of the public key +// 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:" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(hash[:]), "=") + return "SHA256:" + base64.RawStdEncoding.EncodeToString(hash[:]) } From e21d896a20d82d019f45186647c13f29e6ebb92b Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 3 Mar 2026 00:08:43 +0100 Subject: [PATCH 07/16] cleans up tests and removed duplicate test files Signed-off-by: Ricardo Bartels --- git/signatures/gpg_signature_test.go | 181 +-- git/signatures/signature.go | 2 + git/signatures/ssh_signature.go | 15 +- git/signatures/ssh_signature_keys_test.go | 294 ++++ git/signatures/ssh_signature_test.go | 1441 +++-------------- .../testdata/ssh_signatures/README.md | 9 +- .../ssh_signatures/authorized_keys_ecdsa_p256 | 1 - .../ssh_signatures/authorized_keys_ecdsa_p384 | 1 - .../ssh_signatures/authorized_keys_ecdsa_p521 | 1 - .../ssh_signatures/authorized_keys_ed25519 | 1 - .../ssh_signatures/authorized_keys_rsa | 1 - .../ssh_signatures/generate_ssh_fixtures.sh | 29 +- .../{authorized_keys_all => keys_all.pub} | 0 13 files changed, 553 insertions(+), 1423 deletions(-) create mode 100644 git/signatures/ssh_signature_keys_test.go delete mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 delete mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 delete mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 delete mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 delete mode 100644 git/signatures/testdata/ssh_signatures/authorized_keys_rsa rename git/signatures/testdata/ssh_signatures/{authorized_keys_all => keys_all.pub} (100%) diff --git a/git/signatures/gpg_signature_test.go b/git/signatures/gpg_signature_test.go index b5a86617e..22ea81575 100644 --- a/git/signatures/gpg_signature_test.go +++ b/git/signatures/gpg_signature_test.go @@ -191,40 +191,50 @@ func TestVerifyPGPSignature(t *testing.T) { } } -func TestVerifyPGPSignatureWithFixturesForTags(t *testing.T) { +func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { testDataDir := filepath.Join("testdata", "gpg_signatures") // Test cases for each key type using fixtures keyTypes := []struct { - name string - sigFile string - keyFile string - wantErr bool + name string + commitFile string + tagFile string + keyFile string + wantErr bool }{ - {"rsa_2048 valid tag signature", "tag_rsa_2048_signed.txt", "key_rsa_2048.pub", false}, - {"rsa_4096 valid tag signature", "tag_rsa_4096_signed.txt", "key_rsa_4096.pub", false}, - {"ed25519 valid tag signature", "tag_ed25519_signed.txt", "key_ed25519.pub", false}, - {"ecdsa_p256 valid tag signature", "tag_ecdsa_p256_signed.txt", "key_ecdsa_p256.pub", false}, - {"ecdsa_p384 valid tag signature", "tag_ecdsa_p384_signed.txt", "key_ecdsa_p384.pub", false}, - {"ecdsa_p521 valid tag signature", "tag_ecdsa_p521_signed.txt", "key_ecdsa_p521.pub", false}, - {"dsa_2048 valid tag signature", "tag_dsa_2048_signed.txt", "key_dsa_2048.pub", false}, - {"brainpool_p256 valid tag signature", "tag_brainpool_p256_signed.txt", "key_brainpool_p256.pub", false}, - {"brainpool_p384 valid tag signature", "tag_brainpool_p384_signed.txt", "key_brainpool_p384.pub", false}, - {"brainpool_p512 valid tag signature", "tag_brainpool_p512_signed.txt", "key_brainpool_p512.pub", false}, + {"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}, + {"dsa_2048 valid signature", "commit_dsa_2048_signed.txt", "tag_dsa_2048_signed.txt", "key_dsa_2048.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 tag signature", "tag_ed448_signed.txt", "key_ed448.pub", true}, + {"ed448 valid signature", "commit_ed448_signed.txt", "tag_ed448_signed.txt", "key_ed448.pub", true}, } + var allKeysRing []string for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { + 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.sigFile)) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.tagFile)) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Tag using BuildTag @@ -245,44 +255,27 @@ func TestVerifyPGPSignatureWithFixturesForTags(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(fingerprint).ToNot(BeEmpty()) - }) - } -} -func TestVerifyPGPSignatureWithFixtures(t *testing.T) { - testDataDir := filepath.Join("testdata", "gpg_signatures") + // 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 + } - // Test cases for each key type using fixtures - keyTypes := []struct { - name string - sigFile string - keyFile string - wantErr bool - }{ - {"rsa_2048 valid signature", "commit_rsa_2048_signed.txt", "key_rsa_2048.pub", false}, - {"rsa_4096 valid signature", "commit_rsa_4096_signed.txt", "key_rsa_4096.pub", false}, - {"ed25519 valid signature", "commit_ed25519_signed.txt", "key_ed25519.pub", false}, - {"ecdsa_p256 valid signature", "commit_ecdsa_p256_signed.txt", "key_ecdsa_p256.pub", false}, - {"ecdsa_p384 valid signature", "commit_ecdsa_p384_signed.txt", "key_ecdsa_p384.pub", false}, - {"ecdsa_p521 valid signature", "commit_ecdsa_p521_signed.txt", "key_ecdsa_p521.pub", false}, - {"dsa_2048 valid signature", "commit_dsa_2048_signed.txt", "key_dsa_2048.pub", false}, - {"brainpool_p256 valid signature", "commit_brainpool_p256_signed.txt", "key_brainpool_p256.pub", false}, - {"brainpool_p384 valid signature", "commit_brainpool_p384_signed.txt", "key_brainpool_p384.pub", false}, - {"brainpool_p512 valid signature", "commit_brainpool_p512_signed.txt", "key_brainpool_p512.pub", false}, + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(fingerprint).ToNot(BeEmpty()) - // 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", "key_ed448.pub", true}, + }) } for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { + 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.sigFile)) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.commitFile)) g.Expect(err).ToNot(HaveOccurred()) // Build a git.Commit using BuildCommitWithRef @@ -303,90 +296,9 @@ func TestVerifyPGPSignatureWithFixtures(t *testing.T) { 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()) - }) -} - -func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { - testDataDir := filepath.Join("testdata", "gpg_signatures") - - // Read multiple public keys to create a multi-key keyring - keyFiles := []string{ - "key_rsa_2048.pub", - "key_rsa_4096.pub", - "key_ed25519.pub", - "key_ecdsa_p256.pub", - "key_ecdsa_p384.pub", - "key_ecdsa_p521.pub", - "key_dsa_2048.pub", - "key_brainpool_p256.pub", - "key_brainpool_p384.pub", - "key_brainpool_p512.pub", - } - - var keyRings []string - for _, keyFile := range keyFiles { - publicKey, err := os.ReadFile(filepath.Join(testDataDir, keyFile)) - if err != nil { - t.Fatalf("failed to read public key file %s: %v", keyFile, err) - } - keyRings = append(keyRings, string(publicKey)) - } - - // Test cases for each key type using the multi-key keyring - keyTypes := []struct { - name string - sigFile string - wantErr bool - }{ - {"rsa_2048 valid signature with multi-key keyring", "commit_rsa_2048_signed.txt", false}, - {"rsa_4096 valid signature with multi-key keyring", "commit_rsa_4096_signed.txt", false}, - {"ed25519 valid signature with multi-key keyring", "commit_ed25519_signed.txt", false}, - {"ecdsa_p256 valid signature with multi-key keyring", "commit_ecdsa_p256_signed.txt", false}, - {"ecdsa_p384 valid signature with multi-key keyring", "commit_ecdsa_p384_signed.txt", false}, - {"ecdsa_p521 valid signature with multi-key keyring", "commit_ecdsa_p521_signed.txt", false}, - {"dsa_2048 valid signature with multi-key keyring", "commit_dsa_2048_signed.txt", false}, - {"brainpool_p256 valid signature with multi-key keyring", "commit_brainpool_p256_signed.txt", false}, - {"brainpool_p384 valid signature with multi-key keyring", "commit_brainpool_p384_signed.txt", false}, - {"brainpool_p512 valid signature with multi-key keyring", "commit_brainpool_p512_signed.txt", false}, - } - - for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { - g := NewWithT(t) - - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, kt.sigFile)) - 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()) // Verify the signature using the multi-key keyring - fingerprint, err := signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, keyRings...) + fingerprint, err = signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, allKeysRing...) if kt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(fingerprint).To(BeEmpty()) @@ -395,11 +307,12 @@ func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(fingerprint).ToNot(BeEmpty()) + }) } - // Test that an unsigned commit fails with multi-key keyring - t.Run("unsigned commit with multi-key keyring", func(t *testing.T) { + // Test error cases + t.Run("unsigned commit", func(t *testing.T) { g := NewWithT(t) // Parse the unsigned commit from the fixture file @@ -410,8 +323,12 @@ func TestVerifyPGPSignatureWithMultipleKeyRing(t *testing.T) { 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, keyRings...) + 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 index 3e4842a69..f85d4d03c 100644 --- a/git/signatures/signature.go +++ b/git/signatures/signature.go @@ -66,6 +66,8 @@ func IsSSHSignature(signature string) bool { // 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) } diff --git a/git/signatures/ssh_signature.go b/git/signatures/ssh_signature.go index 2561fff10..c3a146cbf 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signatures/ssh_signature.go @@ -89,13 +89,10 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri for _, pubKey := range publicKeys { // Verify the signature using sshsig library // The namespace for Git is "git" - // Git supports both SHA256 and SHA512, so we try both - for _, hashAlgo := range []sshsig.HashAlgorithm{sshsig.HashSHA256, sshsig.HashSHA512} { - err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, hashAlgo, "git") - if err == nil { - // Signature verified successfully - return GetPublicKeyFingerprint(pubKey), nil - } + err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, sig.HashAlgorithm, "git") + if err == nil { + // Signature verified successfully + return getPublicKeyFingerprint(pubKey), nil } } } @@ -103,9 +100,9 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri return "", fmt.Errorf("unable to verify payload with any of the given authorized keys") } -// GetPublicKeyFingerprint returns the SHA256 fingerprint of the public key +// 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 { +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 000000000..3761d469a --- /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 index 82c2f1320..80cacb12b 100644 --- a/git/signatures/ssh_signature_test.go +++ b/git/signatures/ssh_signature_test.go @@ -26,422 +26,203 @@ import ( "github.com/fluxcd/pkg/git/signatures" "github.com/fluxcd/pkg/git/testutils" "github.com/go-git/go-git/v5/plumbing" - "github.com/hiddeco/sshsig" ) -func TestParseAuthorizedKeys(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 +// these tests are in a different package to avoid circular dependencies with gogit.BuildCommitWithRef and gogit.BuildTag -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{}, - }, - } +func TestVerifySSHSignature(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - keys, err := signatures.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 := signatures.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) - } - } - } - }) + pubKeysAll, err := os.ReadFile(filepath.Join(testDataDir, "keys_all.pub")) + if err != nil { + t.Fatalf("Failed to read combined authorized keys: %v", err) } -} - -func TestParseAuthorizedKeysFromFixtures(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - tests := []struct { - name string - fixture string - fingerprintFile string - wantCount int - wantErr bool + // Test cases for each key type using fixtures + keyTypes := []struct { + name string + signedCommitFile string + signedTagFile string + pubKeyFile string + fingerPrintFile string }{ { - name: "ed25519 key", - fixture: "authorized_keys_ed25519", - fingerprintFile: "key_ed25519.pub_fingerprint", - wantCount: 1, - wantErr: false, - }, - { - name: "rsa key", - fixture: "authorized_keys_rsa", - fingerprintFile: "key_rsa.pub_fingerprint", - wantCount: 1, - wantErr: false, + name: "ed25519 valid signature", + signedCommitFile: "commit_ed25519_signed.txt", + signedTagFile: "tag_ed25519_signed.txt", + pubKeyFile: "key_ed25519.pub", + fingerPrintFile: "key_ed25519.pub_fingerprint", }, { - name: "ecdsa p256 key", - fixture: "authorized_keys_ecdsa_p256", - fingerprintFile: "key_ecdsa_p256.pub_fingerprint", - wantCount: 1, - wantErr: false, + 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 p384 key", - fixture: "authorized_keys_ecdsa_p384", - fingerprintFile: "key_ecdsa_p384.pub_fingerprint", - wantCount: 1, - wantErr: false, + 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 p521 key", - fixture: "authorized_keys_ecdsa_p521", - fingerprintFile: "key_ecdsa_p521.pub_fingerprint", - wantCount: 1, - wantErr: false, + 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: "all key types combined", - fixture: "authorized_keys_all", - wantCount: 5, - wantErr: false, + 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 _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.fixture)) + 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 read fixture file %s: %v", tt.fixture, err) + t.Fatalf("Failed to parse commit from fixture: %v", err) } - keys, err := signatures.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) + // 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) } - // 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)) + // 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) } - // Verify that each key has a valid fingerprint - for i, key := range keys { - fingerprint := signatures.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) - } - } + // 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) } - }) - } -} - -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{"authorized_keys_ed25519", "authorized_keys_rsa"}, - wantCount: 2, - wantErr: false, - }, - { - name: "ed25519 + ecdsa p256", - fixtures: []string{"authorized_keys_ed25519", "authorized_keys_ecdsa_p256"}, - wantCount: 2, - wantErr: false, - }, - { - name: "rsa + ecdsa p384 + ecdsa p521", - fixtures: []string{"authorized_keys_rsa", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, - wantCount: 3, - wantErr: false, - }, - { - name: "all ecdsa variants", - fixtures: []string{"authorized_keys_ecdsa_p256", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, - wantCount: 3, - wantErr: false, - }, - { - name: "ed25519 + rsa + all ecdsa", - fixtures: []string{"authorized_keys_ed25519", "authorized_keys_rsa", "authorized_keys_ecdsa_p256", "authorized_keys_ecdsa_p384", "authorized_keys_ecdsa_p521"}, - wantCount: 5, - wantErr: false, - }, - } + // 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) + } - 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") + 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)) - keys, err := signatures.ParseAuthorizedKeys(combinedKeys.String()) - if (err != nil) != tt.wantErr { - t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) - return + // 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 len(keys) != tt.wantCount { - t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + 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 that each key has a valid fingerprint - for i, key := range keys { - fingerprint := signatures.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) - } + // 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) } - }) - } -} -func TestParseSSHSignature(t *testing.T) { - tests := []struct { - name string - sig string - wantErr bool - }{ - { - name: "valid signature with PEM armor", - sig: `-----BEGIN SSH SIGNATURE----- -U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg9uahUwBLlO2Dvuz0MtIA5/iBcK -JCmB1F6QUeXNtccscAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 -AAAAQFb88f1ZXOK1BByC4QQOthH9bZP0/hMcPl62h4oIuEny6W5xd/oOpDv7dmj9A6DiMS -o6RLdWlvb81l/UyYhGEwE= ------END SSH SIGNATURE-----`, - wantErr: false, - }, - { - name: "valid signature without PEM armor", - sig: "U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg9uahUwBLlO2Dvuz0MtIA5/iBcKJCmB1F6QUeXNtccscAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5AAAAQFb88f1ZXOK1BByC4QQOthH9bZP0/hMcPl62h4oIuEny6W5xd/oOpDv7dmj9A6DiMSo6RLdWlvb81l/UyYhGEwE=", - wantErr: true, // sshsig.Unarmor() requires PEM armor - }, - { - name: "empty signature", - sig: "", - wantErr: true, - }, - { - name: "invalid base64", - sig: "-----BEGIN SSH SIGNATURE-----\ninvalid-base64!!!\n-----END SSH SIGNATURE-----", - wantErr: true, - }, - { - name: "invalid format", - sig: "invalid-signature-format", - wantErr: true, - }, - } + // 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) + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sig, err := sshsig.Unarmor([]byte(tt.sig)) - if (err != nil) != tt.wantErr { - t.Errorf("sshsig.Unarmor() error = %v, wantErr %v", err, tt.wantErr) - return + // 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 !tt.wantErr && sig == nil { - t.Errorf("sshsig.Unarmor() returned nil signature") + if fingerprint != expectedFingerprint { + t.Errorf("Tag signature VerifySSHSignature() fingerprint mismatch, got '%s', want '%s'", fingerprint, expectedFingerprint) } + }) } } -func TestGetPublicKeyFingerprint(t *testing.T) { - // Test with a known public key - pubKeyStr := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com" - expectedFingerprint := "SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM" - keys, err := signatures.ParseAuthorizedKeys(pubKeyStr) +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 parse test public key: %v", err) - } - if len(keys) == 0 { - t.Fatal("No keys parsed") + t.Fatalf("Failed to read authorized keys: %v", err) } - fingerprint := signatures.GetPublicKeyFingerprint(keys[0]) - if fingerprint == "" { - t.Error("GetPublicKeyFingerprint() returned empty string") - } - if !strings.HasPrefix(fingerprint, expectedFingerprint) { - t.Errorf("GetPublicKeyFingerprint() = %s, want prefix SHA256:", fingerprint) + // 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) } -} - -func TestVerifySSHSignature(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - // Test cases for each key type using fixtures - keyTypes := []struct { - name string - sigFile string - authFile string - wantErr bool - }{ - {"ed25519 valid signature", "commit_ed25519_signed.txt", "authorized_keys_ed25519", false}, - {"rsa valid signature", "commit_rsa_signed.txt", "authorized_keys_rsa", false}, - {"ecdsa_p256 valid signature", "commit_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, - {"ecdsa_p384 valid signature", "commit_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, - {"ecdsa_p521 valid signature", "commit_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, + // 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) } - 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.sigFile)) - 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) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %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) + } - // Verify the signature using the git.Commit's Signature and Encoded fields - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) + // 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) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - 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_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + fingerprint, err := signatures.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") } - - // 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) + 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(authorizedKeys)) + fingerprint, err = signatures.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") } @@ -451,27 +232,23 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - 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_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, []byte{}, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") } - - // 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) + 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(gitCommit.Signature, []byte{}, string(authorizedKeys)) + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, []byte{}, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") } @@ -481,21 +258,10 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - 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) - } - // Use a different key that won't match wrongKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEyM97VxLgOCuB9Eg5cDtTc8ogkdM1xAyJhzODB9cK1 wrong@example.com" @@ -510,21 +276,21 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, wrongKey) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for wrong authorized keys, got nil") } - - // 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) + 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 := "" @@ -538,29 +304,23 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, emptyAuthKeys) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for empty authorized keys, got nil") } - - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + if fingerprint != "" { + t.Errorf("VerifySSHSignature() returned fingerprint for empty authorized keys: %s", fingerprint) } - - // 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) + 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(authorizedKeys)) + fingerprint, err := signatures.VerifySSHSignature(invalidSig, gitCommit.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") } @@ -570,21 +330,21 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + fingerprint, err = signatures.VerifySSHSignature(invalidSig, gitTag.Encoded, string(pubKey)) + if err == nil { + t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") } - - // 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) + 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-----" @@ -598,21 +358,20 @@ func TestVerifySSHSignature(t *testing.T) { 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) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) + fingerprint, err = signatures.VerifySSHSignature(pgpSig, gitTag.Encoded, "") + if err == nil { + t.Errorf("VerifySSHSignature() expected error for non-SSH signature, got nil") } - - // 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) + 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" @@ -626,824 +385,16 @@ func TestVerifySSHSignature(t *testing.T) { 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) } - }) -} - -func TestVerifySSHSignatureAllKeyTypes(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Test cases for each key type - keyTypes := []struct { - name string - sigFile string - authFile string - wantErr bool - }{ - {"ed25519", "commit_ed25519_signed.txt", "authorized_keys_ed25519", false}, - {"rsa", "commit_rsa_signed.txt", "authorized_keys_rsa", false}, - {"ecdsa_p256", "commit_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, - {"ecdsa_p384", "commit_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, - {"ecdsa_p521", "commit_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, - } - - 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.sigFile)) - 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) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Verify the signature using the git.Commit's Signature and Encoded fields - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} - -func TestVerifySSHSignatureCombinedKeys(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Read the combined authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_all")) - if err != nil { - t.Fatalf("Failed to read combined authorized keys: %v", err) - } - // Test each key type against the combined authorized keys - keyTypes := []struct { - name string - sigFile string - wantErr bool - }{ - {"ed25519", "commit_ed25519_signed.txt", false}, - {"rsa", "commit_rsa_signed.txt", false}, - {"ecdsa_p256", "commit_ecdsa_p256_signed.txt", false}, - {"ecdsa_p384", "commit_ecdsa_p384_signed.txt", false}, - {"ecdsa_p521", "commit_ecdsa_p521_signed.txt", false}, - } - - 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.sigFile)) - 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) - } - - // Verify the signature with combined authorized keys - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} - -func TestBuildCommitWithRefFromFixture(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - tests := []struct { - name string - fixture string - wantErr bool - wantSig bool - }{ - { - name: "ed25519 signed commit", - fixture: "commit_ed25519_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "rsa signed commit", - fixture: "commit_rsa_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p256 signed commit", - fixture: "commit_ecdsa_p256_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p384 signed commit", - fixture: "commit_ecdsa_p384_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p521 signed commit", - fixture: "commit_ecdsa_p521_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "unsigned commit", - fixture: "commit_unsigned.txt", - wantErr: false, - wantSig: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) - 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) != tt.wantErr { - t.Errorf("BuildCommitWithRef() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr { - // Verify the commit was built correctly - if gitCommit == nil { - t.Fatal("BuildCommitWithRef() returned nil commit") - } - - // Check if signature is present as expected - hasSig := gitCommit.Signature != "" - if hasSig != tt.wantSig { - t.Errorf("BuildCommitWithRef() has signature = %v, want %v", hasSig, tt.wantSig) - } - - // Verify the encoded data is present - if len(gitCommit.Encoded) == 0 { - t.Error("BuildCommitWithRef() returned commit with empty Encoded field") - } - - // Verify the reference is set correctly - if gitCommit.Reference != "refs/heads/main" { - t.Errorf("BuildCommitWithRef() reference = %q, want %q", gitCommit.Reference, "refs/heads/main") - } - - // Verify the hash is set - if len(gitCommit.Hash) == 0 { - t.Error("BuildCommitWithRef() returned commit with empty Hash") - } - - // Verify author and committer are set - if gitCommit.Author.Name == "" { - t.Error("BuildCommitWithRef() returned commit with empty Author.Name") - } - if gitCommit.Committer.Name == "" { - t.Error("BuildCommitWithRef() returned commit with empty Committer.Name") - } - - // If the commit has a signature, verify it can be extracted - if tt.wantSig { - // The signature is stored in gitCommit.Signature, not in gitCommit.Encoded - // gitCommit.Encoded contains the encoded commit without the signature - if gitCommit.Signature == "" { - t.Error("BuildCommitWithRef() returned commit with empty Signature field") - } - // Verify the signature contains the expected SSH signature markers - if !strings.Contains(gitCommit.Signature, "-----BEGIN SSH SIGNATURE-----") { - t.Error("BuildCommitWithRef() signature does not contain SSH signature start marker") - } - if !strings.Contains(gitCommit.Signature, "-----END SSH SIGNATURE-----") { - t.Error("BuildCommitWithRef() signature does not contain SSH signature end marker") - } - } - } - }) - } -} - -func TestBuildCommitWithRefAndVerifySSH(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - tests := []struct { - name string - fixture string - authFile string - wantErr bool - }{ - { - name: "ed25519 signed commit", - fixture: "commit_ed25519_signed.txt", - authFile: "authorized_keys_ed25519", - wantErr: false, - }, - { - name: "rsa signed commit", - fixture: "commit_rsa_signed.txt", - authFile: "authorized_keys_rsa", - wantErr: false, - }, - { - name: "ecdsa p256 signed commit", - fixture: "commit_ecdsa_p256_signed.txt", - authFile: "authorized_keys_ecdsa_p256", - wantErr: false, - }, - { - name: "ecdsa p384 signed commit", - fixture: "commit_ecdsa_p384_signed.txt", - authFile: "authorized_keys_ecdsa_p384", - wantErr: false, - }, - { - name: "ecdsa p521 signed commit", - fixture: "commit_ecdsa_p521_signed.txt", - authFile: "authorized_keys_ecdsa_p521", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Parse the commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, tt.fixture)) - 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("BuildCommitWithRef() error = %v", err) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Verify the SSH signature using the git.Commit's VerifySSH method - fingerprint, err := gitCommit.VerifySSH(string(authorizedKeys)) - if (err != nil) != tt.wantErr { - t.Errorf("git.Commit.VerifySSH() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr { - if fingerprint == "" { - t.Error("git.Commit.VerifySSH() returned empty fingerprint") - } - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} - -func TestBuildCommitWithRefWithDifferentRefs(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Parse a signed commit from the fixture file - commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse commit from fixture: %v", err) - } - - tests := []struct { - name string - ref plumbing.ReferenceName - wantRef string - }{ - { - name: "branch reference", - ref: plumbing.ReferenceName("refs/heads/main"), - wantRef: "refs/heads/main", - }, - { - name: "tag reference", - ref: plumbing.ReferenceName("refs/tags/v1.0.0"), - wantRef: "refs/tags/v1.0.0", - }, - { - name: "remote branch reference", - ref: plumbing.ReferenceName("refs/remotes/origin/main"), - wantRef: "refs/remotes/origin/main", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Build a git.Commit using BuildCommitWithRef with different references - gitCommit, err := gogit.BuildCommitWithRef(commitObj, nil, tt.ref) - if err != nil { - t.Fatalf("BuildCommitWithRef() error = %v", err) - } - - // Verify the reference is set correctly - if gitCommit.Reference != tt.wantRef { - t.Errorf("BuildCommitWithRef() reference = %q, want %q", gitCommit.Reference, tt.wantRef) - } - - // Verify other fields are still set correctly - if len(gitCommit.Hash) == 0 { - t.Error("BuildCommitWithRef() returned commit with empty Hash") - } - if gitCommit.Signature == "" { - t.Error("BuildCommitWithRef() returned commit with empty Signature") - } - }) - } -} - -func TestBuildTagFromFixture(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - tests := []struct { - name string - fixture string - wantErr bool - wantSig bool - }{ - { - name: "ed25519 signed tag", - fixture: "tag_ed25519_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "rsa signed tag", - fixture: "tag_rsa_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p256 signed tag", - fixture: "tag_ecdsa_p256_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p384 signed tag", - fixture: "tag_ecdsa_p384_signed.txt", - wantErr: false, - wantSig: true, - }, - { - name: "ecdsa p521 signed tag", - fixture: "tag_ecdsa_p521_signed.txt", - wantErr: false, - wantSig: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %v", err) - } - - // Build a git.Tag using BuildTag - gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) - if (err != nil) != tt.wantErr { - t.Errorf("BuildTag() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr { - // Verify the tag was built correctly - if gitTag == nil { - t.Fatal("BuildTag() returned nil tag") - } - - // Check if signature is present as expected - hasSig := gitTag.Signature != "" - if hasSig != tt.wantSig { - t.Errorf("BuildTag() has signature = %v, want %v", hasSig, tt.wantSig) - } - - // Verify the encoded data is present - if len(gitTag.Encoded) == 0 { - t.Error("BuildTag() returned tag with empty Encoded field") - } - - // Verify the name is set correctly - if gitTag.Name == "" { - t.Error("BuildTag() returned tag with empty Name") - } - - // Verify the hash is set - if len(gitTag.Hash) == 0 { - t.Error("BuildTag() returned tag with empty Hash") - } - - // Verify author is set - if gitTag.Author.Name == "" { - t.Error("BuildTag() returned tag with empty Author.Name") - } - - // If the tag has a signature, verify it can be extracted - if tt.wantSig { - if gitTag.Signature == "" { - t.Error("BuildTag() returned tag with empty Signature field") - } - // Verify the signature contains the expected SSH signature markers - if !strings.Contains(gitTag.Signature, "-----BEGIN SSH SIGNATURE-----") { - t.Error("BuildTag() signature does not contain SSH signature start marker") - } - if !strings.Contains(gitTag.Signature, "-----END SSH SIGNATURE-----") { - t.Error("BuildTag() signature does not contain SSH signature end marker") - } - } - } - }) - } -} - -func TestVerifySSHSignatureForTags(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Test cases for each key type using fixtures - keyTypes := []struct { - name string - sigFile string - authFile string - wantErr bool - }{ - {"ed25519 valid signature", "tag_ed25519_signed.txt", "authorized_keys_ed25519", false}, - {"rsa valid signature", "tag_rsa_signed.txt", "authorized_keys_rsa", false}, - {"ecdsa_p256 valid signature", "tag_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, - {"ecdsa_p384 valid signature", "tag_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, - {"ecdsa_p521 valid signature", "tag_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, - } - - for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Verify the signature using the git.Tag's Signature and Encoded fields - fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } - - // Test error cases - t.Run("empty signature", func(t *testing.T) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - fingerprint, err := signatures.VerifySSHSignature("", gitTag.Encoded, string(authorizedKeys)) - 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) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, []byte{}, string(authorizedKeys)) - 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) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - // Use a different key that won't match - wrongKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEyM97VxLgOCuB9Eg5cDtTc8ogkdM1xAyJhzODB9cK1 wrong@example.com" - - fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, wrongKey) + fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, invalidAuthKeys) 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("invalid signature", func(t *testing.T) { - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_ed25519")) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_ed25519_signed.txt")) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - invalidSig := "-----BEGIN SSH SIGNATURE-----\n invalid\n -----END SSH SIGNATURE-----" - - fingerprint, err := signatures.VerifySSHSignature(invalidSig, gitTag.Encoded, string(authorizedKeys)) - if err == nil { - t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") + t.Errorf("VerifySSHSignature() expected error for invalid authorized keys, got nil") } if fingerprint != "" { - t.Errorf("VerifySSHSignature() returned fingerprint for invalid signature: %s", fingerprint) + t.Errorf("VerifySSHSignature() returned fingerprint for invalid authorized keys: %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) + 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) } }) } - -func TestVerifySSHSignatureForTagsAllKeyTypes(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Test cases for each key type - keyTypes := []struct { - name string - sigFile string - authFile string - wantErr bool - }{ - {"ed25519", "tag_ed25519_signed.txt", "authorized_keys_ed25519", false}, - {"rsa", "tag_rsa_signed.txt", "authorized_keys_rsa", false}, - {"ecdsa_p256", "tag_ecdsa_p256_signed.txt", "authorized_keys_ecdsa_p256", false}, - {"ecdsa_p384", "tag_ecdsa_p384_signed.txt", "authorized_keys_ecdsa_p384", false}, - {"ecdsa_p521", "tag_ecdsa_p521_signed.txt", "authorized_keys_ecdsa_p521", false}, - } - - for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, kt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Verify the signature using the git.Tag's Signature and Encoded fields - fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} - -func TestVerifySSHSignatureForTagsCombinedKeys(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - // Read the combined authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, "authorized_keys_all")) - if err != nil { - t.Fatalf("Failed to read combined authorized keys: %v", err) - } - - // Test each key type against the combined authorized keys - keyTypes := []struct { - name string - sigFile string - wantErr bool - }{ - {"ed25519", "tag_ed25519_signed.txt", false}, - {"rsa", "tag_rsa_signed.txt", false}, - {"ecdsa_p256", "tag_ecdsa_p256_signed.txt", false}, - {"ecdsa_p384", "tag_ecdsa_p384_signed.txt", false}, - {"ecdsa_p521", "tag_ecdsa_p521_signed.txt", false}, - } - - for _, kt := range keyTypes { - t.Run(kt.name, func(t *testing.T) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, kt.sigFile)) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %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) - } - - // Verify the signature with combined authorized keys - fingerprint, err := signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKeys)) - if (err != nil) != kt.wantErr { - t.Errorf("VerifySSHSignature() error = %v, wantErr %v", err, kt.wantErr) - return - } - if !kt.wantErr && fingerprint == "" { - t.Errorf("VerifySSHSignature() returned empty fingerprint") - } - if !kt.wantErr { - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} - -func TestBuildTagAndVerifySSH(t *testing.T) { - testDataDir := filepath.Join("testdata", "ssh_signatures") - - tests := []struct { - name string - fixture string - authFile string - wantErr bool - }{ - { - name: "ed25519 signed tag", - fixture: "tag_ed25519_signed.txt", - authFile: "authorized_keys_ed25519", - wantErr: false, - }, - { - name: "rsa signed tag", - fixture: "tag_rsa_signed.txt", - authFile: "authorized_keys_rsa", - wantErr: false, - }, - { - name: "ecdsa p256 signed tag", - fixture: "tag_ecdsa_p256_signed.txt", - authFile: "authorized_keys_ecdsa_p256", - wantErr: false, - }, - { - name: "ecdsa p384 signed tag", - fixture: "tag_ecdsa_p384_signed.txt", - authFile: "authorized_keys_ecdsa_p384", - wantErr: false, - }, - { - name: "ecdsa p521 signed tag", - fixture: "tag_ecdsa_p521_signed.txt", - authFile: "authorized_keys_ecdsa_p521", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Parse the tag from the fixture file - tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, tt.fixture)) - if err != nil { - t.Fatalf("Failed to parse tag from fixture: %v", err) - } - - // Build a git.Tag using BuildTag - gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) - if err != nil { - t.Fatalf("BuildTag() error = %v", err) - } - - // Read the authorized keys - authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.authFile)) - if err != nil { - t.Fatalf("Failed to read authorized keys: %v", err) - } - - // Verify the SSH signature using the git.Tag's VerifySSH method - fingerprint, err := gitTag.VerifySSH(string(authorizedKeys)) - if (err != nil) != tt.wantErr { - t.Errorf("git.Tag.VerifySSH() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr { - if fingerprint == "" { - t.Error("git.Tag.VerifySSH() returned empty fingerprint") - } - t.Logf("Verified with fingerprint: %s", fingerprint) - } - }) - } -} diff --git a/git/signatures/testdata/ssh_signatures/README.md b/git/signatures/testdata/ssh_signatures/README.md index ab402ffec..1e57a520d 100644 --- a/git/signatures/testdata/ssh_signatures/README.md +++ b/git/signatures/testdata/ssh_signatures/README.md @@ -163,14 +163,7 @@ The script generates the following files: - `key_ecdsa_p384.pub` - ECDSA P-384 public key - `key_ecdsa_p521.pub` - ECDSA P-521 public key - `key_ed25519.pub` - ED25519 public key - -### Authorized Keys Files -- `authorized_keys_rsa` - RSA public key -- `authorized_keys_ecdsa_p256` - ECDSA P-256 public key -- `authorized_keys_ecdsa_p384` - ECDSA P-384 public key -- `authorized_keys_ecdsa_p521` - ECDSA P-521 public key -- `authorized_keys_ed25519` - ED25519 public key -- `authorized_keys_all` - All public keys combined +- `keys_all.pub` - All public keys ### Verified Signers Files - `verified_signers_rsa` - RSA public key with git namespace diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 deleted file mode 100644 index 7364a9a27..000000000 --- a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p256 +++ /dev/null @@ -1 +0,0 @@ -ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 deleted file mode 100644 index aabefb80b..000000000 --- a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p384 +++ /dev/null @@ -1 +0,0 @@ -ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 b/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 deleted file mode 100644 index 82d92898f..000000000 --- a/git/signatures/testdata/ssh_signatures/authorized_keys_ecdsa_p521 +++ /dev/null @@ -1 +0,0 @@ -ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 b/git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 deleted file mode 100644 index 8f745c471..000000000 --- a/git/signatures/testdata/ssh_signatures/authorized_keys_ed25519 +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB0X4BwNz61VyvryI/aq5vUc9fZK1najY6WCSdxzpLLW test-ed25519@example.com diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_rsa b/git/signatures/testdata/ssh_signatures/authorized_keys_rsa deleted file mode 100644 index b02a4d38f..000000000 --- a/git/signatures/testdata/ssh_signatures/authorized_keys_rsa +++ /dev/null @@ -1 +0,0 @@ -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/generate_ssh_fixtures.sh b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh index ab7cba060..b14574bd8 100755 --- a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh +++ b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -46,18 +46,6 @@ generate_ssh_key() { echo " ✓ key_${key_name}.pub_fingerprint created" } -# Function to create authorized_keys files -create_authorized_keys() { - local key_name=$1 - local output_file="$SCRIPT_DIR/authorized_keys_${key_name}" - - echo "Creating authorized_keys for $key_name..." - - # Copy public key - cp "$TEMP_DIR/${key_name}.pub" "$output_file" - echo " ✓ $output_file created" -} - # Function to create verified signers files with git namespace create_verified_signers() { local key_name=$1 @@ -71,8 +59,8 @@ create_verified_signers() { } # Function to create combined authorized_keys file -create_combined_authorized_keys() { - local output_file="$SCRIPT_DIR/authorized_keys_all" +create_combined_pub_keys() { + local output_file="$SCRIPT_DIR/keys_all.pub" echo "Creating combined authorized_keys..." @@ -218,18 +206,11 @@ main() { generate_ssh_key "ed25519" "" "ed25519" echo "" - echo "Step 2: Create authorized_keys files..." + echo "Step 2: Create pub_keys files..." echo "-----------------------------------------------" - # Individual authorized_keys files - create_authorized_keys "rsa" - create_authorized_keys "ecdsa_p256" - create_authorized_keys "ecdsa_p384" - create_authorized_keys "ecdsa_p521" - create_authorized_keys "ed25519" - - # Combined authorized_keys file - create_combined_authorized_keys + # Combined pub_keys file + create_combined_pub_keys echo "" echo "Step 3: Create verified signers files..." diff --git a/git/signatures/testdata/ssh_signatures/authorized_keys_all b/git/signatures/testdata/ssh_signatures/keys_all.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/authorized_keys_all rename to git/signatures/testdata/ssh_signatures/keys_all.pub From 52df5ec3d14011a0bd83a2f3683cacf3b2395848 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 10 Mar 2026 09:53:34 +0100 Subject: [PATCH 08/16] fixes text fixtures public key names Signed-off-by: Ricardo Bartels --- git/git_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/git/git_test.go b/git/git_test.go index d1869b520..a9ce157ee 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -762,18 +762,18 @@ func TestCommit_VerifySSH(t *testing.T) { { name: "valid SSH signature", sigFile: "commit_rsa_signed.txt", - authorizedKeys: "authorized_keys_rsa", + authorizedKeys: "key_rsa.pub", }, { name: "missing signature", sigFile: "commit_unsigned.txt", - authorizedKeys: "authorized_keys_rsa", + 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: "authorized_keys_ed25519", + authorizedKeys: "key_ed25519.pub", wantErr: "unable to verify Git commit SSH signature: unable to verify payload with any of the given authorized keys", }, { @@ -840,18 +840,18 @@ func TestTag_VerifySSH(t *testing.T) { { name: "valid SSH signature", sigFile: "tag_rsa_signed.txt", - authorizedKeys: "authorized_keys_rsa", + authorizedKeys: "key_rsa.pub", }, { name: "missing signature", sigFile: "commit_unsigned.txt", - authorizedKeys: "authorized_keys_rsa", + 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: "authorized_keys_ed25519", + authorizedKeys: "key_ed25519.pub", wantErr: "unable to verify Git tag SSH signature: unable to verify payload with any of the given authorized keys", }, { From 86f85a6bc08e7feb38abbab0d215e08508f52158 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 10 Mar 2026 14:32:15 +0100 Subject: [PATCH 09/16] adds 'SignatureTypeEmpty' check and improves description of 'GetSignatureType' Signed-off-by: Ricardo Bartels --- git/git_test.go | 2 +- git/signatures/signature.go | 14 +++++++++++++- git/signatures/signature_test.go | 2 +- git/signatures/ssh_signature.go | 7 ++++--- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/git/git_test.go b/git/git_test.go index a9ce157ee..ea2347ca8 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -550,7 +550,7 @@ func TestSignatureType(t *testing.T) { name: "unsigned", commit: &Commit{}, tag: &Tag{}, - want: "unknown", + want: "empty", }, { name: "unknown signature type", diff --git a/git/signatures/signature.go b/git/signatures/signature.go index f85d4d03c..5b7ba3ed5 100644 --- a/git/signatures/signature.go +++ b/git/signatures/signature.go @@ -32,6 +32,8 @@ const ( 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. @@ -72,9 +74,16 @@ 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, -// and "unknown" for unrecognized or empty 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) @@ -85,5 +94,8 @@ func GetSignatureType(signature string) string { 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 index 4350421a4..b079b8784 100644 --- a/git/signatures/signature_test.go +++ b/git/signatures/signature_test.go @@ -217,7 +217,7 @@ func TestGetSignatureType(t *testing.T) { { name: "empty signature", signature: "", - want: string(SignatureTypeUnknown), + want: string(SignatureTypeEmpty), }, { name: "unknown signature", diff --git a/git/signatures/ssh_signature.go b/git/signatures/ssh_signature.go index c3a146cbf..f9c80719a 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signatures/ssh_signature.go @@ -27,6 +27,8 @@ import ( 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-----"} @@ -56,7 +58,7 @@ func ParseAuthorizedKeys(authorizedKeys string) ([]gossh.PublicKey, error) { return publicKeys, nil } -// verifySSHSignature verifies the SSH signature against the payload using +// 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) { @@ -88,8 +90,7 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri // Try to verify with each public key for _, pubKey := range publicKeys { // Verify the signature using sshsig library - // The namespace for Git is "git" - err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, sig.HashAlgorithm, "git") + err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, sig.HashAlgorithm, SSHSignatureNamespace) if err == nil { // Signature verified successfully return getPublicKeyFingerprint(pubKey), nil From 7906728af9fbcec35fe23afe20a4105419d24ff1 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Thu, 19 Mar 2026 12:12:51 +0100 Subject: [PATCH 10/16] Update git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh Co-authored-by: Paulo Gomes Signed-off-by: Ricardo Signed-off-by: Ricardo Bartels --- git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh index 66de6958e..f6d7e740f 100755 --- a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -244,4 +244,4 @@ main() { find "$SCRIPT_DIR" -maxdepth 1 \( -name "*.txt" -o -name "key_*.pub" \) -exec ls -lh {} \; 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' } -main \ No newline at end of file +main From d505af10377293a2752256a12e3448fd56d2f530 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Thu, 19 Mar 2026 12:30:36 +0100 Subject: [PATCH 11/16] removes insecure DSA keys from signature test Signed-off-by: Ricardo Bartels --- git/signatures/gpg_signature_test.go | 1 - .../testdata/gpg_signatures/README.md | 23 +---- .../gpg_signatures/commit_dsa_2048_signed.txt | 12 --- .../gpg_signatures/generate_gpg_fixtures.sh | 91 +++++++++---------- .../testdata/gpg_signatures/key_dsa_2048.pub | 25 ----- .../gpg_signatures/tag_dsa_2048_signed.txt | 13 --- 6 files changed, 45 insertions(+), 120 deletions(-) delete mode 100644 git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt delete mode 100644 git/signatures/testdata/gpg_signatures/key_dsa_2048.pub delete mode 100644 git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt diff --git a/git/signatures/gpg_signature_test.go b/git/signatures/gpg_signature_test.go index 22ea81575..40c7c6727 100644 --- a/git/signatures/gpg_signature_test.go +++ b/git/signatures/gpg_signature_test.go @@ -208,7 +208,6 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { {"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}, - {"dsa_2048 valid signature", "commit_dsa_2048_signed.txt", "tag_dsa_2048_signed.txt", "key_dsa_2048.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}, diff --git a/git/signatures/testdata/gpg_signatures/README.md b/git/signatures/testdata/gpg_signatures/README.md index 29fd5fc70..e7e8187ed 100644 --- a/git/signatures/testdata/gpg_signatures/README.md +++ b/git/signatures/testdata/gpg_signatures/README.md @@ -20,7 +20,6 @@ The [`generate_gpg_fixtures.sh`](generate_gpg_fixtures.sh) script automates the 1. **GPG Key Pairs** in supported variants: - RSA (2048 and 4096 bits) - - DSA (2048 bits) - ECC/ECDSA (NIST P-256, P-384, P-521) - Brainpool curves (P-256, P-384, P-512) - EdDSA (Ed25519, Ed448) @@ -81,18 +80,6 @@ Expire-Date: 0 EOF gpg --batch --generate-key batch_rsa_4096.txt -# DSA 2048-bit key -cat > batch_dsa_2048.txt < batch_ecdsa_p256.txt < key_rsa_2048.pub gpg --armor --export test-rsa-4096@example.com > key_rsa_4096.pub -gpg --armor --export test-dsa-2048@example.com > key_dsa_2048.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 @@ -284,7 +270,6 @@ 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_dsa_2048.pub` - DSA 2048-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 @@ -297,7 +282,6 @@ The script generates the following files: ### Signed Commits - `commit_rsa_2048_signed.txt` - RSA 2048-bit signed commit - `commit_rsa_4096_signed.txt` - RSA 4096-bit signed commit -- `commit_dsa_2048_signed.txt` - DSA 2048-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 @@ -310,7 +294,6 @@ The script generates the following files: ### Signed Tags - `tag_rsa_2048_signed.txt` - RSA 2048-bit signed tag - `tag_rsa_4096_signed.txt` - RSA 4096-bit signed tag -- `tag_dsa_2048_signed.txt` - DSA 2048-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 @@ -330,10 +313,6 @@ The script generates the following files: - **RSA 4096**: Stronger RSA key with 4096-bit modulus - Widely supported, but slower than ECC keys -### DSA (Digital Signature Algorithm) -- **DSA 2048**: Legacy algorithm, 2048-bit key -- Less secure than modern alternatives, included for compatibility testing - ### ECDSA (Elliptic Curve Digital Signature Algorithm) - **P-256**: NIST P-256 curve (secp256r1) - **P-384**: NIST P-384 curve (secp384r1) @@ -385,7 +364,7 @@ If key generation fails, ensure that: ### Script structure The script uses separate functions for different key types: -- `generate_rsa_dsa_key()` - For RSA and DSA keys with key length validation +- `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 diff --git a/git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt deleted file mode 100644 index 1132feef8..000000000 --- a/git/signatures/testdata/gpg_signatures/commit_dsa_2048_signed.txt +++ /dev/null @@ -1,12 +0,0 @@ -tree 94e5cb8fdb0551092fe394328dd9de2dbd8394f3 -author Test User 1772188965 +0100 -committer Test User 1772188965 +0100 -gpgsig -----BEGIN PGP SIGNATURE----- - - iHUEABEIAB0WIQQ3p1oEVydAtN6w28QIntqNADkiBwUCaaF1JQAKCRAIntqNADki - B3brAP9bhBteRaxkRDN2rXbAxFdBLACqgqTH10Zv4if3gxZxKQD/ZoAiBYUyWq3C - HKyihQ+PCD2wMv6tyzkC5RI5mumh5Fw= - =C3Wn - -----END PGP SIGNATURE----- - -Test commit signed with dsa_2048 diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh index f6d7e740f..896881f99 100755 --- a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -31,52 +31,52 @@ 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" } @@ -85,41 +85,41 @@ EOF 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" @@ -128,12 +128,12 @@ create_signed_object() { 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" @@ -144,64 +144,61 @@ create_signed_object() { # 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/DSA keys..." + echo "Step 1: Generate RSA keys..." echo "-----------------------------------" - + # RSA keys (different key lengths) generate_key "RSA" "2048" "rsa_2048" generate_key "RSA" "4096" "rsa_4096" - - # DSA key (legacy, but still supported) - generate_key "DSA" "2048" "dsa_2048" - + 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 @@ -210,32 +207,32 @@ main() { 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." diff --git a/git/signatures/testdata/gpg_signatures/key_dsa_2048.pub b/git/signatures/testdata/gpg_signatures/key_dsa_2048.pub deleted file mode 100644 index 908d2c05b..000000000 --- a/git/signatures/testdata/gpg_signatures/key_dsa_2048.pub +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQMuBGmhdSIRCACoIRoP5Lvi8g2dE7Nn9/AI6O3SEnCotRNnT10nmELXwqYonn/9 -3LIhiMMqdMPgtIQZLuvoUlZzMG/mufeHZlezhfUqbFOHY09Czcuvm0zkTBwIDq+a -CA729ICuPAgYAq53iPab5WqGO9H/LAX/6yYGLhQ0GHFdpnyxWnO/OtSBlLxqL+97 -oz6lhGSC9JSNxazSr/3qwvaytzOMI8ptDIVlpydv8WTghXBkjrIkXR8vR++2aEhK -voVCS9HSC19kg9B2fybGu4M5foP9ZIL62O+6rvopGSA1tmWctR2oIoi3Bi7x5vzA -f3NyOYZT8F+nnxwotdxzeoYxh/rVld4i321jAQCYLj2IVmgisitLwHKxXVT/aX3O -CAaEI/ESOcBm/arO9Qf9HLfKlc2wVtXL1g0KjaMZVvh9nvqzchBxmTtlLGmU5gIU -r7ZqDQ2pqavmZJ1YRBlGPRLnL8n1NXZMj8OHPRHyUJQ4oph8FFRoBOwspEw+i67j -jl42mc20IhOU28QPmtsmlEHJwdhZsYmCImtWFilHS8ThPewY+Qn2S0L+4nnBkTy4 -1y3ZGRSzQQhH1jOJtBdNBadQcrYppMWgxHNIe0V3s+7FCc89jJRECj608ZrlLYT1 -7KGPZDPqDtR668Br7sP6PjPJD6mnycsrQSNu1rFU3fsuClVlLeT9mWpwQspfQEYa -vmuRh48uuGUQFBanDM5EPTG7c4aB6Gz1k8J/HtXRpwf/Rc8eHZIVGrpc/7CuChGF -fqloBvAz77A3Blr7KaYIViEXj9dcw75Aurtk9lhtUpYe4A66ZdyoZE03xsKmATKX -Ois1YgBaQGEZoOM632pbv3bFrSCjrZnLMnwLIGDhqKnJy7H0mALKL9iILcN7lF0P -WU1YSNgZFU6X70aJvwEmOjeBM5YhGS+e4OPZW/z+b3f/1jE3dGJwz6LsT+M8+xWw -uqN1ZJ+Ijvg8k6HIFx4eXY0zPLElIaWkZExNki/T35jnazb8ZzCeu4/RiJz6YMwd -OAPIZ3I0dZJe5BO32eRbMQFb+OzEcVTNV5Jc/m09b9jEfOvmrHRHoDgGrF6/0S3Y -R7QlVGVzdCBVc2VyIDx0ZXN0LWRzYV8yMDQ4QGV4YW1wbGUuY29tPoiTBBMRCAA7 -FiEEN6daBFcnQLTesNvECJ7ajQA5IgcFAmmhdSICGyMFCwkIBwICIgIGFQoJCAsC -BBYCAwECHgcCF4AACgkQCJ7ajQA5IgcsBAD9F9koK8sIUApNcCFUqCGR9olYimkN -juoedSfOpMV/+j0A+wXU0jUfweGWUv7MPGmh1Sn0oMOBZTIL0LU+x/F3glLl -=lJcF ------END PGP PUBLIC KEY BLOCK----- diff --git a/git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt b/git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt deleted file mode 100644 index 46578ae13..000000000 --- a/git/signatures/testdata/gpg_signatures/tag_dsa_2048_signed.txt +++ /dev/null @@ -1,13 +0,0 @@ -object 8ee3b79d5ad1fa463deea7fc9bfcbca311168d01 -type commit -tag test-tag-dsa_2048 -tagger Test User 1772188968 +0100 - -Test tag signed with dsa_2048 ------BEGIN PGP SIGNATURE----- - -iHUEABEIAB0WIQQ3p1oEVydAtN6w28QIntqNADkiBwUCaaF1KAAKCRAIntqNADki -Byq0AP9rHhQiJKh3rPNYW06C6N9yGnccU8nE5S5EfeH8Gps6SQD/f19dyM5euse9 -vylc3KD1sfdFekiLuW2WpDIw4JbAbMg= -=k9QD ------END PGP SIGNATURE----- From 70dca9f5bec098bdc46b5cf018a6caa187367f83 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Mon, 27 Apr 2026 22:32:50 +0200 Subject: [PATCH 12/16] updates git go.mod dependencies Signed-off-by: Ricardo Bartels --- git/go.mod | 7 ------- git/go.sum | 2 -- 2 files changed, 9 deletions(-) diff --git a/git/go.mod b/git/go.mod index fd5a8a5ff..70d697eb6 100644 --- a/git/go.mod +++ b/git/go.mod @@ -20,13 +20,6 @@ require ( github.com/fluxcd/pkg/version v0.15.0 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.0 - github.com/onsi/gomega v1.40.0 - golang.org/x/crypto v0.50.0 - github.com/fluxcd/pkg/gittestserver v0.28.0 - github.com/fluxcd/pkg/ssh v0.25.0 - github.com/fluxcd/pkg/version v0.15.0 - github.com/go-git/go-billy/v5 v5.9.0 - github.com/go-git/go-git/v5 v5.19.0 github.com/hiddeco/sshsig v0.2.0 github.com/onsi/gomega v1.40.0 golang.org/x/crypto v0.50.0 diff --git a/git/go.sum b/git/go.sum index e51d7f654..b379885ff 100644 --- a/git/go.sum +++ b/git/go.sum @@ -42,8 +42,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/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= From 074c9de325ed2129e65382d29cd66e5012aa8f52 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Fri, 8 May 2026 23:34:50 +0200 Subject: [PATCH 13/16] fixes tests for unsigned tags Signed-off-by: Ricardo Bartels --- git/git_test.go | 4 ++-- .../testdata/gpg_signatures/generate_gpg_fixtures.sh | 10 +++++++--- .../testdata/gpg_signatures/tag_unsigned.txt | 6 ++++++ .../testdata/ssh_signatures/generate_ssh_fixtures.sh | 11 ++++++++--- .../testdata/ssh_signatures/tag_unsigned.txt | 6 ++++++ 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 git/signatures/testdata/gpg_signatures/tag_unsigned.txt create mode 100644 git/signatures/testdata/ssh_signatures/tag_unsigned.txt diff --git a/git/git_test.go b/git/git_test.go index ea2347ca8..3e286654d 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -677,7 +677,7 @@ func TestTag_VerifyGPG(t *testing.T) { }, { name: "missing signature", - sigFile: "commit_unsigned.txt", + sigFile: "tag_unsigned.txt", keyFile: "key_rsa_2048.pub", wantErr: "unable to verify Git tag: unable to verify payload as the provided signature is empty", }, @@ -844,7 +844,7 @@ func TestTag_VerifySSH(t *testing.T) { }, { name: "missing signature", - sigFile: "commit_unsigned.txt", + sigFile: "tag_unsigned.txt", authorizedKeys: "key_rsa.pub", wantErr: "unable to verify Git tag SSH signature: unable to verify payload as the provided signature is empty", }, diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh index 896881f99..30514ebeb 100755 --- a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh +++ b/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh @@ -142,7 +142,7 @@ create_signed_object() { } # Function to create unsigned commit -create_unsigned_commit() { +create_unsigned_commit_and_tag() { echo "Creating unsigned commit..." # Create temporary Git repository @@ -162,6 +162,10 @@ create_unsigned_commit() { # Export commit object git cat-file commit HEAD > "$SCRIPT_DIR/commit_unsigned.txt" + # Create and export tag object + git tag -a test-tag -m "Test tag" + git cat-file tag test-tag > "$SCRIPT_DIR/tag_unsigned.txt" + cd "$SCRIPT_DIR" echo " ✓ commit_unsigned.txt created" } @@ -226,7 +230,7 @@ main() { echo "Step 5: Create unsigned commit..." echo "------------------------------------------" - create_unsigned_commit + create_unsigned_commit_and_tag echo "" echo "=== Cleanup ===" @@ -238,7 +242,7 @@ main() { 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 ")"}' + find "$SCRIPT_DIR" -maxdepth 1 \( -name "*.txt" -o -name "key_*.pub" \) -exec ls -lh {} \; 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' | sort } main diff --git a/git/signatures/testdata/gpg_signatures/tag_unsigned.txt b/git/signatures/testdata/gpg_signatures/tag_unsigned.txt new file mode 100644 index 000000000..fc22314e4 --- /dev/null +++ b/git/signatures/testdata/gpg_signatures/tag_unsigned.txt @@ -0,0 +1,6 @@ +object 4aab80f202c9442c6bb439d6985d70592d30811a +type commit +tag test-tag +tagger Test User 1772188971 +0200 + +Test tag diff --git a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh index b14574bd8..8386b012f 100755 --- a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh +++ b/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh @@ -163,8 +163,9 @@ create_signed_object() { } # Function to create unsigned commit -create_unsigned_commit() { +create_unsigned_commit_and_tag() { local commit_file="$SCRIPT_DIR/commit_unsigned.txt" + local tag_file="$SCRIPT_DIR/tag_unsigned.txt" echo "Creating unsigned commit..." @@ -185,6 +186,10 @@ create_unsigned_commit() { # Export commit object git cat-file commit HEAD > "$commit_file" + # Create and export tag object + git tag -a test-tag -m "Test tag" + git cat-file tag test-tag > "$tag_file" + cd "$SCRIPT_DIR" echo " ✓ $commit_file created" } @@ -252,7 +257,7 @@ main() { echo "Step 6: Create unsigned commit..." echo "------------------------------------------" - create_unsigned_commit + create_unsigned_commit_and_tag echo "" echo "=== Cleanup ===" @@ -264,7 +269,7 @@ main() { 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 ")"}' + find "$SCRIPT_DIR" -maxdepth 1 \( -name "*.txt" -o -name "key_*.pub" -o -name "authorized_keys*" -o -name "verified_signers*" \) -exec ls -lh {} \; 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' | sort } # Run script diff --git a/git/signatures/testdata/ssh_signatures/tag_unsigned.txt b/git/signatures/testdata/ssh_signatures/tag_unsigned.txt new file mode 100644 index 000000000..4e5a55762 --- /dev/null +++ b/git/signatures/testdata/ssh_signatures/tag_unsigned.txt @@ -0,0 +1,6 @@ +object 44e81f94b13509da0a0c9ad89b590c786b383a28 +type commit +tag test-tag +tagger Test User 1772153090 +0200 + +Test tag From 9ab452068d470e1346cb00237efbfcdcb9803533 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Mon, 18 May 2026 23:22:52 +0200 Subject: [PATCH 14/16] renames package to signature and fixes requested changes Signed-off-by: Ricardo Bartels --- git/git.go | 26 +- git/git_test.go | 8 +- .../gpg_signature.go | 6 +- .../gpg_signature_test.go | 16 +- git/{signatures => signature}/signature.go | 31 +- .../signature_test.go | 4 +- .../ssh_signature.go | 15 +- .../ssh_signature_test.go | 317 ++++++++++++++++-- .../testdata/gpg_signatures/README.md | 0 .../commit_brainpool_p256_signed.txt | 0 .../commit_brainpool_p384_signed.txt | 0 .../commit_brainpool_p512_signed.txt | 0 .../commit_ecdsa_p256_signed.txt | 0 .../commit_ecdsa_p384_signed.txt | 0 .../commit_ecdsa_p521_signed.txt | 0 .../gpg_signatures/commit_ed25519_signed.txt | 0 .../gpg_signatures/commit_ed448_signed.txt | 0 .../gpg_signatures/commit_rsa_2048_signed.txt | 0 .../gpg_signatures/commit_rsa_4096_signed.txt | 0 .../gpg_signatures/commit_unsigned.txt | 0 .../gpg_signatures/generate_gpg_fixtures.sh | 0 .../gpg_signatures/key_brainpool_p256.pub | 0 .../gpg_signatures/key_brainpool_p384.pub | 0 .../gpg_signatures/key_brainpool_p512.pub | 0 .../gpg_signatures/key_ecdsa_p256.pub | 0 .../gpg_signatures/key_ecdsa_p384.pub | 0 .../gpg_signatures/key_ecdsa_p521.pub | 0 .../testdata/gpg_signatures/key_ed25519.pub | 0 .../testdata/gpg_signatures/key_ed448.pub | 0 .../testdata/gpg_signatures/key_rsa_2048.pub | 0 .../testdata/gpg_signatures/key_rsa_4096.pub | 0 .../tag_brainpool_p256_signed.txt | 0 .../tag_brainpool_p384_signed.txt | 0 .../tag_brainpool_p512_signed.txt | 0 .../gpg_signatures/tag_ecdsa_p256_signed.txt | 0 .../gpg_signatures/tag_ecdsa_p384_signed.txt | 0 .../gpg_signatures/tag_ecdsa_p521_signed.txt | 0 .../gpg_signatures/tag_ed25519_signed.txt | 0 .../gpg_signatures/tag_ed448_signed.txt | 0 .../gpg_signatures/tag_rsa_2048_signed.txt | 0 .../gpg_signatures/tag_rsa_4096_signed.txt | 0 .../testdata/gpg_signatures/tag_unsigned.txt | 0 .../testdata/ssh_signatures/README.md | 0 .../commit_ecdsa_p256_signed.txt | 0 .../commit_ecdsa_p384_signed.txt | 0 .../commit_ecdsa_p521_signed.txt | 0 .../ssh_signatures/commit_ed25519_signed.txt | 0 .../ssh_signatures/commit_rsa_signed.txt | 0 .../ssh_signatures/commit_unsigned.txt | 0 .../ssh_signatures/generate_ssh_fixtures.sh | 0 .../ssh_signatures/key_ecdsa_p256.pub | 0 .../key_ecdsa_p256.pub_fingerprint | 0 .../ssh_signatures/key_ecdsa_p384.pub | 0 .../key_ecdsa_p384.pub_fingerprint | 0 .../ssh_signatures/key_ecdsa_p521.pub | 0 .../key_ecdsa_p521.pub_fingerprint | 0 .../testdata/ssh_signatures/key_ed25519.pub | 0 .../key_ed25519.pub_fingerprint | 0 .../testdata/ssh_signatures/key_rsa.pub | 0 .../ssh_signatures/key_rsa.pub_fingerprint | 0 .../testdata/ssh_signatures/keys_all.pub | 0 .../ssh_signatures/tag_ecdsa_p256_signed.txt | 0 .../ssh_signatures/tag_ecdsa_p384_signed.txt | 0 .../ssh_signatures/tag_ecdsa_p521_signed.txt | 0 .../ssh_signatures/tag_ed25519_signed.txt | 0 .../ssh_signatures/tag_rsa_signed.txt | 0 .../testdata/ssh_signatures/tag_unsigned.txt | 0 .../ssh_signatures/verified_signers_all | 0 .../verified_signers_ecdsa_p256 | 0 .../verified_signers_ecdsa_p384 | 0 .../verified_signers_ecdsa_p521 | 0 .../ssh_signatures/verified_signers_ed25519 | 0 .../ssh_signatures/verified_signers_rsa | 0 git/signatures/ssh_signature_keys_test.go | 294 ---------------- 74 files changed, 338 insertions(+), 379 deletions(-) rename git/{signatures => signature}/gpg_signature.go (92%) rename git/{signatures => signature}/gpg_signature_test.go (95%) rename git/{signatures => signature}/signature.go (84%) rename git/{signatures => signature}/signature_test.go (98%) rename git/{signatures => signature}/ssh_signature.go (86%) rename git/{signatures => signature}/ssh_signature_test.go (55%) rename git/{signatures => signature}/testdata/gpg_signatures/README.md (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_brainpool_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_brainpool_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_brainpool_p512_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_ed25519_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_ed448_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_rsa_2048_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_rsa_4096_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/commit_unsigned.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/generate_gpg_fixtures.sh (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_brainpool_p256.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_brainpool_p384.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_brainpool_p512.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_ecdsa_p256.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_ecdsa_p384.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_ecdsa_p521.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_ed25519.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_ed448.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_rsa_2048.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/key_rsa_4096.pub (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_brainpool_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_brainpool_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_brainpool_p512_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_ed25519_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_ed448_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_rsa_2048_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_rsa_4096_signed.txt (100%) rename git/{signatures => signature}/testdata/gpg_signatures/tag_unsigned.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/README.md (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_ed25519_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_rsa_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/commit_unsigned.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/generate_ssh_fixtures.sh (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p256.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p384.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p521.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ed25519.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_ed25519.pub_fingerprint (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_rsa.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/key_rsa.pub_fingerprint (100%) rename git/{signatures => signature}/testdata/ssh_signatures/keys_all.pub (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_ed25519_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_rsa_signed.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/tag_unsigned.txt (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_all (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_ecdsa_p256 (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_ecdsa_p384 (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_ecdsa_p521 (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_ed25519 (100%) rename git/{signatures => signature}/testdata/ssh_signatures/verified_signers_rsa (100%) delete mode 100644 git/signatures/ssh_signature_keys_test.go diff --git a/git/git.go b/git/git.go index 1d53cd33d..e0cbf35e9 100644 --- a/git/git.go +++ b/git/git.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/fluxcd/pkg/git/signatures" + "github.com/fluxcd/pkg/git/signature" ) const ( @@ -124,7 +124,7 @@ func (c *Commit) Verify(keyRings ...string) (string, error) { // tag (if present). Users are expected to explicitly verify the referencing // tag's signature using `c.ReferencingTag.Verify()` func (c *Commit) VerifyGPG(keyRings ...string) (string, error) { - fingerprint, err := signatures.VerifyPGPSignature(c.Signature, c.Encoded, keyRings...) + fingerprint, err := signature.VerifyPGPSignature(c.Signature, c.Encoded, keyRings...) if err != nil { return "", fmt.Errorf("unable to verify Git commit: %w", err) } @@ -136,7 +136,7 @@ func (c *Commit) VerifyGPG(keyRings ...string) (string, 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...) + fingerprint, err := signature.VerifySSHSignature(c.Signature, c.Encoded, authorizedKeys...) if err != nil { return "", fmt.Errorf("unable to verify Git commit SSH signature: %w", err) } @@ -179,7 +179,7 @@ func (t *Tag) Verify(keyRings ...string) (string, error) { // It returns the fingerprint of the key the signature was verified // with, or an error. func (t *Tag) VerifyGPG(keyRings ...string) (string, error) { - fingerprint, err := signatures.VerifyPGPSignature(t.Signature, t.Encoded, keyRings...) + fingerprint, err := signature.VerifyPGPSignature(t.Signature, t.Encoded, keyRings...) if err != nil { return "", fmt.Errorf("unable to verify Git tag: %w", err) } @@ -189,7 +189,7 @@ func (t *Tag) VerifyGPG(keyRings ...string) (string, error) { // 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...) + fingerprint, err := signature.VerifySSHSignature(t.Signature, t.Encoded, authorizedKeys...) if err != nil { return "", fmt.Errorf("unable to verify Git tag SSH signature: %w", err) } @@ -245,34 +245,34 @@ func IsSignedTag(t Tag) bool { // IsPGPSigned returns true if the commit has a PGP signature. func (c *Commit) IsPGPSigned() bool { - return signatures.IsPGPSignature(c.Signature) + return signature.IsPGPSignature(c.Signature) } // IsSSHSigned returns true if the commit has an SSH signature. func (c *Commit) IsSSHSigned() bool { - return signatures.IsSSHSignature(c.Signature) + return signature.IsSSHSignature(c.Signature) } // SignatureType returns the type of the commit signature as a string. -// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, // and "unknown" for unrecognized or empty signatures. func (c *Commit) SignatureType() string { - return signatures.GetSignatureType(c.Signature) + return signature.GetSignatureType(c.Signature) } // IsPGPSigned returns true if the tag has a PGP signature. func (t *Tag) IsPGPSigned() bool { - return signatures.IsPGPSignature(t.Signature) + return signature.IsPGPSignature(t.Signature) } // IsSSHSigned returns true if the tag has an SSH signature. func (t *Tag) IsSSHSigned() bool { - return signatures.IsSSHSignature(t.Signature) + return signature.IsSSHSignature(t.Signature) } // SignatureType returns the type of the tag signature as a string. -// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, // and "unknown" for unrecognized or empty signatures. func (t *Tag) SignatureType() string { - return signatures.GetSignatureType(t.Signature) + return signature.GetSignatureType(t.Signature) } diff --git a/git/git_test.go b/git/git_test.go index 3e286654d..2c7b6ba8c 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -573,7 +573,7 @@ func TestSignatureType(t *testing.T) { } func TestCommit_VerifyGPG(t *testing.T) { - testDataDir := filepath.Join("signatures", "testdata", "gpg_signatures") + testDataDir := filepath.Join("signature", "testdata", "gpg_signatures") tests := []struct { name string @@ -662,7 +662,7 @@ func TestCommit_VerifyGPG(t *testing.T) { } func TestTag_VerifyGPG(t *testing.T) { - testDataDir := filepath.Join("signatures", "testdata", "gpg_signatures") + testDataDir := filepath.Join("signature", "testdata", "gpg_signatures") tests := []struct { name string @@ -751,7 +751,7 @@ func TestTag_VerifyGPG(t *testing.T) { } func TestCommit_VerifySSH(t *testing.T) { - testDataDir := filepath.Join("signatures", "testdata", "ssh_signatures") + testDataDir := filepath.Join("signature", "testdata", "ssh_signatures") tests := []struct { name string @@ -829,7 +829,7 @@ func TestCommit_VerifySSH(t *testing.T) { } func TestTag_VerifySSH(t *testing.T) { - testDataDir := filepath.Join("signatures", "testdata", "ssh_signatures") + testDataDir := filepath.Join("signature", "testdata", "ssh_signatures") tests := []struct { name string diff --git a/git/signatures/gpg_signature.go b/git/signature/gpg_signature.go similarity index 92% rename from git/signatures/gpg_signature.go rename to git/signature/gpg_signature.go index 94c2ae2ac..3d4cea221 100644 --- a/git/signatures/gpg_signature.go +++ b/git/signature/gpg_signature.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures +package signature import ( "bytes" @@ -32,7 +32,7 @@ var PGPSignaturePrefix = []string{ } // VerifyPGPSignature verifies the PGP signature against the payload using -// the provided key rings. It returns the fingerprint of the key that +// the provided key rings. It returns the key ID of the key that // successfully verified the signature, or an error. func VerifyPGPSignature(signature string, payload []byte, keyRings ...string) (string, error) { if signature == "" { @@ -53,7 +53,7 @@ func VerifyPGPSignature(signature string, payload []byte, keyRings ...string) (s 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) + signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(payload), strings.NewReader(signature), nil) if err == nil { return signer.PrimaryKey.KeyIdString(), nil } diff --git a/git/signatures/gpg_signature_test.go b/git/signature/gpg_signature_test.go similarity index 95% rename from git/signatures/gpg_signature_test.go rename to git/signature/gpg_signature_test.go index 40c7c6727..56da46564 100644 --- a/git/signatures/gpg_signature_test.go +++ b/git/signature/gpg_signature_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures_test +package signature_test import ( "os" @@ -22,7 +22,7 @@ import ( "testing" "github.com/fluxcd/pkg/git/gogit" - "github.com/fluxcd/pkg/git/signatures" + "github.com/fluxcd/pkg/git/signature" "github.com/fluxcd/pkg/git/testutils" "github.com/go-git/go-git/v5/plumbing" . "github.com/onsi/gomega" @@ -177,7 +177,7 @@ func TestVerifyPGPSignature(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - got, err := signatures.VerifyPGPSignature(tt.sig, tt.payload, tt.keyRings...) + got, err := signature.VerifyPGPSignature(tt.sig, tt.payload, tt.keyRings...) if tt.wantErr != "" { g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring(tt.wantErr)) @@ -245,7 +245,7 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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)) + fingerprint, err := signature.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, string(publicKey)) if kt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(fingerprint).To(BeEmpty()) @@ -256,7 +256,7 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { g.Expect(fingerprint).ToNot(BeEmpty()) // Verify the signature using the multi-key keyring - fingerprint, err = signatures.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, allKeysRing...) + fingerprint, err = signature.VerifyPGPSignature(gitTag.Signature, gitTag.Encoded, allKeysRing...) if kt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(fingerprint).To(BeEmpty()) @@ -286,7 +286,7 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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)) + fingerprint, err := signature.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, string(publicKey)) if kt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(fingerprint).To(BeEmpty()) @@ -297,7 +297,7 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { g.Expect(fingerprint).ToNot(BeEmpty()) // Verify the signature using the multi-key keyring - fingerprint, err = signatures.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, allKeysRing...) + fingerprint, err = signature.VerifyPGPSignature(gitCommit.Signature, gitCommit.Encoded, allKeysRing...) if kt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(fingerprint).To(BeEmpty()) @@ -327,7 +327,7 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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)) + fingerprint, err := signature.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/signature/signature.go similarity index 84% rename from git/signatures/signature.go rename to git/signature/signature.go index 5b7ba3ed5..8861dce43 100644 --- a/git/signatures/signature.go +++ b/git/signature/signature.go @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures +package signature import ( + "slices" "strings" ) @@ -40,30 +41,20 @@ const ( // 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) + return slices.ContainsFunc(PGPSignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) } // IsSSHSignature tests if the given signature is of type SSH. // It returns true if the signature starts with the SSH signature prefix. func IsSSHSignature(signature string) bool { - return startsWithStrings(signature, SSHSignaturePrefix) + return slices.ContainsFunc(SSHSignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) } // IsX509Signature tests if the given signature is of type x509. @@ -71,7 +62,9 @@ func IsSSHSignature(signature string) bool { // 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) + return slices.ContainsFunc(X509SignaturePrefix, func(prefix string) bool { + return strings.HasPrefix(strings.TrimSpace(signature), prefix) + }) } // IsEmptySignature tests if the given signature string is empty. @@ -81,7 +74,7 @@ func IsEmptySignature(signature string) bool { } // GetSignatureType returns the type of the signature as a string. -// It returns "pgp" for PGP signatures, "ssh" for SSH signatures, +// It returns "openpgp" for PGP signatures, "ssh" for SSH signatures, // "x509" for S/MIME signatures, "empty" for an empty signature // and "unknown" for unrecognized signatures. func GetSignatureType(signature string) string { diff --git a/git/signatures/signature_test.go b/git/signature/signature_test.go similarity index 98% rename from git/signatures/signature_test.go rename to git/signature/signature_test.go index b079b8784..c50f3c02e 100644 --- a/git/signatures/signature_test.go +++ b/git/signature/signature_test.go @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures_test +package signature_test import ( "testing" - . "github.com/fluxcd/pkg/git/signatures" + . "github.com/fluxcd/pkg/git/signature" ) func TestIsPGPSignature(t *testing.T) { diff --git a/git/signatures/ssh_signature.go b/git/signature/ssh_signature.go similarity index 86% rename from git/signatures/ssh_signature.go rename to git/signature/ssh_signature.go index f9c80719a..c052a73f1 100644 --- a/git/signatures/ssh_signature.go +++ b/git/signature/ssh_signature.go @@ -14,12 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures +package signature import ( "bytes" - "crypto/sha256" - "encoding/base64" "fmt" "strings" @@ -38,7 +36,7 @@ var SSHSignaturePrefix = []string{"-----BEGIN SSH SIGNATURE-----"} func ParseAuthorizedKeys(authorizedKeys string) ([]gossh.PublicKey, error) { var publicKeys []gossh.PublicKey - for _, line := range strings.Split(authorizedKeys, "\n") { + for line := range strings.Lines(authorizedKeys) { line = strings.TrimSpace(line) // Skip empty lines and comments @@ -93,17 +91,10 @@ func VerifySSHSignature(signature string, payload []byte, authorizedKeys ...stri err := sshsig.Verify(bytes.NewReader(payload), sig, pubKey, sig.HashAlgorithm, SSHSignatureNamespace) if err == nil { // Signature verified successfully - return getPublicKeyFingerprint(pubKey), nil + return gossh.FingerprintSHA256(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_test.go b/git/signature/ssh_signature_test.go similarity index 55% rename from git/signatures/ssh_signature_test.go rename to git/signature/ssh_signature_test.go index 80cacb12b..fb447abba 100644 --- a/git/signatures/ssh_signature_test.go +++ b/git/signature/ssh_signature_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signatures_test +package signature_test import ( "os" @@ -23,9 +23,10 @@ import ( "testing" "github.com/fluxcd/pkg/git/gogit" - "github.com/fluxcd/pkg/git/signatures" + "github.com/fluxcd/pkg/git/signature" "github.com/fluxcd/pkg/git/testutils" "github.com/go-git/go-git/v5/plumbing" + gossh "golang.org/x/crypto/ssh" ) // these tests are in a different package to avoid circular dependencies with gogit.BuildCommitWithRef and gogit.BuildTag @@ -123,7 +124,7 @@ func TestVerifySSHSignature(t *testing.T) { 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)) + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(authorizedKey)) if err != nil { t.Errorf("Commit signature VerifySSHSignature() error = %v", err) } @@ -135,7 +136,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Verifying the correct fingerprint is returned from a list of public keys - fingerprint, err = signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(pubKeysAll)) + fingerprint, err = signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, string(pubKeysAll)) if err != nil { t.Errorf("Commit signature VerifySSHSignature() error = %v", err) } @@ -147,7 +148,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Verify the signature using the git.Tag's Signature and Encoded fields - fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKey)) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(authorizedKey)) if err != nil { t.Errorf("Tag signature VerifySSHSignature() error = %v", err) } @@ -159,7 +160,7 @@ func TestVerifySSHSignature(t *testing.T) { } // Verifying the correct fingerprint is returned from a list of public keys - fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(pubKeysAll)) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, string(pubKeysAll)) if err != nil { t.Errorf("Tag signature VerifySSHSignature() error = %v", err) } @@ -177,21 +178,21 @@ func TestVerifySSHSignature(t *testing.T) { func TestSSHSignatureValidationCases(t *testing.T) { testDataDir := filepath.Join("testdata", "ssh_signatures") - key_type := "ed25519" + keyType := "ed25519" - pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_"+key_type+".pub")) + pubKey, err := os.ReadFile(filepath.Join(testDataDir, "key_"+keyType+".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")) + commitObj, err := testutils.ParseCommitFromFixture(filepath.Join(testDataDir, "commit_"+keyType+"_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")) + tagObj, err := testutils.ParseTagFromFixture(filepath.Join(testDataDir, "tag_"+keyType+"_signed.txt")) if err != nil { t.Fatalf("Failed to parse tag from fixture: %v", err) } @@ -211,7 +212,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { // Test error cases t.Run("empty signature", func(t *testing.T) { - fingerprint, err := signatures.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) + fingerprint, err := signature.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") } @@ -222,7 +223,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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)) + fingerprint, err = signature.VerifySSHSignature("", gitCommit.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty signature, got nil") } @@ -237,7 +238,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { t.Run("empty payload", func(t *testing.T) { - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, []byte{}, string(pubKey)) + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, []byte{}, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") } @@ -248,7 +249,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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)) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, []byte{}, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty payload, got nil") } @@ -265,7 +266,7 @@ func TestSSHSignatureValidationCases(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) + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, wrongKey) if err == nil { t.Errorf("VerifySSHSignature() expected error for wrong authorized keys, got nil") } @@ -277,7 +278,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, wrongKey) if err == nil { t.Errorf("VerifySSHSignature() expected error for wrong authorized keys, got nil") } @@ -294,7 +295,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { // Use empty authorized keys emptyAuthKeys := "" - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, emptyAuthKeys) + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, emptyAuthKeys) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty authorized keys, got nil") } @@ -305,7 +306,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, emptyAuthKeys) if err == nil { t.Errorf("VerifySSHSignature() expected error for empty authorized keys, got nil") } @@ -320,7 +321,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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)) + fingerprint, err := signature.VerifySSHSignature(invalidSig, gitCommit.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") } @@ -331,7 +332,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to unarmor SSH signature'", err) } - fingerprint, err = signatures.VerifySSHSignature(invalidSig, gitTag.Encoded, string(pubKey)) + fingerprint, err = signature.VerifySSHSignature(invalidSig, gitTag.Encoded, string(pubKey)) if err == nil { t.Errorf("VerifySSHSignature() expected error for invalid signature, got nil") } @@ -348,7 +349,7 @@ func TestSSHSignatureValidationCases(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, "") + fingerprint, err := signature.VerifySSHSignature(pgpSig, gitCommit.Encoded, "") if err == nil { t.Errorf("VerifySSHSignature() expected error for non-SSH signature, got nil") } @@ -359,7 +360,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { t.Errorf("VerifySSHSignature() error = %v, want 'unable to verify SSH signature, detected signature format: openpgp'", err) } - fingerprint, err = signatures.VerifySSHSignature(pgpSig, gitTag.Encoded, "") + fingerprint, err = signature.VerifySSHSignature(pgpSig, gitTag.Encoded, "") if err == nil { t.Errorf("VerifySSHSignature() expected error for non-SSH signature, got nil") } @@ -375,7 +376,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { // Use invalid authorized keys invalidAuthKeys := "invalid-key-data" - fingerprint, err := signatures.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, invalidAuthKeys) + fingerprint, err := signature.VerifySSHSignature(gitCommit.Signature, gitCommit.Encoded, invalidAuthKeys) if err == nil { t.Errorf("VerifySSHSignature() expected error for invalid authorized keys, got nil") } @@ -386,7 +387,7 @@ func TestSSHSignatureValidationCases(t *testing.T) { t.Errorf("VerifySSHSignature() error = %v, want error containing 'unable to parse authorized key'", err) } - fingerprint, err = signatures.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, invalidAuthKeys) + fingerprint, err = signature.VerifySSHSignature(gitTag.Signature, gitTag.Encoded, invalidAuthKeys) if err == nil { t.Errorf("VerifySSHSignature() expected error for invalid authorized keys, got nil") } @@ -398,3 +399,271 @@ func TestSSHSignatureValidationCases(t *testing.T) { } }) } + +func TestParseAuthorizedKeysAndPublicFingerprint(t *testing.T) { + tests := []struct { + name string + authorizedKeys string + wantCount int + wantErr bool + wantFingerprints []string + }{ + { + name: "single key", + authorizedKeys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com", + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM"}, + }, + { + name: "key with additional directives", + authorizedKeys: "no-user-rc,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test@example.com additional long comment about nothing", + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM"}, + }, + { + name: "multiple keys", + authorizedKeys: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPbmoVMAS5Ttg77s9DLSAOf4gXCiQpgdRekFHlzbXHLH test1@example.com +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL7Xspf5BmRD7ipGo4SNCftjzeunry1znmU78RhcVOYwLNCR5MVm22N9c1aYacIxHmi/TxkNTdQdEB8dd4mfA4Q= test-ecdsa_p256@example.com`, + wantCount: 2, + wantErr: false, + wantFingerprints: []string{"SHA256:CGIPzdGcFuLkjItmqTm5kJNvof4yB662MxZXoxntLYM", "SHA256:oU8IT7UOnJlOTOvr/W1cYf1SkdocFm5F7SAXOwuo8Kc"}, + }, + { + name: "with comments", + authorizedKeys: `# This is a comment +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKQ9Upb3Pa7b5NWbozm20PqpFc5WZCCCBlX9+eFELAjKdBze2EbTTKvx9YskKJ8PWLE8D9w20sjDivNwfUjoiZGgbJQcJKcKPrtovOYPv0JKpoyZ0PuLpq9kjSRTRnShEw== test-ecdsa_p384@example.com +# Another comment`, + wantCount: 1, + wantErr: false, + wantFingerprints: []string{"SHA256:+vwrYGpHfAAWIzT2x+uV+duJG7ZnSvCbRKwdPApx7JA"}, + }, + { + name: "with empty lines", + authorizedKeys: `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGSY+OAEbrNSJ4QD6NgJIJQV8kmjqi+BhfeAAthEv0eCq1CADrCqKt0poxCahYNCTMLlMvqW7xBw6wDB0kV0/4CTwBX9HRftFUpaZanPtfvMNhPT/CDMrTsNSzg/H32Hu/fuvLwyPQ0JzRXgf+qiq3OZ4q0VjERU7L13UDoz4FgHJIeVQ== test-ecdsa_p521@example.com + +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCpreiO+8XsB4xXGNmwuO48a7WPghb5ihCJNPyQZpnaPfq6vhNVWSgq8AIjBmJOJYo4HZyiHqpS4OBc86glk6qMv8YHRt4VRVBP+DjPLDIsOR7+2HBlOPHMm8lTDi+iMPHBDxqFy7mSDB4+v7n700+49vYhWjZJpesnnE6JoitxSVhmqp75jeNRNU6PD00z+gMUcviv8UOs/Apg1Cw5f+4T9yOnjlOHaFH/ButvZ0t2VF0cs28tfCuLAoumjine5Gm6tCRQlZOoapNJzvnYT+86f/PEU/4kDYf3wT7S+NnUDfCsIpDVlOXPvjnQ/DudhqEnnXvfch+eBCI7rtJBHIGPKFdmC4cUROa0UDGR6o/JxLtx4ZTbkGpq6MVwdrb7qJ+Oib1U8xVimWFfarkm7deVXWD3wB5Wa8Ko/a/WuYfE3gYRhb8iXPYd71FsEy4F41JCMZDcIqMiQRe3e2gvY+z2sf02kHOFeWJmrAY9FFjPL85VD0Dg++jrExkGFjcBTw9gUG5OPGpwqQ9WHO8E8DPza+i5J/wu4DODyLrLxuXHPeSYUjcvh5ln8P70qL+Irwn1mgn2PkIZW0XCPBt6Iylg55t5sfyy03P0Kmb4U3TrppMeig7Lr9LDU4Doh7Fj6oLYDGFUV+F52SSuPs5SfrWd6Apiz+VPjsAh5btPPJNlzQ== test-rsa@example.com`, + wantCount: 2, + wantErr: false, + wantFingerprints: []string{"SHA256:3FcWgX5RsACruglrcBJP/hefUZcYHJGnrk07U6yKin8", "SHA256:TxoYgaeIj5A7Md4rHNfxPdqawooc4NIGjIMbcQ7YKbw"}, + }, + { + name: "empty", + authorizedKeys: "", + wantCount: 0, + wantErr: false, + wantFingerprints: []string{}, + }, + { + name: "invalid key", + authorizedKeys: "invalid-key-data", + wantCount: 0, + wantErr: true, + wantFingerprints: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keys, err := signature.ParseAuthorizedKeys(tt.authorizedKeys) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + // Validate expected fingerprint if specified + if len(tt.wantFingerprints) > 0 && len(keys) > 0 { + for _, key := range keys { + found := false + fingerprint := gossh.FingerprintSHA256(key) + for _, wantedFingerprint := range tt.wantFingerprints { + if fingerprint == wantedFingerprint { + found = true + } + } + if !found { + t.Errorf("ParseAuthorizedKeys() fingerprint '%s'not in list of wanted fingerprints %s", fingerprint, tt.wantFingerprints) + } + } + } + }) + } +} + +func TestParseAuthorizedKeysFromFixtures(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixture string + fingerprintFile string + wantCount int + wantErr bool + }{ + { + name: "ed25519 key", + fixture: "key_ed25519.pub", + fingerprintFile: "key_ed25519.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "rsa key", + fixture: "key_rsa.pub", + fingerprintFile: "key_rsa.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p256 key", + fixture: "key_ecdsa_p256.pub", + fingerprintFile: "key_ecdsa_p256.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p384 key", + fixture: "key_ecdsa_p384.pub", + fingerprintFile: "key_ecdsa_p384.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "ecdsa p521 key", + fixture: "key_ecdsa_p521.pub", + fingerprintFile: "key_ecdsa_p521.pub_fingerprint", + wantCount: 1, + wantErr: false, + }, + { + name: "all key types combined", + fixture: "keys_all.pub", + wantCount: 5, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, tt.fixture)) + if err != nil { + t.Fatalf("Failed to read fixture file %s: %v", tt.fixture, err) + } + + keys, err := signature.ParseAuthorizedKeys(string(authorizedKeys)) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + + // Read expected fingerprint from file if provided + var expectedFingerprint string + if tt.fingerprintFile != "" { + fingerprintData, err := os.ReadFile(filepath.Join(testDataDir, tt.fingerprintFile)) + if err != nil { + t.Fatalf("Failed to read fingerprint file %s: %v", tt.fingerprintFile, err) + } + expectedFingerprint = strings.TrimSpace(string(fingerprintData)) + } + + // Verify that each key has a valid fingerprint + for i, key := range keys { + fingerprint := gossh.FingerprintSHA256(key) + if fingerprint == "" { + t.Errorf("Key %d has empty fingerprint", i) + } + if !strings.HasPrefix(fingerprint, "SHA256:") { + t.Errorf("Key %d fingerprint %s does not have SHA256: prefix", i, fingerprint) + } + // Validate fingerprint against the one read from file + if expectedFingerprint != "" { + if fingerprint != expectedFingerprint { + t.Errorf("Key %d got fingerprint %s, want %s (from %s)", i, fingerprint, expectedFingerprint, tt.fingerprintFile) + } + } + } + }) + } +} + +func TestParseAuthorizedKeysCombinations(t *testing.T) { + testDataDir := filepath.Join("testdata", "ssh_signatures") + + tests := []struct { + name string + fixtures []string + wantCount int + wantErr bool + }{ + { + name: "ed25519 + rsa", + fixtures: []string{"key_ed25519.pub", "key_rsa.pub"}, + wantCount: 2, + wantErr: false, + }, + { + name: "ed25519 + ecdsa p256", + fixtures: []string{"key_ed25519.pub", "key_ecdsa_p256.pub"}, + wantCount: 2, + wantErr: false, + }, + { + name: "rsa + ecdsa p384 + ecdsa p521", + fixtures: []string{"key_rsa.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 3, + wantErr: false, + }, + { + name: "all ecdsa variants", + fixtures: []string{"key_ecdsa_p256.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 3, + wantErr: false, + }, + { + name: "ed25519 + rsa + all ecdsa", + fixtures: []string{"key_ed25519.pub", "key_rsa.pub", "key_ecdsa_p256.pub", "key_ecdsa_p384.pub", "key_ecdsa_p521.pub"}, + wantCount: 5, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var combinedKeys strings.Builder + for _, fixture := range tt.fixtures { + authorizedKeys, err := os.ReadFile(filepath.Join(testDataDir, fixture)) + if err != nil { + t.Fatalf("Failed to read fixture file %s: %v", fixture, err) + } + combinedKeys.Write(authorizedKeys) + combinedKeys.WriteString("\n") + } + + keys, err := signature.ParseAuthorizedKeys(combinedKeys.String()) + if (err != nil) != tt.wantErr { + t.Errorf("ParseAuthorizedKeys() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(keys) != tt.wantCount { + t.Errorf("ParseAuthorizedKeys() got %d keys, want %d", len(keys), tt.wantCount) + } + + // Verify that each key has a valid fingerprint + for i, key := range keys { + fingerprint := gossh.FingerprintSHA256(key) + if fingerprint == "" { + t.Errorf("Key %d has empty fingerprint", i) + } + if !strings.HasPrefix(fingerprint, "SHA256:") { + t.Errorf("Key %d fingerprint %s does not have SHA256: prefix", i, fingerprint) + } + } + }) + } +} diff --git a/git/signatures/testdata/gpg_signatures/README.md b/git/signature/testdata/gpg_signatures/README.md similarity index 100% rename from git/signatures/testdata/gpg_signatures/README.md rename to git/signature/testdata/gpg_signatures/README.md diff --git a/git/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p256_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_brainpool_p256_signed.txt rename to git/signature/testdata/gpg_signatures/commit_brainpool_p256_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p384_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_brainpool_p384_signed.txt rename to git/signature/testdata/gpg_signatures/commit_brainpool_p384_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt b/git/signature/testdata/gpg_signatures/commit_brainpool_p512_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_brainpool_p512_signed.txt rename to git/signature/testdata/gpg_signatures/commit_brainpool_p512_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt rename to git/signature/testdata/gpg_signatures/commit_ecdsa_p256_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt rename to git/signature/testdata/gpg_signatures/commit_ecdsa_p384_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt b/git/signature/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt rename to git/signature/testdata/gpg_signatures/commit_ecdsa_p521_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt b/git/signature/testdata/gpg_signatures/commit_ed25519_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_ed25519_signed.txt rename to git/signature/testdata/gpg_signatures/commit_ed25519_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt b/git/signature/testdata/gpg_signatures/commit_ed448_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_ed448_signed.txt rename to git/signature/testdata/gpg_signatures/commit_ed448_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt b/git/signature/testdata/gpg_signatures/commit_rsa_2048_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_rsa_2048_signed.txt rename to git/signature/testdata/gpg_signatures/commit_rsa_2048_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt b/git/signature/testdata/gpg_signatures/commit_rsa_4096_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_rsa_4096_signed.txt rename to git/signature/testdata/gpg_signatures/commit_rsa_4096_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/commit_unsigned.txt b/git/signature/testdata/gpg_signatures/commit_unsigned.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/commit_unsigned.txt rename to git/signature/testdata/gpg_signatures/commit_unsigned.txt diff --git a/git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh b/git/signature/testdata/gpg_signatures/generate_gpg_fixtures.sh similarity index 100% rename from git/signatures/testdata/gpg_signatures/generate_gpg_fixtures.sh rename to git/signature/testdata/gpg_signatures/generate_gpg_fixtures.sh diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p256.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_brainpool_p256.pub rename to git/signature/testdata/gpg_signatures/key_brainpool_p256.pub diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p384.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_brainpool_p384.pub rename to git/signature/testdata/gpg_signatures/key_brainpool_p384.pub diff --git a/git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub b/git/signature/testdata/gpg_signatures/key_brainpool_p512.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_brainpool_p512.pub rename to git/signature/testdata/gpg_signatures/key_brainpool_p512.pub diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p256.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_ecdsa_p256.pub rename to git/signature/testdata/gpg_signatures/key_ecdsa_p256.pub diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p384.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_ecdsa_p384.pub rename to git/signature/testdata/gpg_signatures/key_ecdsa_p384.pub diff --git a/git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub b/git/signature/testdata/gpg_signatures/key_ecdsa_p521.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_ecdsa_p521.pub rename to git/signature/testdata/gpg_signatures/key_ecdsa_p521.pub diff --git a/git/signatures/testdata/gpg_signatures/key_ed25519.pub b/git/signature/testdata/gpg_signatures/key_ed25519.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_ed25519.pub rename to git/signature/testdata/gpg_signatures/key_ed25519.pub diff --git a/git/signatures/testdata/gpg_signatures/key_ed448.pub b/git/signature/testdata/gpg_signatures/key_ed448.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_ed448.pub rename to git/signature/testdata/gpg_signatures/key_ed448.pub diff --git a/git/signatures/testdata/gpg_signatures/key_rsa_2048.pub b/git/signature/testdata/gpg_signatures/key_rsa_2048.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_rsa_2048.pub rename to git/signature/testdata/gpg_signatures/key_rsa_2048.pub diff --git a/git/signatures/testdata/gpg_signatures/key_rsa_4096.pub b/git/signature/testdata/gpg_signatures/key_rsa_4096.pub similarity index 100% rename from git/signatures/testdata/gpg_signatures/key_rsa_4096.pub rename to git/signature/testdata/gpg_signatures/key_rsa_4096.pub diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p256_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_brainpool_p256_signed.txt rename to git/signature/testdata/gpg_signatures/tag_brainpool_p256_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p384_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_brainpool_p384_signed.txt rename to git/signature/testdata/gpg_signatures/tag_brainpool_p384_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt b/git/signature/testdata/gpg_signatures/tag_brainpool_p512_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_brainpool_p512_signed.txt rename to git/signature/testdata/gpg_signatures/tag_brainpool_p512_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt rename to git/signature/testdata/gpg_signatures/tag_ecdsa_p256_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt rename to git/signature/testdata/gpg_signatures/tag_ecdsa_p384_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt b/git/signature/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt rename to git/signature/testdata/gpg_signatures/tag_ecdsa_p521_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt b/git/signature/testdata/gpg_signatures/tag_ed25519_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_ed25519_signed.txt rename to git/signature/testdata/gpg_signatures/tag_ed25519_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt b/git/signature/testdata/gpg_signatures/tag_ed448_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_ed448_signed.txt rename to git/signature/testdata/gpg_signatures/tag_ed448_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt b/git/signature/testdata/gpg_signatures/tag_rsa_2048_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_rsa_2048_signed.txt rename to git/signature/testdata/gpg_signatures/tag_rsa_2048_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt b/git/signature/testdata/gpg_signatures/tag_rsa_4096_signed.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_rsa_4096_signed.txt rename to git/signature/testdata/gpg_signatures/tag_rsa_4096_signed.txt diff --git a/git/signatures/testdata/gpg_signatures/tag_unsigned.txt b/git/signature/testdata/gpg_signatures/tag_unsigned.txt similarity index 100% rename from git/signatures/testdata/gpg_signatures/tag_unsigned.txt rename to git/signature/testdata/gpg_signatures/tag_unsigned.txt diff --git a/git/signatures/testdata/ssh_signatures/README.md b/git/signature/testdata/ssh_signatures/README.md similarity index 100% rename from git/signatures/testdata/ssh_signatures/README.md rename to git/signature/testdata/ssh_signatures/README.md diff --git a/git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt rename to git/signature/testdata/ssh_signatures/commit_ecdsa_p256_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt rename to git/signature/testdata/ssh_signatures/commit_ecdsa_p384_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt b/git/signature/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt rename to git/signature/testdata/ssh_signatures/commit_ecdsa_p521_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt b/git/signature/testdata/ssh_signatures/commit_ed25519_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_ed25519_signed.txt rename to git/signature/testdata/ssh_signatures/commit_ed25519_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt b/git/signature/testdata/ssh_signatures/commit_rsa_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_rsa_signed.txt rename to git/signature/testdata/ssh_signatures/commit_rsa_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/commit_unsigned.txt b/git/signature/testdata/ssh_signatures/commit_unsigned.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/commit_unsigned.txt rename to git/signature/testdata/ssh_signatures/commit_unsigned.txt diff --git a/git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh b/git/signature/testdata/ssh_signatures/generate_ssh_fixtures.sh similarity index 100% rename from git/signatures/testdata/ssh_signatures/generate_ssh_fixtures.sh rename to git/signature/testdata/ssh_signatures/generate_ssh_fixtures.sh diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub rename to git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint rename to git/signature/testdata/ssh_signatures/key_ecdsa_p256.pub_fingerprint diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub rename to git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint rename to git/signature/testdata/ssh_signatures/key_ecdsa_p384.pub_fingerprint diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub rename to git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub diff --git a/git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint rename to git/signature/testdata/ssh_signatures/key_ecdsa_p521.pub_fingerprint diff --git a/git/signatures/testdata/ssh_signatures/key_ed25519.pub b/git/signature/testdata/ssh_signatures/key_ed25519.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ed25519.pub rename to git/signature/testdata/ssh_signatures/key_ed25519.pub diff --git a/git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_ed25519.pub_fingerprint similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_ed25519.pub_fingerprint rename to git/signature/testdata/ssh_signatures/key_ed25519.pub_fingerprint diff --git a/git/signatures/testdata/ssh_signatures/key_rsa.pub b/git/signature/testdata/ssh_signatures/key_rsa.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_rsa.pub rename to git/signature/testdata/ssh_signatures/key_rsa.pub diff --git a/git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint b/git/signature/testdata/ssh_signatures/key_rsa.pub_fingerprint similarity index 100% rename from git/signatures/testdata/ssh_signatures/key_rsa.pub_fingerprint rename to git/signature/testdata/ssh_signatures/key_rsa.pub_fingerprint diff --git a/git/signatures/testdata/ssh_signatures/keys_all.pub b/git/signature/testdata/ssh_signatures/keys_all.pub similarity index 100% rename from git/signatures/testdata/ssh_signatures/keys_all.pub rename to git/signature/testdata/ssh_signatures/keys_all.pub diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt rename to git/signature/testdata/ssh_signatures/tag_ecdsa_p256_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt rename to git/signature/testdata/ssh_signatures/tag_ecdsa_p384_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt b/git/signature/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt rename to git/signature/testdata/ssh_signatures/tag_ecdsa_p521_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt b/git/signature/testdata/ssh_signatures/tag_ed25519_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_ed25519_signed.txt rename to git/signature/testdata/ssh_signatures/tag_ed25519_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt b/git/signature/testdata/ssh_signatures/tag_rsa_signed.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_rsa_signed.txt rename to git/signature/testdata/ssh_signatures/tag_rsa_signed.txt diff --git a/git/signatures/testdata/ssh_signatures/tag_unsigned.txt b/git/signature/testdata/ssh_signatures/tag_unsigned.txt similarity index 100% rename from git/signatures/testdata/ssh_signatures/tag_unsigned.txt rename to git/signature/testdata/ssh_signatures/tag_unsigned.txt diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_all b/git/signature/testdata/ssh_signatures/verified_signers_all similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_all rename to git/signature/testdata/ssh_signatures/verified_signers_all diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 b/git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p256 similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p256 rename to git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p256 diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 b/git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p384 similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p384 rename to git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p384 diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 b/git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p521 similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_ecdsa_p521 rename to git/signature/testdata/ssh_signatures/verified_signers_ecdsa_p521 diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_ed25519 b/git/signature/testdata/ssh_signatures/verified_signers_ed25519 similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_ed25519 rename to git/signature/testdata/ssh_signatures/verified_signers_ed25519 diff --git a/git/signatures/testdata/ssh_signatures/verified_signers_rsa b/git/signature/testdata/ssh_signatures/verified_signers_rsa similarity index 100% rename from git/signatures/testdata/ssh_signatures/verified_signers_rsa rename to git/signature/testdata/ssh_signatures/verified_signers_rsa diff --git a/git/signatures/ssh_signature_keys_test.go b/git/signatures/ssh_signature_keys_test.go deleted file mode 100644 index 3761d469a..000000000 --- a/git/signatures/ssh_signature_keys_test.go +++ /dev/null @@ -1,294 +0,0 @@ -/* -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) - } - } - }) - } -} From 5f6af02beaf4d2430d8119404e9d85330ea3171f Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Mon, 18 May 2026 23:37:21 +0200 Subject: [PATCH 15/16] switches signarture type from public to private Signed-off-by: Ricardo Bartels --- git/signature/signature.go | 22 +++++++++++----------- git/signature/signature_test.go | 22 ++++++++++------------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/git/signature/signature.go b/git/signature/signature.go index 8861dce43..1fd833c76 100644 --- a/git/signature/signature.go +++ b/git/signature/signature.go @@ -22,19 +22,19 @@ import ( ) // SignatureType represents the type of a signature. -type SignatureType string +type signatureType string const ( // SignatureTypePGP represents a openPGP signature. - SignatureTypePGP SignatureType = "openpgp" + signatureTypePGP signatureType = "openpgp" // SignatureTypeSSH represents an SSH signature. - SignatureTypeSSH SignatureType = "ssh" + signatureTypeSSH signatureType = "ssh" // SignatureTypeX509 represents an x509 signature. - SignatureTypeX509 SignatureType = "x509" + signatureTypeX509 signatureType = "x509" // SignatureTypeUnknown represents an unknown signature type. - SignatureTypeUnknown SignatureType = "unknown" + signatureTypeUnknown signatureType = "unknown" // SignatureTypeEmpty represents an empty signature. - SignatureTypeEmpty SignatureType = "empty" + signatureTypeEmpty signatureType = "empty" ) // IsX509Signature is the prefix used by Git to identify x509 signatures. @@ -79,16 +79,16 @@ func IsEmptySignature(signature string) bool { // and "unknown" for unrecognized signatures. func GetSignatureType(signature string) string { if IsPGPSignature(signature) { - return string(SignatureTypePGP) + return string(signatureTypePGP) } if IsSSHSignature(signature) { - return string(SignatureTypeSSH) + return string(signatureTypeSSH) } if IsX509Signature(signature) { - return string(SignatureTypeX509) + return string(signatureTypeX509) } if IsEmptySignature(signature) { - return string(SignatureTypeEmpty) + return string(signatureTypeEmpty) } - return string(SignatureTypeUnknown) + return string(signatureTypeUnknown) } diff --git a/git/signature/signature_test.go b/git/signature/signature_test.go index c50f3c02e..7bf82f2b6 100644 --- a/git/signature/signature_test.go +++ b/git/signature/signature_test.go @@ -14,12 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package signature_test +package signature import ( "testing" - - . "github.com/fluxcd/pkg/git/signature" ) func TestIsPGPSignature(t *testing.T) { @@ -187,47 +185,47 @@ func TestGetSignatureType(t *testing.T) { { name: "PGP signature", signature: "-----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - want: string(SignatureTypePGP), + want: string(signatureTypePGP), }, { name: "PGP signature with leading whitespace", signature: " -----BEGIN PGP SIGNATURE-----\n-----END PGP SIGNATURE-----", - want: string(SignatureTypePGP), + want: string(signatureTypePGP), }, { name: "SSH signature", signature: "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - want: string(SignatureTypeSSH), + want: string(signatureTypeSSH), }, { name: "SSH signature with leading whitespace", signature: " -----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----", - want: string(SignatureTypeSSH), + want: string(signatureTypeSSH), }, { name: "x509 signature", signature: "-----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", - want: string(SignatureTypeX509), + want: string(signatureTypeX509), }, { name: "x509 signature with leading whitespace", signature: " -----BEGIN SIGNED MESSAGE-----\n-----END SIGNED MESSAGE-----", - want: string(SignatureTypeX509), + want: string(signatureTypeX509), }, { name: "empty signature", signature: "", - want: string(SignatureTypeEmpty), + want: string(signatureTypeEmpty), }, { name: "unknown signature", signature: "-----BEGIN UNKNOWN SIGNATURE-----\n-----END UNKNOWN SIGNATURE-----", - want: string(SignatureTypeUnknown), + want: string(signatureTypeUnknown), }, { name: "whitespace only", signature: " \n\t ", - want: string(SignatureTypeUnknown), + want: string(signatureTypeUnknown), }, } From 94254473ec254683e940d0cdf64b4775992ac8c0 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 19 May 2026 00:00:40 +0200 Subject: [PATCH 16/16] moves buildTag and buildCommitWithRef to internal/build package Signed-off-by: Ricardo Bartels --- .gitignore | 2 +- git/gogit/clone.go | 88 ++---------------------- git/internal/build/build.go | 103 ++++++++++++++++++++++++++++ git/signature/gpg_signature_test.go | 14 ++-- git/signature/ssh_signature_test.go | 20 +++--- 5 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 git/internal/build/build.go diff --git a/.gitignore b/.gitignore index 892d69ea2..f0050ac90 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ -build/ +/build/ bin/ testbin/ diff --git a/git/gogit/clone.go b/git/gogit/clone.go index 8b78c3cbb..b59744db0 100644 --- a/git/gogit/clone.go +++ b/git/gogit/clone.go @@ -19,7 +19,6 @@ package gogit import ( "context" "fmt" - "io" "os" "sort" "strings" @@ -29,11 +28,11 @@ import ( extgogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/storage/memory" "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/internal/build" "github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/version" ) @@ -136,7 +135,7 @@ func (g *Client) cloneBranch(ctx context.Context, url, branch string, opts repos } g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return BuildCommitWithRef(cc, nil, ref) + return build.CommitWithRef(cc, nil, ref) } func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -236,7 +235,7 @@ func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository. g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return BuildCommitWithRef(cc, tagObj, ref) + return build.CommitWithRef(cc, tagObj, ref) } func (g *Client) cloneCommit(ctx context.Context, url, commit string, opts repository.CloneConfig) (*git.Commit, error) { @@ -305,7 +304,7 @@ func (g *Client) cloneCommit(ctx context.Context, url, commit string, opts repos g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return BuildCommitWithRef(cc, nil, cloneOpts.ReferenceName) + return build.CommitWithRef(cc, nil, cloneOpts.ReferenceName) } func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts repository.CloneConfig) (*git.Commit, error) { @@ -439,7 +438,7 @@ func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts re g.repository = repo g.sparseCheckoutDirectories = opts.SparseCheckoutDirectories - return BuildCommitWithRef(cc, tagObj, tagRef.Name()) + return build.CommitWithRef(cc, tagObj, tagRef.Name()) } func (g *Client) cloneRefName(ctx context.Context, url string, refName string, cloneOpts repository.CloneConfig) (*git.Commit, error) { @@ -574,83 +573,6 @@ func filterRefs(refs []*plumbing.Reference, currentRef plumbing.ReferenceName) s return "" } -func buildSignature(s object.Signature) git.Signature { - return git.Signature{ - Name: s.Name, - Email: s.Email, - When: s.When, - } -} - -func BuildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { - if t == nil { - return &git.Tag{ - Name: ref.Short(), - }, nil - } - - encoded := &plumbing.MemoryObject{} - if err := t.EncodeWithoutSignature(encoded); err != nil { - return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) - } - reader, err := encoded.Reader() - if err != nil { - return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) - } - b, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("unable to read encoded tag '%s': %w", t.Name, err) - } - - return &git.Tag{ - Hash: []byte(t.Hash.String()), - Name: t.Name, - Author: buildSignature(t.Tagger), - Signature: t.PGPSignature, - Encoded: b, - Message: t.Message, - }, nil -} - -func BuildCommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceName) (*git.Commit, error) { - if c == nil { - return nil, fmt.Errorf("unable to construct commit: no object") - } - - // Encode commit components excluding signature into SignedData. - encoded := &plumbing.MemoryObject{} - if err := c.EncodeWithoutSignature(encoded); err != nil { - return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) - } - reader, err := encoded.Reader() - if err != nil { - return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) - } - b, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("unable to read encoded commit '%s': %w", c.Hash, err) - } - cc := &git.Commit{ - Hash: []byte(c.Hash.String()), - Reference: ref.String(), - Author: buildSignature(c.Author), - Committer: buildSignature(c.Committer), - Signature: c.PGPSignature, - Encoded: b, - Message: c.Message, - } - - if ref.IsTag() { - tt, err := BuildTag(t, ref) - if err != nil { - return nil, err - } - cc.ReferencingTag = tt - } - - return cc, nil -} - func isRemoteBranchNotFoundErr(err error, ref string) bool { return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref '%s'", ref)) } diff --git a/git/internal/build/build.go b/git/internal/build/build.go new file mode 100644 index 000000000..773da26cb --- /dev/null +++ b/git/internal/build/build.go @@ -0,0 +1,103 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package build + +import ( + "fmt" + "io" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/fluxcd/pkg/git" +) + +func signature(s object.Signature) git.Signature { + return git.Signature{ + Name: s.Name, + Email: s.Email, + When: s.When, + } +} + +func Tag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) { + if t == nil { + return &git.Tag{ + Name: ref.Short(), + }, nil + } + + encoded := &plumbing.MemoryObject{} + if err := t.EncodeWithoutSignature(encoded); err != nil { + return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) + } + reader, err := encoded.Reader() + if err != nil { + return nil, fmt.Errorf("unable to encode tag '%s': %w", t.Name, err) + } + b, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("unable to read encoded tag '%s': %w", t.Name, err) + } + + return &git.Tag{ + Hash: []byte(t.Hash.String()), + Name: t.Name, + Author: signature(t.Tagger), + Signature: t.PGPSignature, + Encoded: b, + Message: t.Message, + }, nil +} + +func CommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceName) (*git.Commit, error) { + if c == nil { + return nil, fmt.Errorf("unable to construct commit: no object") + } + + encoded := &plumbing.MemoryObject{} + if err := c.EncodeWithoutSignature(encoded); err != nil { + return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) + } + reader, err := encoded.Reader() + if err != nil { + return nil, fmt.Errorf("unable to encode commit '%s': %w", c.Hash, err) + } + b, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("unable to read encoded commit '%s': %w", c.Hash, err) + } + cc := &git.Commit{ + Hash: []byte(c.Hash.String()), + Reference: ref.String(), + Author: signature(c.Author), + Committer: signature(c.Committer), + Signature: c.PGPSignature, + Encoded: b, + Message: c.Message, + } + + if ref.IsTag() { + tt, err := Tag(t, ref) + if err != nil { + return nil, err + } + cc.ReferencingTag = tt + } + + return cc, nil +} diff --git a/git/signature/gpg_signature_test.go b/git/signature/gpg_signature_test.go index 56da46564..5109c152b 100644 --- a/git/signature/gpg_signature_test.go +++ b/git/signature/gpg_signature_test.go @@ -21,7 +21,7 @@ import ( "path/filepath" "testing" - "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/internal/build" "github.com/fluxcd/pkg/git/signature" "github.com/fluxcd/pkg/git/testutils" "github.com/go-git/go-git/v5/plumbing" @@ -236,8 +236,8 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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")) + // Build a git.Tag using build.Tag + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) g.Expect(err).ToNot(HaveOccurred()) // Read the public key @@ -277,8 +277,8 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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")) + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) g.Expect(err).ToNot(HaveOccurred()) // Read the public key @@ -318,8 +318,8 @@ func TestVerifyPGPSignatureForCommitsAndTags(t *testing.T) { 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")) + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) g.Expect(err).ToNot(HaveOccurred()) // Read a public key diff --git a/git/signature/ssh_signature_test.go b/git/signature/ssh_signature_test.go index fb447abba..76dac1b1b 100644 --- a/git/signature/ssh_signature_test.go +++ b/git/signature/ssh_signature_test.go @@ -22,14 +22,14 @@ import ( "strings" "testing" - "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/internal/build" "github.com/fluxcd/pkg/git/signature" "github.com/fluxcd/pkg/git/testutils" "github.com/go-git/go-git/v5/plumbing" gossh "golang.org/x/crypto/ssh" ) -// these tests are in a different package to avoid circular dependencies with gogit.BuildCommitWithRef and gogit.BuildTag +// these tests are in a different package to avoid circular dependencies with build.CommitWithRef and build.Tag func TestVerifySSHSignature(t *testing.T) { testDataDir := filepath.Join("testdata", "ssh_signatures") @@ -93,8 +93,8 @@ func TestVerifySSHSignature(t *testing.T) { 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")) + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) if err != nil { t.Fatalf("Failed to build commit: %v", err) } @@ -105,8 +105,8 @@ func TestVerifySSHSignature(t *testing.T) { 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")) + // Build a git.Commit using build.CommitWithRef + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) if err != nil { t.Fatalf("Failed to build commit: %v", err) } @@ -197,14 +197,14 @@ func TestSSHSignatureValidationCases(t *testing.T) { 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")) + // Build a git.Commit using build.CommitWithRef + gitCommit, err := build.CommitWithRef(commitObj, nil, plumbing.ReferenceName("refs/heads/main")) if err != nil { t.Fatalf("Failed to build commit: %v", err) } - // Build a git.Tag using BuildTag - gitTag, err := gogit.BuildTag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) + // Build a git.Tag using build.Tag + gitTag, err := build.Tag(tagObj, plumbing.ReferenceName("refs/tags/test-tag")) if err != nil { t.Fatalf("Failed to build tag: %v", err) }