mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-03 19:27:07 +00:00
262 lines
7.8 KiB
Go
262 lines
7.8 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/gittuf/gittuf/internal/signerverifier/common"
|
|
"github.com/gittuf/gittuf/internal/signerverifier/sigstore"
|
|
sslibsvssh "github.com/gittuf/gittuf/internal/signerverifier/ssh"
|
|
"github.com/hiddeco/sshsig"
|
|
"github.com/secure-systems-lab/go-securesystemslib/signerverifier"
|
|
"github.com/sigstore/cosign/v2/pkg/cosign"
|
|
gitsignVerifier "github.com/sigstore/gitsign/pkg/git"
|
|
gitsignRekor "github.com/sigstore/gitsign/pkg/rekor"
|
|
"github.com/sigstore/sigstore/pkg/fulcioroots"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const (
|
|
rekorPublicGoodInstance = "https://rekor.sigstore.dev"
|
|
namespaceSSHSignature string = "git"
|
|
gpgPrivateKeyPEMHeader string = "PGP PRIVATE KEY"
|
|
opensshPrivateKeyPEMHeader string = "OPENSSH PRIVATE KEY"
|
|
rsaPrivateKeyPEMHeader string = "RSA PRIVATE KEY"
|
|
genericPrivateKeyPEMHeader string = "PRIVATE KEY"
|
|
signingFormatGPG string = "gpg"
|
|
signingFormatSSH string = "ssh"
|
|
)
|
|
|
|
var (
|
|
ErrNotCommitOrTag = errors.New("invalid object type, expected commit or tag for signature verification")
|
|
ErrSigningKeyNotSpecified = errors.New("signing key not specified in git config")
|
|
ErrUnknownSigningMethod = errors.New("unknown signing method (not one of gpg, ssh, x509)")
|
|
ErrIncorrectVerificationKey = errors.New("incorrect key provided to verify signature")
|
|
ErrVerifyingSigstoreSignature = errors.New("unable to verify Sigstore signature")
|
|
ErrVerifyingSSHSignature = errors.New("unable to verify SSH signature")
|
|
ErrInvalidSignature = errors.New("unable to parse signature / signature has unexpected header")
|
|
)
|
|
|
|
// CanSign inspects the Git configuration to determine if commit / tag signing
|
|
// is possible.
|
|
func (r *Repository) CanSign() error {
|
|
config, err := r.GetGitConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Format is one of GPG, SSH, X509
|
|
format := getSigningMethod(config)
|
|
|
|
// If format is GPG or X509, the signing key parameter is optional
|
|
// However, for SSH, the signing key must be set
|
|
if format == signingFormatSSH {
|
|
keyInfo := getSigningKeyInfo(config)
|
|
if keyInfo == "" {
|
|
return ErrSigningKeyNotSpecified
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifySignature verifies the cryptographic signature associated with the
|
|
// specified object. The `objectID` must point to a Git commit or tag object.
|
|
func (r *Repository) VerifySignature(ctx context.Context, objectID Hash, key *signerverifier.SSLibKey) error {
|
|
if err := r.ensureIsCommit(objectID); err == nil {
|
|
return r.verifyCommitSignature(ctx, objectID, key)
|
|
}
|
|
|
|
if err := r.ensureIsTag(objectID); err == nil {
|
|
return r.verifyTagSignature(ctx, objectID, key)
|
|
}
|
|
|
|
return ErrNotCommitOrTag
|
|
}
|
|
|
|
func signGitObjectUsingKey(contents, pemKeyBytes []byte) (string, error) {
|
|
block, _ := pem.Decode(pemKeyBytes)
|
|
if block == nil {
|
|
// openpgp implements its own armor-decode method, pem.Decode considers
|
|
// the input invalid. We haven't tested if this is universal, so in case
|
|
// pem.Decode does succeed on a GPG key, we catch it below.
|
|
return signGitObjectUsingGPGKey(contents, pemKeyBytes)
|
|
}
|
|
|
|
switch block.Type {
|
|
case gpgPrivateKeyPEMHeader:
|
|
return signGitObjectUsingGPGKey(contents, pemKeyBytes)
|
|
case opensshPrivateKeyPEMHeader, rsaPrivateKeyPEMHeader, genericPrivateKeyPEMHeader:
|
|
return signGitObjectUsingSSHKey(contents, pemKeyBytes)
|
|
}
|
|
|
|
return "", ErrUnknownSigningMethod
|
|
}
|
|
|
|
func signGitObjectUsingGPGKey(contents, pemKeyBytes []byte) (string, error) {
|
|
reader := bytes.NewReader(contents)
|
|
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pemKeyBytes))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sig := new(strings.Builder)
|
|
if err := openpgp.ArmoredDetachSign(sig, keyring[0], reader, nil); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return sig.String(), nil
|
|
}
|
|
|
|
func signGitObjectUsingSSHKey(contents, pemKeyBytes []byte) (string, error) {
|
|
signer, err := ssh.ParsePrivateKey(pemKeyBytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sshSig, err := sshsig.Sign(bytes.NewReader(contents), signer, sshsig.HashSHA512, namespaceSSHSignature)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sigBytes := sshsig.Armor(sshSig)
|
|
|
|
return string(sigBytes), nil
|
|
}
|
|
|
|
// verifyGitsignSignature handles the Sigstore-specific workflow involved in
|
|
// verifying commit or tag signatures issued by gitsign.
|
|
func verifyGitsignSignature(ctx context.Context, repo *Repository, key *signerverifier.SSLibKey, data, signature []byte) error {
|
|
checkOpts := &cosign.CheckOpts{
|
|
Identities: []cosign.Identity{{
|
|
Issuer: key.KeyVal.Issuer,
|
|
Subject: key.KeyVal.Identity,
|
|
}},
|
|
}
|
|
|
|
var verifier *gitsignVerifier.CertVerifier
|
|
sigstoreRootFilePath := os.Getenv(sigstore.EnvSigstoreRootFile)
|
|
if sigstoreRootFilePath == "" {
|
|
root, err := fulcioroots.Get()
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
intermediate, err := fulcioroots.GetIntermediates()
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
|
|
checkOpts.RootCerts = root
|
|
checkOpts.IntermediateCerts = intermediate
|
|
|
|
verifier, err = gitsignVerifier.NewCertVerifier(
|
|
gitsignVerifier.WithRootPool(root),
|
|
gitsignVerifier.WithIntermediatePool(intermediate),
|
|
)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
} else {
|
|
slog.Debug("Using environment variables to establish trust for Sigstore instance...")
|
|
rootCerts, err := common.LoadCertsFromPath(sigstoreRootFilePath)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
root := x509.NewCertPool()
|
|
for _, cert := range rootCerts {
|
|
root.AddCert(cert)
|
|
}
|
|
|
|
checkOpts.RootCerts = root
|
|
|
|
verifier, err = gitsignVerifier.NewCertVerifier(
|
|
gitsignVerifier.WithRootPool(root),
|
|
)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
}
|
|
|
|
verifiedCert, err := verifier.Verify(ctx, data, signature, true)
|
|
if err != nil {
|
|
return ErrIncorrectVerificationKey
|
|
}
|
|
|
|
rekorURL := rekorPublicGoodInstance
|
|
// Check git config to see if rekor server must be overridden
|
|
config, err := repo.GetGitConfig()
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
if configValue, has := config[sigstore.GitConfigRekor]; has {
|
|
slog.Debug(fmt.Sprintf("Using '%s' as Rekor instance...", configValue))
|
|
rekorURL = configValue
|
|
}
|
|
|
|
// gitsignRekor.NewWithOptions invokes cosign.GetRekorPubs which looks at
|
|
// the env var, so we don't have to do anything here
|
|
rekor, err := gitsignRekor.NewWithOptions(ctx, rekorURL)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
|
|
checkOpts.RekorClient = rekor.Rekor
|
|
checkOpts.RekorPubKeys = rekor.PublicKeys()
|
|
|
|
// cosign.GetCTLogPubs already looks at the env var, so we don't have to do
|
|
// anything here
|
|
ctPub, err := cosign.GetCTLogPubs(ctx)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSigstoreSignature, err)
|
|
}
|
|
|
|
checkOpts.CTLogPubKeys = ctPub
|
|
|
|
if _, err := cosign.ValidateAndUnpackCert(verifiedCert, checkOpts); err != nil {
|
|
return errors.Join(ErrIncorrectVerificationKey, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifySSHKeySignature verifies Git signatures issued by SSH keys.
|
|
func verifySSHKeySignature(ctx context.Context, key *signerverifier.SSLibKey, data, signature []byte) error {
|
|
verifier, err := sslibsvssh.NewVerifierFromKey(key)
|
|
if err != nil {
|
|
return errors.Join(ErrVerifyingSSHSignature, err)
|
|
}
|
|
|
|
if err := verifier.Verify(ctx, data, signature); err != nil {
|
|
return errors.Join(ErrVerifyingSSHSignature, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getSigningMethod(gitConfig map[string]string) string {
|
|
format, ok := gitConfig["gpg.format"]
|
|
if !ok {
|
|
return signingFormatGPG // default to gpg
|
|
}
|
|
return format
|
|
}
|
|
|
|
func getSigningKeyInfo(gitConfig map[string]string) string {
|
|
keyInfo, ok := gitConfig["user.signingkey"]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return keyInfo
|
|
}
|