mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-08-19 03:26:20 +00:00
Uses the `sigstore-go` library for fetching the `TrustedRoot`, which contains the Sigstore infrastructure certificates needed to validate the leaf ephemeral certificates used to sign artifacts. Refactors: - replace `TrustedRootSingleton()` with `getDefaultCosignCheckOpts()`, since only `VerifyImage()` will now need that data. - replace `cosign.ValidateAndUnpackCert` with`sigstoreVerify.VerifyLeafCertificate()` - use `sync.Once` for sigstore and rekor clients, and the `TrustedRoot` ## Testing - existing tests continue to pass - [negative tests ](https://github.com/slsa-framework/slsa-verifier/blob/d96b9777090694fa5096ee1b9c710a46b5a66f5e/cli/slsa-verifier/main_regression_test.go#L450-L471) against rekor TLogs - manual invocations of `verify-artifact`. --------- Signed-off-by: Ramon Petgrave <ramon.petgrave64@gmail.com>
543 lines
15 KiB
Go
543 lines
15 KiB
Go
package gha
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
|
sigstoreRoot "github.com/sigstore/sigstore-go/pkg/root"
|
|
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
|
"github.com/slsa-framework/slsa-verifier/v2/options"
|
|
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance"
|
|
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common"
|
|
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
|
|
)
|
|
|
|
type hosted string
|
|
|
|
const (
|
|
hostedSelf hosted = "self-hosted"
|
|
hostedGitHub hosted = "github-hosted"
|
|
|
|
publishAttestationV01 = "https://github.com/npm/attestation/tree/main/specs/publish/"
|
|
)
|
|
|
|
var intotoStatements = map[string]bool{
|
|
intoto.StatementInTotoV01: true,
|
|
"https://in-toto.io/Statement/v1": true,
|
|
}
|
|
var provenancePredicates = map[string]bool{
|
|
common.ProvenanceV02Type: true,
|
|
common.ProvenanceV1Type: true,
|
|
}
|
|
var publishPredicates = map[string]bool{
|
|
publishAttestationV01: true,
|
|
}
|
|
var errrorInvalidAttestations = errors.New("invalid npm attestations")
|
|
var attestationKeyAtomicValue atomic.Value
|
|
|
|
type attestationSet struct {
|
|
Attestations []attestation `json:"attestations"`
|
|
}
|
|
|
|
type attestation struct {
|
|
PredicateType string `json:"predicateType"`
|
|
BundleBytes BundleBytes `json:"bundle"`
|
|
}
|
|
|
|
type BundleBytes []byte
|
|
|
|
func (b *BundleBytes) UnmarshalJSON(data []byte) error {
|
|
*b = data
|
|
return nil
|
|
}
|
|
|
|
type Npm struct {
|
|
ctx context.Context
|
|
root *sigstoreRoot.LiveTrustedRoot
|
|
verifiedBuilderID *utils.TrustedBuilderID
|
|
verifiedProvenanceAtt *SignedAttestation
|
|
verifiedPublishAtt *SignedAttestation
|
|
provenanceAttestation *attestation
|
|
publishAttestation *attestation
|
|
}
|
|
|
|
func (n *Npm) ProvenanceEnvelope() *dsse.Envelope {
|
|
return n.verifiedProvenanceAtt.Envelope
|
|
}
|
|
|
|
func (n *Npm) ProvenanceLeafCertificate() *x509.Certificate {
|
|
return n.verifiedProvenanceAtt.SigningCert
|
|
}
|
|
|
|
func NpmNew(ctx context.Context, root *sigstoreRoot.LiveTrustedRoot, attestationBytes []byte) (*Npm, error) {
|
|
var aSet attestationSet
|
|
if err := json.Unmarshal(attestationBytes, &aSet); err != nil {
|
|
return nil, fmt.Errorf("%w: json.Unmarshal: %v", errrorInvalidAttestations, err)
|
|
}
|
|
|
|
prov, pub, err := extractAttestations(aSet.Attestations)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Npm{
|
|
ctx: ctx,
|
|
root: root,
|
|
|
|
provenanceAttestation: prov,
|
|
publishAttestation: pub,
|
|
}, nil
|
|
}
|
|
|
|
func extractAttestations(attestations []attestation) (*attestation, *attestation, error) {
|
|
if len(attestations) < 2 {
|
|
return nil, nil, fmt.Errorf("%w: invalid number of attestations: %v", errrorInvalidAttestations, len(attestations))
|
|
}
|
|
|
|
var provenanceAttestation *attestation
|
|
var publishAttestation *attestation
|
|
for i := range attestations {
|
|
att := attestations[i]
|
|
// Provenance type verification.
|
|
if _, ok := provenancePredicates[att.PredicateType]; ok {
|
|
provenanceAttestation = &att
|
|
}
|
|
// Publish type verification.
|
|
if strings.HasPrefix(att.PredicateType, publishAttestationV01) {
|
|
publishAttestation = &att
|
|
}
|
|
}
|
|
|
|
if provenanceAttestation == nil || publishAttestation == nil {
|
|
return nil, nil, fmt.Errorf("%w: invalid attestation types", errrorInvalidAttestations)
|
|
}
|
|
return provenanceAttestation, publishAttestation, nil
|
|
}
|
|
|
|
// getAttestationKey retrieves the attestation key and holds it in memory.
|
|
func getAttestationKey(npmRegistryPublicKeyID string) (string, error) {
|
|
value := attestationKeyAtomicValue.Load()
|
|
if value != nil {
|
|
return value.(string), nil
|
|
}
|
|
npmRegistryPublicKey, err := getKeyDataFromSigstoreTuf(npmRegistryPublicKeyID, attestationKeyUsage)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
attestationKeyAtomicValue.Store(npmRegistryPublicKey)
|
|
return npmRegistryPublicKey, nil
|
|
}
|
|
|
|
func (n *Npm) verifyProvenanceAttestationSignature() error {
|
|
// Re-use the standard bundle verification.
|
|
signedProvenance, err := VerifyProvenanceBundle(n.ctx, n.provenanceAttestation.BundleBytes, n.root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.verifiedProvenanceAtt = signedProvenance
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifyPublishAttestationSignature() error {
|
|
// First verify the bundle and its rekor entry.
|
|
signedPublish, err := verifyBundleAndEntryFromBytes(n.ctx, n.publishAttestation.BundleBytes, n.root, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the keyID used to sign the attestaion.
|
|
// It is untrusted until we search the TUF root and find and key with the same KeyID.
|
|
npmRegistryPublicKeyID := signedPublish.PublicKey.Hint
|
|
|
|
// Retrieve the key material.
|
|
// We found the associated public key in the TUF root, so now we can trust this KeyID.
|
|
npmRegistryPublicKey, err := getAttestationKey(npmRegistryPublicKeyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify the PAE signature.
|
|
derKey, err := base64.StdEncoding.DecodeString(npmRegistryPublicKey)
|
|
if err != nil {
|
|
return fmt.Errorf("DecodeString: %w", err)
|
|
}
|
|
|
|
envVerifier, err := utils.DsseVerifierNew(derKey, utils.KeyFormatDER, npmRegistryPublicKeyID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = envVerifier.Verify(context.Background(), signedPublish.Envelope)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", serrors.ErrorInvalidSignature, err)
|
|
}
|
|
|
|
// Verification done.
|
|
n.verifiedPublishAtt = signedPublish
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifyIntotoHeaders() error {
|
|
if err := verifyIntotoTypes(n.verifiedProvenanceAtt, provenancePredicates, intoto.PayloadType, false); err != nil {
|
|
return err
|
|
}
|
|
if err := verifyIntotoTypes(n.verifiedPublishAtt, publishPredicates, intoto.PayloadType, true); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyIntotoTypes(att *SignedAttestation, predicateTypes map[string]bool, payloadType string, prefix bool) error {
|
|
env := att.Envelope
|
|
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error())
|
|
}
|
|
|
|
var statement intoto.Statement
|
|
if err := json.Unmarshal(pyld, &statement); err != nil {
|
|
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error())
|
|
}
|
|
|
|
// Envelope verification.
|
|
if env.PayloadType != payloadType {
|
|
return fmt.Errorf("%w: expected payload type '%v', got '%s'",
|
|
serrors.ErrorInvalidDssePayload, payloadType, env.PayloadType)
|
|
}
|
|
|
|
// Statement verification.
|
|
if _, exists := intotoStatements[statement.Type]; !exists {
|
|
return fmt.Errorf("%w: expected statement header type one of '%v', got '%s'",
|
|
serrors.ErrorInvalidDssePayload, intotoStatements, statement.Type)
|
|
}
|
|
|
|
if !prefix {
|
|
if _, exists := predicateTypes[statement.PredicateType]; !exists {
|
|
return fmt.Errorf("%w: expected predicate type one of '%v', got '%s'", serrors.ErrorInvalidDssePayload, predicateTypes, statement.PredicateType)
|
|
}
|
|
}
|
|
|
|
if prefix {
|
|
hasPrefix := false
|
|
for k := range predicateTypes {
|
|
if strings.HasPrefix(statement.PredicateType, k) {
|
|
hasPrefix = true
|
|
break
|
|
}
|
|
}
|
|
if !hasPrefix {
|
|
return fmt.Errorf("%w: expected predicate type with prefix one of '%v', got '%s'",
|
|
serrors.ErrorInvalidDssePayload, predicateTypes, statement.PredicateType)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifiedProvenanceBytes() ([]byte, error) {
|
|
// TODO(#493): prune the provenance and return only
|
|
// verified fields.
|
|
// NOTE: we currently don't verify the materials' commit sha.
|
|
return []byte{}, nil
|
|
}
|
|
|
|
func (n *Npm) verifyPackageName(name *string) error {
|
|
if name == nil {
|
|
return nil
|
|
}
|
|
|
|
// Verify subject name in provenance.
|
|
if err := verifyProvenanceSubjectName(n.verifiedBuilderID, n.verifiedProvenanceAtt, *name); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify subject name in publish attestation.
|
|
if err := verifyPublishSubjectName(n.verifiedPublishAtt, *name); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify predicate name in publish attestation.
|
|
if err := verifyPublishPredicateName(n.verifiedPublishAtt, *name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifyPackageVersion(version *string) error {
|
|
if version == nil {
|
|
return nil
|
|
}
|
|
|
|
// Verify subject version in provenance.
|
|
if err := verifyProvenanceSubjectVersion(n.verifiedBuilderID, n.verifiedProvenanceAtt, *version); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify subject version in publish attestation.
|
|
if err := verifyPublishSubjectVersion(n.verifiedPublishAtt, *version); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify predicate version in publish attestation.
|
|
if err := verifyPublishPredicateVersion(n.verifiedPublishAtt, *version); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifyBuilderID(
|
|
provenanceOpts *options.ProvenanceOpts,
|
|
builderOpts *options.BuilderOpts,
|
|
defaultBuilders map[string]bool,
|
|
) (*utils.TrustedBuilderID, error) {
|
|
// Verify certificate information.
|
|
builder, err := verifyNpmEnvAndCert(
|
|
n.ProvenanceEnvelope(),
|
|
n.ProvenanceLeafCertificate(),
|
|
provenanceOpts, builderOpts,
|
|
defaultBuilders,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n.verifiedBuilderID = builder
|
|
return builder, err
|
|
}
|
|
|
|
func verifyPublishPredicateVersion(att *SignedAttestation, expectedVersion string) error {
|
|
_, version, err := publishPredicateData(att)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if version != expectedVersion {
|
|
return fmt.Errorf("%w: got '%v', expected '%v'", serrors.ErrorMismatchPackageVersion,
|
|
version, expectedVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyPublishPredicateName(att *SignedAttestation, expectedName string) error {
|
|
name, _, err := publishPredicateData(att)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if name != expectedName {
|
|
return fmt.Errorf("%w: got '%v', expected '%v'", serrors.ErrorMismatchPackageName,
|
|
name, expectedName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func subjectsFromAttestation(att *SignedAttestation) ([]intoto.Subject, error) {
|
|
env := att.Envelope
|
|
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
|
|
}
|
|
statement := struct {
|
|
intoto.StatementHeader
|
|
}{}
|
|
if err := json.Unmarshal(pyld, &statement); err != nil {
|
|
return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
|
|
}
|
|
|
|
if len(statement.Subject) == 0 {
|
|
return nil, fmt.Errorf("%w: no subjects", serrors.ErrorInvalidDssePayload)
|
|
}
|
|
return statement.Subject, nil
|
|
}
|
|
|
|
func publishPredicateData(att *SignedAttestation) (string, string, error) {
|
|
env := att.Envelope
|
|
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error())
|
|
}
|
|
|
|
statement := struct {
|
|
intoto.StatementHeader
|
|
Predicate struct {
|
|
Version string `json:"version"`
|
|
Name string `json:"name"`
|
|
} `json:"predicate"`
|
|
}{}
|
|
if err := json.Unmarshal(pyld, &statement); err != nil {
|
|
return "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error())
|
|
}
|
|
|
|
return statement.Predicate.Name, statement.Predicate.Version, nil
|
|
}
|
|
|
|
func verifyProvenanceSubjectVersion(b *utils.TrustedBuilderID, att *SignedAttestation, expectedVersion string) error {
|
|
subject, err := getSubject(b, att)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, subVersion, err := getPackageNameAndVersion(subject)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if subVersion != expectedVersion {
|
|
return fmt.Errorf("%w: got '%v', expected '%v'", serrors.ErrorMismatchPackageVersion,
|
|
subVersion, expectedVersion)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Npm) verifyPublishAttestationSubjectDigest(expectedHash string) error {
|
|
publishSubjects, err := subjectsFromAttestation(n.verifiedPublishAtt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 8 bit represented in hex, so 8/2=4.
|
|
bitLength := len(expectedHash) * 4
|
|
expectedAlgo := fmt.Sprintf("sha%v", bitLength)
|
|
if bitLength < 256 {
|
|
return fmt.Errorf("%w: expected minimum sha256, got %s", serrors.ErrorInvalidHash, expectedAlgo)
|
|
}
|
|
|
|
for _, subject := range publishSubjects {
|
|
digestSet := subject.Digest
|
|
hash, exists := digestSet[expectedAlgo]
|
|
if !exists {
|
|
continue
|
|
}
|
|
if hash == expectedHash {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NOTE: We don't need to verify that the digest matches the one in the provenance
|
|
// because the provenance verification will verify the hash as well.
|
|
return fmt.Errorf("expected hash '%s' not found: %w", expectedHash, serrors.ErrorMismatchHash)
|
|
}
|
|
|
|
func verifyPublishSubjectVersion(att *SignedAttestation, expectedVersion string) error {
|
|
_, version, err := publishPredicateData(att)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if version != expectedVersion {
|
|
return fmt.Errorf("%w: got '%v', expected '%v'", serrors.ErrorMismatchPackageVersion,
|
|
version, expectedVersion)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func verifyPublishSubjectName(att *SignedAttestation, expectedName string) error {
|
|
name, _, err := publishPredicateData(att)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return verifyName(name, expectedName)
|
|
}
|
|
|
|
func verifyProvenanceSubjectName(b *utils.TrustedBuilderID, att *SignedAttestation, expectedName string) error {
|
|
prov, err := slsaprovenance.ProvenanceFromEnvelope(b.Name(), att.Envelope)
|
|
if err != nil {
|
|
return fmt.Errorf("reading provenance: %w", err)
|
|
}
|
|
|
|
subjects, err := prov.Subjects()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
|
|
}
|
|
if len(subjects) != 1 {
|
|
return fmt.Errorf("%w: expected 1 subject, got %v", serrors.ErrorInvalidDssePayload, len(subjects))
|
|
}
|
|
|
|
// Package name starts with a prefix.
|
|
prefix := "pkg:npm/"
|
|
if !strings.HasPrefix(subjects[0].Name, prefix) {
|
|
return fmt.Errorf("%w: %s", serrors.ErrorInvalidPackageName, subjects[0].Name)
|
|
}
|
|
|
|
// URL decode the package name from the attestation.
|
|
subjectName, err := url.QueryUnescape(subjects[0].Name[len(prefix):])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", serrors.ErrorInvalidEncoding, err)
|
|
}
|
|
|
|
return verifyName(subjectName, expectedName)
|
|
}
|
|
|
|
func verifyName(actual, expected string) error {
|
|
subName, _, err := getPackageNameAndVersion(actual)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if subName != expected {
|
|
return fmt.Errorf("%w: got '%v', expected '%v'", serrors.ErrorMismatchPackageName,
|
|
subName, expected)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getPackageNameAndVersion(name string) (string, string, error) {
|
|
var pkgname, pkgtag string
|
|
n := name
|
|
if strings.HasPrefix(name, "@") {
|
|
n = n[1:]
|
|
}
|
|
parts := strings.Split(n, "@")
|
|
if len(parts) > 2 {
|
|
return "", "", fmt.Errorf("%w: %v", serrors.ErrorInvalidPackageName, name)
|
|
}
|
|
|
|
pkgname = parts[0]
|
|
if strings.HasPrefix(name, "@") {
|
|
pkgname = "@" + pkgname
|
|
}
|
|
if len(parts) == 2 {
|
|
pkgtag = parts[1]
|
|
}
|
|
|
|
return pkgname, pkgtag, nil
|
|
}
|
|
|
|
func getSubject(b *utils.TrustedBuilderID, att *SignedAttestation) (string, error) {
|
|
prov, err := slsaprovenance.ProvenanceFromEnvelope(b.Name(), att.Envelope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
subjects, err := prov.Subjects()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
if len(subjects) != 1 {
|
|
return "", fmt.Errorf("%w: subject length: %v", serrors.ErrorInvalidSubject, len(subjects))
|
|
}
|
|
|
|
// Package name starts with a prefix.
|
|
prefix := "pkg:npm/"
|
|
if !strings.HasPrefix(subjects[0].Name, prefix) {
|
|
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidPackageName, subjects[0].Name)
|
|
}
|
|
|
|
// URL decode the package name from the attestation.
|
|
subject, err := url.QueryUnescape(subjects[0].Name[len(prefix):])
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidEncoding, err)
|
|
}
|
|
return subject, err
|
|
}
|