mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-02 02:37:05 +00:00
Signed-off-by: Pat Zielinski <git@patzielinski.com> Signed-off-by: PratGit1606 <prathamgithub16@gmail.com>
180 lines
5.7 KiB
Go
180 lines
5.7 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/gittuf/gittuf/pkg/gitstore"
|
|
"github.com/go-git/go-git/v6/plumbing"
|
|
"github.com/hiddeco/sshsig" //nolint:staticcheck
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const (
|
|
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 covers the signing side, in
|
|
// signGitObjectUsingKey. Verification reports
|
|
// gitobject.ErrUnknownSigningMethod instead.
|
|
ErrUnknownSigningMethod = errors.New("unknown signing method (not one of gpg, ssh, x509)")
|
|
)
|
|
|
|
// CanSign inspects the Git configuration to determine if commit / tag signing
|
|
// is possible.
|
|
func (r *Repository) CanSign() error {
|
|
// Format is one of GPG, SSH, X509. GPG is the default when unset.
|
|
format, ok, err := r.LookupConfig(gitstore.ConfigGPGFormat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
format = signingFormatGPG
|
|
}
|
|
|
|
// 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, ok, err := r.LookupConfig(gitstore.ConfigUserSigningKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok || keyInfo == "" {
|
|
return ErrSigningKeyNotSpecified
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetObjectSignature returns the signed payload and the detached signature for
|
|
// the specified Git object. The `objectID` must point to a commit or tag
|
|
// object. An unsigned object returns an empty signature and no error, and
|
|
// verification layers decide how to treat it. For commits the signature is
|
|
// read from the header matching the repository's object format (`gpgsig` or
|
|
// `gpgsig-sha256`). For tags it is the block Git appends to the payload.
|
|
// Signatures containing multiple armored blocks are returned verbatim, and
|
|
// callers performing verification must reject them.
|
|
func (r *Repository) GetObjectSignature(objectID Hash) ([]byte, []byte, error) {
|
|
if err := r.ensureIsCommit(objectID); err == nil {
|
|
goGitRepo, err := r.GetGoGitRepository()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error opening repository: %w", err)
|
|
}
|
|
|
|
commit, err := goGitRepo.CommitObject(plumbing.NewHash(objectID.String()))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to load commit object: %w", err)
|
|
}
|
|
|
|
payload, err := getCommitBytesWithoutSignature(commit)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to encode commit contents: %w", err)
|
|
}
|
|
|
|
return payload, []byte(signatureForObjectID(objectID, commit.Signature, commit.SignatureSHA256)), nil
|
|
}
|
|
|
|
if err := r.ensureIsTag(objectID); err == nil {
|
|
goGitRepo, err := r.GetGoGitRepository()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error opening repository: %w", err)
|
|
}
|
|
|
|
tag, err := goGitRepo.TagObject(plumbing.NewHash(objectID.String()))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to load tag object: %w", err)
|
|
}
|
|
|
|
payload, err := getTagBytesWithoutSignature(tag)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to encode tag contents: %w", err)
|
|
}
|
|
|
|
// Git appends tag signatures to the tag payload regardless of the
|
|
// object format, so the signature is always in the Signature field.
|
|
return payload, []byte(tag.Signature), nil
|
|
}
|
|
|
|
return nil, nil, 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
|
|
}
|
|
|
|
// signatureForObjectID selects the signature stored for a Git commit based on
|
|
// its hash algorithm, identified by the OID length. SHA-256 commits store
|
|
// their signature under the `gpgsig-sha256` header (go-git's SignatureSHA256),
|
|
// SHA-1 commits under `gpgsig` (Signature). Tags are not covered here: Git
|
|
// appends tag signatures to the tag payload regardless of the object format.
|
|
func signatureForObjectID(objectID Hash, signature, signatureSHA256 string) string {
|
|
if objectID.IsSHA256() {
|
|
return signatureSHA256
|
|
}
|
|
return signature
|
|
}
|