mirror of
https://github.com/google/go-github.git
synced 2026-10-28 16:34:37 +08:00
201 lines
6.0 KiB
Go
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
|
|
}
|