Files

201 lines
6.0 KiB
Go

// Copyright 2024 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This is a simple example of how to verify artifact attestations
// hosted on GitHub using the sigstore-go library.
// This is a very barebones example drawn from the sigstore-go
// library's examples and should not be used in production.
package main
import (
"context"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"github.com/google/go-github/v93/github"
"github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/root"
"github.com/sigstore/sigstore-go/pkg/verify"
)
var (
owner = flag.String("owner", "cli", "GitHub organization or user to scope attestation lookup by")
// You can use a utility like openssl or sha256sum to
// compute the digest.
artifactDigest = flag.String("artifact-digest", "", "The digest of the artifact")
// The algorithm used to compute the digest of the artifact.
// Note that the GitHub API currently only supports querying
// by sha256 digest.
artifactDigestAlgorithm = flag.String("artifact-digest-algorithm", "sha256", "The algorithm used to compute the digest of the artifact")
// Attestations produced by GitHub Actions use ID tokens
// issued by GitHub.
expectedIssuer = flag.String("expected-issuer", "https://token.actions.githubusercontent.com", "Issuer of the OIDC token")
// Subject Alternative Name is set to the calling workflow file.
// This value will vary from repository to repository.
expectedSAN = flag.String("expected-san", "https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk", "The expected Subject Alternative Name (SAN) of the certificate used to sign the attestation")
// Attestations produced by GitHub Actions use the public
// good trust root maintained by Sigstore.
// A copy is included in this repo for convenience.
//
// https://github.com/sigstore/root-signing/raw/refs/heads/main/targets/trusted_root.json
trustedRootJSONPath = flag.String("trusted-root-json-path", "verifyartifact/trusted-root-public-good.json", "Path to the trusted root JSON file")
)
func usage() {
fmt.Fprintln(os.Stderr, "This is an example of how to verify the provenance of an artifact using GitHub Attestations and the sigstore-go library.")
fmt.Fprintf(os.Stderr, "\nUsage: %v [flags]\n", os.Args[0])
fmt.Fprint(os.Stderr, "\nThe flags are:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, `
Example:
Verifying a GitHub CLI artifact
%v -owner cli \
-artifact-digest 2ce2e480e3c3f7ca0af83418d3ebaeedacee135dbac94bd946d7d84edabcdb64 \
-expected-san https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk
See https://github.com/cli/cli/attestations/2543768 for a summary of the attestation.
`, os.Args[0])
}
func main() {
flag.Parse()
if *artifactDigest == "" {
fmt.Fprintln(os.Stderr, "artifact-digest is required.")
usage()
os.Exit(1)
}
token := os.Getenv("GITHUB_AUTH_TOKEN")
if token == "" {
log.Fatal("Unauthorized: No token present. Please set the GITHUB_AUTH_TOKEN environment variable to a valid token with `attestations:read` permission.")
}
ctx := context.Background()
client, err := github.NewClient(github.WithAuthToken(token))
if err != nil {
log.Fatalf("Error creating GitHub client: %v", err)
}
// Fetch attestations from the GitHub API.
// The attestations API doesn't differentiate between users and orgs,
// so we can use the OrganizationsService to fetch attestations for both.
attestations, _, err := client.Organizations.ListAttestations(ctx, *owner, fmt.Sprintf("%v:%v", *artifactDigestAlgorithm, *artifactDigest), nil)
if err != nil {
log.Fatal(err)
}
if len(attestations.Attestations) == 0 {
log.Fatal("No attestations found.")
}
sev, err := getSignedEntityVerifier()
if err != nil {
log.Fatal(err)
}
pb, err := getPolicyBuilder()
if err != nil {
log.Fatal(err)
}
var b *bundle.Bundle
for _, attestation := range attestations.Attestations {
if err := json.Unmarshal(attestation.Bundle, &b); err != nil {
log.Fatal(err)
}
err := runVerification(sev, pb, b)
if err != nil {
log.Fatal(err)
}
}
}
func getTrustedMaterial() (root.TrustedMaterialCollection, error) {
trustedRootJSON, err := os.ReadFile(*trustedRootJSONPath)
if err != nil {
return nil, fmt.Errorf("failed to read %v: %w", *trustedRootJSONPath, err)
}
trustedRoot, err := root.NewTrustedRootFromJSON(trustedRootJSON)
if err != nil {
return nil, err
}
trustedMaterial := root.TrustedMaterialCollection{
trustedRoot,
}
return trustedMaterial, nil
}
func getIdentityPolicies() ([]verify.PolicyOption, error) {
certID, err := verify.NewShortCertificateIdentity(*expectedIssuer, "", *expectedSAN, "")
if err != nil {
return nil, err
}
return []verify.PolicyOption{
verify.WithCertificateIdentity(certID),
}, nil
}
func getSignedEntityVerifier() (*verify.Verifier, error) {
// Set up the verifier
verifierConfig := []verify.VerifierOption{
verify.WithSignedCertificateTimestamps(1),
verify.WithObserverTimestamps(1),
verify.WithTransparencyLog(1),
}
// Set up the trusted material
trustedMaterial, err := getTrustedMaterial()
if err != nil {
return nil, err
}
return verify.NewVerifier(trustedMaterial, verifierConfig...)
}
func getPolicyBuilder() (*verify.PolicyBuilder, error) {
// Set up the identity policy
identityPolicies, err := getIdentityPolicies()
if err != nil {
return nil, err
}
// Set up the artifact policy
artifactDigestBytes, err := hex.DecodeString(*artifactDigest)
if err != nil {
return nil, err
}
artifactPolicy := verify.WithArtifactDigest(*artifactDigestAlgorithm, artifactDigestBytes)
pb := verify.NewPolicy(artifactPolicy, identityPolicies...)
return &pb, nil
}
func runVerification(sev *verify.Verifier, pb *verify.PolicyBuilder, b *bundle.Bundle) error {
res, err := sev.Verify(b, *pb)
if err != nil {
return err
}
fmt.Fprint(os.Stderr, "Verification successful!\n")
marshaled, err := json.MarshalIndent(res, "", " ")
if err != nil {
return err
}
fmt.Println(string(marshaled))
return nil
}