Merge remote-tracking branch 'upstream/trunk' into attestation-verify-ref-commit-policy-opts
This commit is contained in:
commit
6c0cdca554
11 changed files with 99 additions and 80 deletions
8
.github/workflows/deployment.yml
vendored
8
.github/workflows/deployment.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
22
test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh
Executable file
22
test/integration/attestation-cmd/verify/verify-with-custom-trusted-root.sh
Executable 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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue