gittuf/internal/gitinterface/signature.go
Aditya Sirish A Yelgundhalli a3fedc1524
sigstore: Add support for private sigstore instances
Signed-off-by: Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net>
2024-10-16 15:14:38 -04:00

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
}