Merge remote-tracking branch 'upstream/trunk' into attestation-verify-ref-commit-policy-opts

This commit is contained in:
Meredith Lancaster 2025-01-24 09:32:18 -07:00
commit 6c0cdca554
11 changed files with 99 additions and 80 deletions

View file

@ -203,10 +203,8 @@ jobs:
platform="x64"
;;
*_arm64 )
echo "skipping building MSI for arm64 because WiX 3.11 doesn't support it: https://github.com/wixtoolset/issues/issues/6141" >&2
continue
#source_dir="$PWD/dist/windows_windows_arm64"
#platform="arm64"
source_dir="$PWD/dist/windows_windows_arm64"
platform="arm64"
;;
* )
printf "unsupported architecture: %s\n" "$MSI_NAME" >&2
@ -299,7 +297,7 @@ jobs:
rpmsign --addsign dist/*.rpm
- name: Attest release artifacts
if: inputs.environment == 'production'
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
with:
subject-path: "dist/gh_*"
- name: Run createrepo

View file

@ -2,8 +2,8 @@ package io
import (
"fmt"
"strings"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
)
@ -65,26 +65,24 @@ func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) {
if !h.debugEnabled || !h.IO.IsStdoutTTY() {
return 0, nil
}
return fmt.Fprintf(h.IO.ErrOut, f, v...)
}
func (h *Handler) PrintTable(headers []string, rows [][]string) error {
func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) {
if !h.IO.IsStdoutTTY() {
return nil
return 0, nil
}
t := tableprinter.New(h.IO, tableprinter.WithHeader(headers...))
maxColLen := 0
for _, row := range rows {
for _, field := range row {
t.AddField(field, tableprinter.WithTruncate(nil))
if len(row[0]) > maxColLen {
maxColLen = len(row[0])
}
t.EndRow()
}
if err := t.Render(); err != nil {
return fmt.Errorf("failed to print output: %v", err)
info := ""
for _, row := range rows {
dots := strings.Repeat(".", maxColLen-len(row[0]))
info += fmt.Sprintf("%s:%s %s\n", row[0], dots, row[1])
}
return nil
return fmt.Fprintln(h.IO.ErrOut, info)
}

View file

@ -67,7 +67,7 @@ func (v *FailSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder)
return nil, fmt.Errorf("failed to verify attestations")
}
func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult {
func BuildMockResult(b *bundle.Bundle, buildConfigURI, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult {
statement := &in_toto.Statement{}
statement.PredicateType = SLSAPredicateV1
@ -80,10 +80,11 @@ func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourc
Signature: &verify.SignatureVerificationResult{
Certificate: &certificate.Summary{
Extensions: certificate.Extensions{
BuildConfigURI: buildConfigURI,
BuildSignerURI: buildSignerURI,
Issuer: issuer,
SourceRepositoryOwnerURI: sourceRepoOwnerURI,
SourceRepositoryURI: sourceRepoURI,
Issuer: issuer,
},
},
},
@ -93,9 +94,10 @@ func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourc
func BuildSigstoreJsMockResult(t *testing.T) AttestationProcessingResult {
bundle := data.SigstoreBundle(t)
buildConfigURI := "https://github.com/sigstore/sigstore-js/.github/workflows/build.yml@refs/heads/main"
buildSignerURI := "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main"
sourceRepoOwnerURI := "https://github.com/sigstore"
sourceRepoURI := "https://github.com/sigstore/sigstore-js"
issuer := "https://token.actions.githubusercontent.com"
return BuildMockResult(bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer)
return BuildMockResult(bundle, buildConfigURI, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer)
}

View file

@ -54,10 +54,7 @@ func (c EnforcementCriteria) Valid() error {
func (c EnforcementCriteria) BuildPolicyInformation() string {
policyAttr := make([][]string, 0, 6)
policyAttr = appendStr(policyAttr, "- OIDC Issuer must match", c.Certificate.Issuer)
if c.Certificate.RunnerEnvironment == GitHubRunner {
policyAttr = appendStr(policyAttr, "- Action workflow Runner Environment must match ", GitHubRunner)
}
policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType)
policyAttr = appendStr(policyAttr, "- Source Repository Owner URI must match", c.Certificate.SourceRepositoryOwnerURI)
@ -65,14 +62,17 @@ func (c EnforcementCriteria) BuildPolicyInformation() string {
policyAttr = appendStr(policyAttr, "- Source Repository URI must match", c.Certificate.SourceRepositoryURI)
}
policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType)
if c.SAN != "" {
policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match", c.SAN)
} else if c.SANRegex != "" {
policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match regex", c.SANRegex)
}
policyAttr = appendStr(policyAttr, "- OIDC Issuer must match", c.Certificate.Issuer)
if c.Certificate.RunnerEnvironment == GitHubRunner {
policyAttr = appendStr(policyAttr, "- Action workflow Runner Environment must match ", GitHubRunner)
}
maxColLen := 0
for _, attr := range policyAttr {
if len(attr[0]) > maxColLen {

View file

@ -83,7 +83,7 @@ func TestVerifyAttestations(t *testing.T) {
attestations := []*api.Attestation{sgjAttestation[0], reusableWorkflowAttestations[0], sgjAttestation[1]}
require.Len(t, attestations, 3)
rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer)
rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer)
sgjResult := verification.BuildSigstoreJsMockResult(t)
mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult}
mockSgVerifier := verification.NewMockSigstoreVerifierWithMockResults(t, mockResults)

View file

@ -56,7 +56,7 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er
signedRepoRegex := expandToGitHubURLRegex(opts.Tenant, opts.SignerRepo)
c.SANRegex = signedRepoRegex
} else if opts.SignerWorkflow != "" {
validatedWorkflowRegex, err := validateSignerWorkflow(opts)
validatedWorkflowRegex, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow)
if err != nil {
return verification.EnforcementCriteria{}, err
}
@ -140,23 +140,23 @@ func buildSigstoreVerifyPolicy(c verification.EnforcementCriteria, a artifact.Di
return policy, nil
}
func validateSignerWorkflow(opts *Options) (string, error) {
func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) {
// we expect a provided workflow argument be in the format [HOST/]/<OWNER>/<REPO>/path/to/workflow.yml
// if the provided workflow does not contain a host, set the host
match, err := regexp.MatchString(hostRegex, opts.SignerWorkflow)
match, err := regexp.MatchString(hostRegex, signerWorkflow)
if err != nil {
return "", err
}
if match {
return fmt.Sprintf("^https://%s", opts.SignerWorkflow), nil
return fmt.Sprintf("^https://%s", signerWorkflow), nil
}
// if the provided workflow did not match the expect format
// we move onto creating a signer workflow using the provided host name
if opts.Hostname == "" {
if hostname == "" {
return "", errors.New("unknown host")
}
return fmt.Sprintf("^https://%s/%s", opts.Hostname, opts.SignerWorkflow), nil
return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil
}

View file

@ -4,7 +4,6 @@ import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/stretchr/testify/require"
)
@ -263,14 +262,8 @@ func TestValidateSignerWorkflow(t *testing.T) {
}
for _, tc := range testcases {
opts := &Options{
Config: factory.New("test").Config,
SignerWorkflow: tc.providedSignerWorkflow,
}
// All host resolution is done verify.go:RunE
opts.Hostname = tc.host
workflowRegex, err := validateSignerWorkflow(opts)
workflowRegex, err := validateSignerWorkflow(tc.host, tc.providedSignerWorkflow)
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
if tc.expectErr {

View file

@ -6,6 +6,7 @@ import (
"regexp"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
@ -261,19 +262,32 @@ func runVerify(opts *Options) error {
return nil
}
opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg())
opts.Logger.Printf("The following %s matched the policy criteria\n\n", text.Pluralize(len(verified), "attestation"))
// Otherwise print the results to the terminal in a table
tableContent, err := buildTableVerifyContent(opts.Tenant, verified)
if err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results"))
return err
}
// Otherwise print the results to the terminal
for i, v := range verified {
buildConfigURI := v.VerificationResult.Signature.Certificate.Extensions.BuildConfigURI
sourceRepoAndOrg, sourceWorkflow, err := extractAttestationDetail(opts.Tenant, buildConfigURI)
if err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse build config URI"))
return err
}
builderSignerURI := v.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI
signerRepoAndOrg, signerWorkflow, err := extractAttestationDetail(opts.Tenant, builderSignerURI)
if err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse build signer URI"))
return err
}
headers := []string{"repo", "predicate_type", "workflow"}
if err = opts.Logger.PrintTable(headers, tableContent); err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to print attestation details to table"))
return err
opts.Logger.Printf("- Attestation #%d\n", i+1)
rows := [][]string{
{" - Build repo", sourceRepoAndOrg},
{" - Build workflow", sourceWorkflow},
{" - Signer repo", signerRepoAndOrg},
{" - Signer workflow", signerWorkflow},
}
//nolint:errcheck
opts.Logger.PrintBulletPoints(rows)
}
// All attestations passed verification and policy evaluation
@ -304,39 +318,15 @@ func extractAttestationDetail(tenant, builderSignerURI string) (string, string,
match := orgAndRepoRegexp.FindStringSubmatch(builderSignerURI)
if len(match) < 2 {
return "", "", fmt.Errorf("no match found for org and repo")
return "", "", fmt.Errorf("no match found for org and repo: %s", builderSignerURI)
}
orgAndRepo := match[1]
match = workflowRegexp.FindStringSubmatch(builderSignerURI)
if len(match) < 2 {
return "", "", fmt.Errorf("no match found for workflow")
return "", "", fmt.Errorf("no match found for workflow: %s", builderSignerURI)
}
workflow := match[1]
return orgAndRepo, workflow, nil
}
func buildTableVerifyContent(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) {
content := make([][]string, len(results))
for i, res := range results {
if res.VerificationResult == nil ||
res.VerificationResult.Signature == nil ||
res.VerificationResult.Signature.Certificate == nil {
return nil, fmt.Errorf("bundle missing verification result fields")
}
builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI
repoAndOrg, workflow, err := extractAttestationDetail(tenant, builderSignerURI)
if err != nil {
return nil, err
}
if res.VerificationResult.Statement == nil {
return nil, fmt.Errorf("bundle missing attestation statement (bundle must originate from GitHub Artifact Attestations)")
}
predicateType := res.VerificationResult.Statement.PredicateType
content[i] = []string{repoAndOrg, predicateType, workflow}
}
return content, nil
}

View file

@ -415,7 +415,7 @@ func TestRunVerify(t *testing.T) {
opts.BundlePath = ""
opts.Owner = "sigstore"
require.Nil(t, runVerify(&opts))
require.NoError(t, runVerify(&opts))
})
t.Run("with owner which not matches SourceRepositoryOwnerURI", func(t *testing.T) {

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
# Get the root directory of the repository
rootDir="$(git rev-parse --show-toplevel)"
ghBuildPath="$rootDir/bin/gh"
artifactPath="$rootDir/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz"
bundlePath="$rootDir/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl"
# Download a custom trusted root for verification
if ! $ghBuildPath attestation trusted-root > trusted_root.jsonl; then
# cleanup test data
echo "Failed to download trusted root"
exit 1
fi
if ! $ghBuildPath attestation verify "$artifactPath" -b "$bundlePath" --digest-alg=sha512 --owner=sigstore --custom-trusted-root trusted_root.jsonl; then
echo "Failed to verify package with a Sigstore v0.2.0 bundle"
exit 1
fi

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# Get the root directory of the repository
rootDir="$(git rev-parse --show-toplevel)"
ghBuildPath="$rootDir/bin/gh"
ghCLIArtifact="$rootDir/pkg/cmd/attestation/test/data/gh_2.60.1_windows_arm64.zip"
# Verify the gh CLI artifact
echo "Testing with package $ghCLIArtifact"
if ! $ghBuildPath attestation verify "$ghCLIArtifact" --digest-alg=sha256 --owner=cli; then
echo "Failed to verify"
exit 1
fi