mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-08-18 02:56:31 +00:00
508 lines
17 KiB
Go
508 lines
17 KiB
Go
package gha
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
fulcio "github.com/sigstore/fulcio/pkg/certificate"
|
|
|
|
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
|
"github.com/slsa-framework/slsa-verifier/v2/options"
|
|
ghacommon "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common"
|
|
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
|
|
)
|
|
|
|
var (
|
|
trustedBuilderRepository = "slsa-framework/slsa-github-generator"
|
|
e2eTestRepository = "slsa-framework/example-package"
|
|
jReleaserActionRepository = "jreleaser/release-action"
|
|
certOidcIssuer = "https://token.actions.githubusercontent.com"
|
|
githubCom = "github.com/"
|
|
httpsGithubCom = "https://" + githubCom
|
|
// This is used in sigstore-go's NewShortCertificateIdentity for validating the certificate. We
|
|
// do specific builder verification after this.
|
|
certSubjectRegexp = httpsGithubCom + "*"
|
|
)
|
|
|
|
var defaultArtifactTrustedReusableWorkflows = map[string]bool{
|
|
ghacommon.GenericGeneratorBuilderID: true,
|
|
ghacommon.GoBuilderID: true,
|
|
ghacommon.ContainerBasedBuilderID: true,
|
|
}
|
|
|
|
var defaultContainerTrustedReusableWorkflows = map[string]bool{
|
|
ghacommon.ContainerGeneratorBuilderID: true,
|
|
}
|
|
|
|
var defaultBYOBReusableWorkflows = map[string]bool{
|
|
ghacommon.GenericDelegatorBuilderID: true,
|
|
ghacommon.GenericLowPermsDelegatorBuilderID: true,
|
|
}
|
|
|
|
var JReleaserRepository = httpsGithubCom + jReleaserActionRepository
|
|
|
|
// VerifyCertficateSourceRepository verifies the source repository.
|
|
func VerifyCertficateSourceRepository(id *WorkflowIdentity,
|
|
sourceRepo string,
|
|
) error {
|
|
// The caller repository in the x509 extension is not fully qualified. It only contains
|
|
// {org}/{repository}.
|
|
expectedSource := strings.TrimPrefix(sourceRepo, "git+https://")
|
|
expectedSource = strings.TrimPrefix(expectedSource, githubCom)
|
|
if id.SourceRepository != expectedSource {
|
|
return fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
|
|
expectedSource, id.SourceRepository)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyBuilderIdentity verifies the signing certificate information.
|
|
// Builder IDs are verified against an expected builder ID provided in the
|
|
// builerOpts, or against the set of defaultBuilders provided. The identiy
|
|
// in the certificate corresponds to a GitHub workflow's path.
|
|
func VerifyBuilderIdentity(id *WorkflowIdentity,
|
|
builderOpts *options.BuilderOpts,
|
|
defaultBuilders map[string]bool,
|
|
) (*utils.TrustedBuilderID, bool, error) {
|
|
// Issuer verification.
|
|
// NOTE: this is necessary before we do any further verification.
|
|
if id.Issuer != certOidcIssuer {
|
|
return nil, false, fmt.Errorf("%w: %q", serrors.ErrorInvalidOIDCIssuer, id.Issuer)
|
|
}
|
|
|
|
// cert URI is https://github.com/org/repo/path/to/workflow@ref
|
|
// Remove '@' from Path
|
|
workflowID := id.SubjectWorkflowName()
|
|
workflowTag := id.SubjectWorkflowRef()
|
|
|
|
if workflowID == "" || workflowTag == "" {
|
|
return nil, false, fmt.Errorf("%w: workflow uri: %q", serrors.ErrorMalformedURI, id.SubjectWorkflow.String())
|
|
}
|
|
|
|
// Verify trusted workflow.
|
|
builderID, byob, err := verifyTrustedBuilderID(workflowID, workflowTag,
|
|
builderOpts.ExpectedID, defaultBuilders)
|
|
if err != nil {
|
|
return nil, byob, err
|
|
}
|
|
|
|
// Verify the ref is a full semantic version tag.
|
|
if err := verifyTrustedBuilderRef(id, workflowTag); err != nil {
|
|
return nil, byob, err
|
|
}
|
|
|
|
return builderID, byob, nil
|
|
}
|
|
|
|
// Verifies the builder ID at path against an expected builderID.
|
|
// If an expected builderID is not provided, uses the defaultBuilders.
|
|
func verifyTrustedBuilderID(certBuilderID, certTag string, expectedBuilderID *string, defaultTrustedBuilders map[string]bool) (*utils.TrustedBuilderID, bool, error) {
|
|
var trustedBuilderID *utils.TrustedBuilderID
|
|
var err error
|
|
// WARNING: we don't validate the tag here, because we need to allow
|
|
// refs/heads/main for e2e tests. See verifyTrustedBuilderRef().
|
|
// No builder ID provided by user: use the default trusted workflows.
|
|
if expectedBuilderID == nil || *expectedBuilderID == "" {
|
|
if _, ok := defaultTrustedBuilders[certBuilderID]; !ok {
|
|
return nil, false, fmt.Errorf("%w: %s with builderID provided: %t", serrors.ErrorUntrustedReusableWorkflow, certBuilderID, expectedBuilderID != nil)
|
|
}
|
|
|
|
// Construct the builderID using the certificate's builder's name and tag.
|
|
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if:
|
|
// - the builder in the cert is a BYOB builder
|
|
// - the caller trusts the BYOB builder
|
|
// If both are true, we don't match the user-provided builder ID
|
|
// against the certificate. Instead that will be done by the caller.
|
|
//
|
|
// This return of the delegator builderID enables non-compulsory
|
|
// builderID feature for BYOB builders by setting byob flag to true.
|
|
return trustedBuilderID, isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders), nil
|
|
}
|
|
|
|
// Verify the builderID.
|
|
// We only accept IDs on github.com.
|
|
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if:
|
|
// - the builder in the cert is a BYOB builder
|
|
// - the caller trusts the BYOB builder
|
|
// If both are true, we don't match the user-provided builder ID
|
|
// against the certificate. Instead that will be done by the caller.
|
|
if isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders) {
|
|
return trustedBuilderID, true, nil
|
|
}
|
|
|
|
// Not a BYOB builder. BuilderID provided by user should match the certificate.
|
|
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
|
|
// so we pass `allowRef = true`.
|
|
if err := trustedBuilderID.MatchesLoose(*expectedBuilderID, true); err != nil {
|
|
return nil, false, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
|
|
}
|
|
|
|
return trustedBuilderID, false, nil
|
|
}
|
|
|
|
func isTrustedDelegatorBuilder(certBuilder *utils.TrustedBuilderID, trustedBuilders map[string]bool) bool {
|
|
for byobBuilder := range defaultBYOBReusableWorkflows {
|
|
// Check that the certificate builder is a BYOB workflow.
|
|
if err := certBuilder.MatchesLoose(byobBuilder, true); err == nil {
|
|
// We found a delegator workflow that matches the certificate identity.
|
|
// Check that the BYOB builder is trusted by the caller.
|
|
if _, ok := trustedBuilders[byobBuilder]; !ok {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
|
|
// This lets us use the pre-build builder binary generated during release (release happen at main).
|
|
// For other projects, we only allow semantic versions that map to a release.
|
|
func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
|
|
if (id.SourceRepository == trustedBuilderRepository ||
|
|
id.SourceRepository == e2eTestRepository) &&
|
|
options.TestingEnabled() {
|
|
// // Allow verification on the main branch to support e2e tests.
|
|
if ref == "refs/heads/main" || ref == "refs/heads/ramonpetgrave64-internal-builder-sigstore-bundle" {
|
|
return nil
|
|
}
|
|
|
|
return utils.IsValidBuilderTag(ref, true)
|
|
}
|
|
|
|
return utils.IsValidBuilderTag(ref, false)
|
|
}
|
|
|
|
func getExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier, encoded bool) (string, error) {
|
|
for _, ext := range cert.Extensions {
|
|
if !ext.Id.Equal(oid) {
|
|
continue
|
|
}
|
|
if !encoded {
|
|
return string(ext.Value), nil
|
|
}
|
|
|
|
// Decode first.
|
|
var decoded string
|
|
rest, err := asn1.Unmarshal(ext.Value, &decoded)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w", err)
|
|
}
|
|
if len(rest) != 0 {
|
|
return "", fmt.Errorf("decoding has rest for oid %v", oid)
|
|
}
|
|
return decoded, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
type Hosted int
|
|
|
|
const (
|
|
HostedSelf Hosted = iota
|
|
HostedGitHub
|
|
)
|
|
|
|
// WorkflowIdentity is a identity captured from a Fulcio certificate.
|
|
// See https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md.
|
|
type WorkflowIdentity struct {
|
|
// The source repository
|
|
SourceRepository string
|
|
// The commit SHA where the workflow was triggered.
|
|
SourceSha1 string
|
|
// Ref of the source.
|
|
SourceRef *string
|
|
// ID of the source repository.
|
|
SourceID *string
|
|
// Source owner ID of repository.
|
|
SourceOwnerID *string
|
|
|
|
// Workflow path OIDC subject - ref of reuseable workflow or trigger workflow.
|
|
SubjectWorkflow *url.URL
|
|
// Subject commit sha1.
|
|
SubjectSha1 *string
|
|
// Hosted status of the subject.
|
|
SubjectHosted *Hosted
|
|
|
|
// BuildTrigger
|
|
BuildTrigger string
|
|
// Build config path, i.e. the trigger workflow.
|
|
BuildConfigPath *string
|
|
|
|
// Run ID
|
|
RunID *string
|
|
// Issuer
|
|
Issuer string
|
|
}
|
|
|
|
// SubjectWorkflowName returns the subject workflow without the git ref.
|
|
func (id *WorkflowIdentity) SubjectWorkflowName() string {
|
|
// NOTE: You should be able to copy a net.URL struct safely.
|
|
// See: https://github.com/golang/go/issues/38351
|
|
withoutRef := *id.SubjectWorkflow
|
|
withoutRef.Path = id.SubjectWorkflowPath()
|
|
return withoutRef.String()
|
|
}
|
|
|
|
// SubjectWorkflowPath returns the subject workflow without the server url.
|
|
func (id *WorkflowIdentity) SubjectWorkflowPath() string {
|
|
i := strings.LastIndex(id.SubjectWorkflow.Path, "@")
|
|
if i == -1 {
|
|
return id.SubjectWorkflow.Path
|
|
}
|
|
return id.SubjectWorkflow.Path[:i]
|
|
}
|
|
|
|
// SubjectWorkflowRef returns the ref for the subject workflow.
|
|
func (id *WorkflowIdentity) SubjectWorkflowRef() string {
|
|
i := strings.LastIndex(id.SubjectWorkflow.Path, "@")
|
|
if i == -1 {
|
|
return ""
|
|
}
|
|
return id.SubjectWorkflow.Path[i+1:]
|
|
}
|
|
|
|
func getHosted(cert *x509.Certificate) (*Hosted, error) {
|
|
runnerEnv, err := getExtension(cert, fulcio.OIDRunnerEnvironment, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if runnerEnv == "github-hosted" {
|
|
r := HostedGitHub
|
|
return &r, nil
|
|
}
|
|
if runnerEnv == "self-hosted" {
|
|
r := HostedSelf
|
|
return &r, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func validateClaimsEqual(deprecated, existing string) error {
|
|
if deprecated != "" && existing != "" && deprecated != existing {
|
|
return fmt.Errorf("%w: '%v' != '%v'", serrors.ErrorInvalidFormat, deprecated, existing)
|
|
}
|
|
if deprecated == "" && existing == "" {
|
|
return fmt.Errorf("%w: claims are empty", serrors.ErrorInvalidFormat)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getAndValidateEqualClaims(cert *x509.Certificate, deprecatedOid, oid asn1.ObjectIdentifier) (string, error) {
|
|
deprecatedValue, err := getExtension(cert, deprecatedOid, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
value, err := getExtension(cert, oid, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := validateClaimsEqual(deprecatedValue, value); err != nil {
|
|
return "", err
|
|
}
|
|
// New certificates.
|
|
if value != "" {
|
|
return value, nil
|
|
}
|
|
// Old certificates.
|
|
if deprecatedValue != "" {
|
|
return deprecatedValue, nil
|
|
}
|
|
// Both values are empty.
|
|
return "", fmt.Errorf("%w: empty fields %v and %v", serrors.ErrorInvalidCertificate,
|
|
deprecatedOid, oid)
|
|
}
|
|
|
|
// GetWorkflowFromCertificate gets the workflow identity from the Fulcio authenticated content.
|
|
// See https://github.com/sigstore/fulcio/blob/e763d76e3f7786b52db4b27ab87dc446da24895a/pkg/certificate/extensions.go.
|
|
// https://github.com/golangci/golangci-lint/issues/741#issuecomment-784171870.
|
|
//
|
|
//nolint:staticcheck // we want to disable SA1019 only to use deprecated methods but there is a bug in golangci-lint.
|
|
func GetWorkflowInfoFromCertificate(cert *x509.Certificate) (*WorkflowIdentity, error) {
|
|
if len(cert.URIs) == 0 {
|
|
return nil, fmt.Errorf("%w: missing URI information from certificate", serrors.ErrorInvalidFormat)
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.2: DEPRECATED.
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726412--github-workflow-BuildTrigger-deprecated
|
|
// 1.3.6.1.4.1.57264.1.20 | Build Trigger
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264120--build-trigger
|
|
buildTrigger, err := getAndValidateEqualClaims(cert, fulcio.OIDGitHubWorkflowTrigger, fulcio.OIDBuildTrigger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.3: DEPRECATED.
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726413--github-workflow-sha-deprecated
|
|
// 1.3.6.1.4.1.57264.1.13 | Source Repository Digest
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264113--source-repository-digest
|
|
sourceSha1, err := getAndValidateEqualClaims(cert, fulcio.OIDGitHubWorkflowSHA, fulcio.OIDSourceRepositoryDigest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 1.3.6.1.4.1.57264.1.19 | Build Config Digest
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264119--build-config-digest
|
|
buildConfigSha1, err := getExtension(cert, fulcio.OIDBuildConfigDigest, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateClaimsEqual(sourceSha1, buildConfigSha1); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// IssuerV1: 1.3.6.1.4.1.57264.1.1
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726411--issuer
|
|
// IssuerV2: 1.3.6.1.4.1.57264.1.8
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726418--issuer-v2
|
|
issuer, err := getAndValidateEqualClaims(cert, fulcio.OIDIssuer, fulcio.OIDIssuerV2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.5: DEPRECATED.
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726415--github-workflow-repository-deprecated
|
|
deprecatedSourceRepository, err := getExtension(cert, fulcio.OIDGitHubWorkflowRepository, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 1.3.6.1.4.1.57264.1.12 | Source Repository URI
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264112--source-repository-uri
|
|
sourceURI, err := getExtension(cert, fulcio.OIDSourceRepositoryURI, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deprecatedSourceRepository != "" && sourceURI != "" &&
|
|
httpsGithubCom+deprecatedSourceRepository != sourceURI {
|
|
return nil, fmt.Errorf("%w: '%v' != '%v'",
|
|
serrors.ErrorInvalidFormat, httpsGithubCom+deprecatedSourceRepository, sourceURI)
|
|
}
|
|
sourceRepository := strings.TrimPrefix(sourceURI, httpsGithubCom)
|
|
// Handle old certifcates.
|
|
if sourceRepository == "" {
|
|
sourceRepository = deprecatedSourceRepository
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.10 | Build Signer Digest
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264110--build-signer-digest
|
|
subjectSha1, err := getExtension(cert, fulcio.OIDBuildSignerDigest, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.11 | Runner Environment
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264111--runner-environment
|
|
subjectHosted, err := getHosted(cert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.14 | Source Repository Ref
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264114--source-repository-ref
|
|
sourceRef, err := getExtension(cert, fulcio.OIDSourceRepositoryRef, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.15 | Source Repository Identifier
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264115--source-repository-identifier
|
|
sourceID, err := getExtension(cert, fulcio.OIDSourceRepositoryIdentifier, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.17 | Source Repository Owner Identifier
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264117--source-repository-owner-identifier
|
|
sourceOwnerID, err := getExtension(cert, fulcio.OIDSourceRepositoryOwnerIdentifier, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.18 | Build Config URI
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264118--build-config-uri
|
|
var buildConfigPath string
|
|
buildConfigURI, err := getExtension(cert, fulcio.OIDBuildConfigURI, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if buildConfigURI != "" {
|
|
parts := strings.Split(buildConfigURI, "@")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("%w: %v",
|
|
serrors.ErrorInvalidFormat, buildConfigURI)
|
|
}
|
|
prefix := fmt.Sprintf("https://github.com/%v/", sourceRepository)
|
|
if !strings.HasPrefix(parts[0], prefix) {
|
|
return nil, fmt.Errorf("%w: prefix: %v",
|
|
serrors.ErrorInvalidFormat, parts[0])
|
|
}
|
|
buildConfigPath = strings.TrimPrefix(parts[0], prefix)
|
|
}
|
|
|
|
// 1.3.6.1.4.1.57264.1.21 | Run Invocation URI
|
|
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264121--run-invocation-uri
|
|
runURI, err := getExtension(cert, fulcio.OIDRunInvocationURI, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runID := strings.TrimPrefix(runURI, fmt.Sprintf("https://github.com/%s/actions/runs/", sourceRepository))
|
|
|
|
// Subject path.
|
|
if !strings.HasPrefix(cert.URIs[0].Path, "/") {
|
|
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidFormat, cert.URIs[0].Path)
|
|
}
|
|
subjectWorkflow := cert.URIs[0]
|
|
|
|
var pSubjectSha1, pSourceID, pSourceRef, pSourceOwnerID, pBuildConfigPath, pRunID *string
|
|
if subjectSha1 != "" {
|
|
pSubjectSha1 = &subjectSha1
|
|
}
|
|
if sourceID != "" {
|
|
pSourceID = &sourceID
|
|
}
|
|
if sourceRef != "" {
|
|
pSourceRef = &sourceRef
|
|
}
|
|
if sourceOwnerID != "" {
|
|
pSourceOwnerID = &sourceOwnerID
|
|
}
|
|
if buildConfigPath != "" {
|
|
pBuildConfigPath = &buildConfigPath
|
|
}
|
|
if runID != "" {
|
|
pRunID = &runID
|
|
}
|
|
|
|
return &WorkflowIdentity{
|
|
// Issuer.
|
|
Issuer: issuer,
|
|
// Subject
|
|
SubjectWorkflow: subjectWorkflow,
|
|
SubjectSha1: pSubjectSha1,
|
|
SubjectHosted: subjectHosted,
|
|
// Source.
|
|
SourceRepository: sourceRepository,
|
|
SourceSha1: sourceSha1,
|
|
SourceRef: pSourceRef,
|
|
SourceID: pSourceID,
|
|
SourceOwnerID: pSourceOwnerID,
|
|
// Build.
|
|
BuildTrigger: buildTrigger,
|
|
BuildConfigPath: pBuildConfigPath,
|
|
// Other.
|
|
RunID: pRunID,
|
|
}, nil
|
|
}
|