gh attestation trusted-root subcommand (#9206)

Adds `trusted-root` subcommand to `gh attestation`.

For use in upcoming docs on how to do offline verification with artifact
attestations.

---------

Signed-off-by: Zach Steindler <steiza@github.com>
Co-authored-by: Fredrik Skogman <kommendorkapten@github.com>
This commit is contained in:
Zach Steindler 2024-07-01 11:50:39 -04:00 committed by GitHub
parent 0f2e1ed9d1
commit f972050dc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 280 additions and 240 deletions

View file

@ -4,7 +4,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/attestation/download"
"github.com/cli/cli/v2/pkg/cmd/attestation/inspect"
"github.com/cli/cli/v2/pkg/cmd/attestation/tufrootverify"
"github.com/cli/cli/v2/pkg/cmd/attestation/trustedroot"
"github.com/cli/cli/v2/pkg/cmd/attestation/verify"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -24,7 +24,7 @@ func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command {
root.AddCommand(download.NewDownloadCmd(f, nil))
root.AddCommand(inspect.NewInspectCmd(f, nil))
root.AddCommand(verify.NewVerifyCmd(f, nil))
root.AddCommand(tufrootverify.NewTUFRootVerifyCmd(f, nil))
root.AddCommand(trustedroot.NewTrustedRootCmd(f, nil))
return root
}

View file

@ -1,90 +1 @@
{
"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
"tlogs": [
{
"baseUrl": "https://rekor.sigstore.dev",
"hashAlgorithm": "SHA2_256",
"publicKey": {
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
"validFor": {
"start": "2021-01-12T11:53:27.000Z"
}
},
"logId": {
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
}
}
],
"certificateAuthorities": [
{
"subject": {
"organization": "sigstore.dev",
"commonName": "sigstore"
},
"uri": "https://fulcio.sigstore.dev",
"certChain": {
"certificates": [
{
"rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="
}
]
},
"validFor": {
"start": "2021-03-07T03:20:29.000Z",
"end": "2022-12-31T23:59:59.999Z"
}
},
{
"subject": {
"organization": "sigstore.dev",
"commonName": "sigstore"
},
"uri": "https://fulcio.sigstore.dev",
"certChain": {
"certificates": [
{
"rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
},
{
"rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
}
]
},
"validFor": {
"start": "2022-04-13T20:06:15.000Z"
}
}
],
"ctlogs": [
{
"baseUrl": "https://ctfe.sigstore.dev/test",
"hashAlgorithm": "SHA2_256",
"publicKey": {
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
"validFor": {
"start": "2021-03-14T00:00:00.000Z",
"end": "2022-10-31T23:59:59.999Z"
}
},
"logId": {
"keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
}
},
{
"baseUrl": "https://ctfe.sigstore.dev/2022",
"hashAlgorithm": "SHA2_256",
"publicKey": {
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
"validFor": {
"start": "2022-10-20T00:00:00.000Z"
}
},
"logId": {
"keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
}
}
]
}
{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1","tlogs":[{"baseUrl":"https://rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-01-12T11:53:27.000Z"}},"logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}}],"certificateAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="}]},"validFor":{"start":"2021-03-07T03:20:29.000Z","end":"2022-12-31T23:59:59.999Z"}},{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"}]},"validFor":{"start":"2022-04-13T20:06:15.000Z"}}],"ctlogs":[{"baseUrl":"https://ctfe.sigstore.dev/test","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-03-14T00:00:00.000Z","end":"2022-10-31T23:59:59.999Z"}},"logId":{"keyId":"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="}},{"baseUrl":"https://ctfe.sigstore.dev/2022","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2022-10-20T00:00:00.000Z"}},"logId":{"keyId":"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="}}]}

View file

@ -0,0 +1,131 @@
package trustedroot
import (
"bytes"
"encoding/json"
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/spf13/cobra"
)
type Options struct {
TufUrl string
TufRootPath string
VerifyOnly bool
}
type tufClientInstantiator func(o *tuf.Options) (*tuf.Client, error)
func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command {
opts := &Options{}
trustedRootCmd := cobra.Command{
Use: "trusted-root [--tuf-url <url> --tuf-root <file-path>] [--verify-only]",
Args: cobra.ExactArgs(0),
Short: "Output trusted_root.jsonl contents, likely for offline verification",
Long: heredoc.Docf(`
### NOTE: This feature is currently in beta, and subject to change.
Output contents for a trusted_root.jsonl file, likely for offline verification.
When using %[1]sgh attestation verify%[1]s, if your machine is on the internet,
this will happen automatically. But to do offline verification, you need to
supply a trusted root file with %[1]s--custom-trusted-root%[1]s; this command
will help you fetch a %[1]strusted_root.jsonl%[1]s file for that purpose.
You can call this command without any flags to get a trusted root file covering
the Sigstore Public Good Instance as well as GitHub's Sigstore instance.
Otherwise you can use %[1]s--tuf-url%[1]s to specify the URL of a custom TUF
repository mirror, and %[1]s--tuf-root%[1]s should be the path to the
%[1]sroot.json%[1]s file that you securely obtained out-of-band.
If you just want to verify the integrity of your local TUF repository, and don't
want the contents of a trusted_root.jsonl file, use %[1]s--verify-only%[1]s.
`, "`"),
Example: heredoc.Doc(`
# Get a trusted_root.jsonl for both Sigstore Public Good and GitHub's instance
gh attestation trusted-root
`),
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
if err := getTrustedRoot(tuf.New, opts); err != nil {
return fmt.Errorf("Failed to verify the TUF repository: %w", err)
}
return nil
},
}
trustedRootCmd.Flags().StringVarP(&opts.TufUrl, "tuf-url", "", "", "URL to the TUF repository mirror")
trustedRootCmd.Flags().StringVarP(&opts.TufRootPath, "tuf-root", "", "", "Path to the TUF root.json file on disk")
trustedRootCmd.MarkFlagsRequiredTogether("tuf-url", "tuf-root")
trustedRootCmd.Flags().BoolVarP(&opts.VerifyOnly, "verify-only", "", false, "Don't output trusted_root.jsonl contents")
return &trustedRootCmd
}
func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
var tufOptions []*tuf.Options
tufOpt := verification.DefaultOptionsWithCacheSetting()
// Disable local caching, so we get up-to-date response from TUF repository
tufOpt.CacheValidity = 0
if opts.TufUrl != "" && opts.TufRootPath != "" {
tufRoot, err := os.ReadFile(opts.TufRootPath)
if err != nil {
return fmt.Errorf("failed to read root file %s: %v", opts.TufRootPath, err)
}
tufOpt.Root = tufRoot
tufOpt.RepositoryBaseURL = opts.TufUrl
tufOptions = append(tufOptions, tufOpt)
} else {
// Get from both Sigstore public good and GitHub private instance
tufOptions = append(tufOptions, tufOpt)
tufOpt = verification.GitHubTUFOptions()
tufOpt.CacheValidity = 0
tufOptions = append(tufOptions, tufOpt)
}
for _, tufOpt = range tufOptions {
tufClient, err := makeTUF(tufOpt)
if err != nil {
return fmt.Errorf("failed to create TUF client: %v", err)
}
t, err := tufClient.GetTarget("trusted_root.json")
if err != nil {
return err
}
output := new(bytes.Buffer)
err = json.Compact(output, t)
if err != nil {
return err
}
if !opts.VerifyOnly {
fmt.Println(output)
} else {
fmt.Printf("Local TUF repository for %s updated and verified\n", tufOpt.RepositoryBaseURL)
}
}
return nil
}

View file

@ -1,4 +1,4 @@
package tufrootverify
package trustedroot
import (
"bytes"
@ -6,16 +6,16 @@ import (
"strings"
"testing"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTUFRootVerifyCmd(t *testing.T) {
func TestNewTrustedRootCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
@ -27,29 +27,37 @@ func TestNewTUFRootVerifyCmd(t *testing.T) {
wantsErr bool
}{
{
name: "Missing mirror flag",
cli: "--root ../verification/embed/tuf-repo.github.com/root.json",
wantsErr: true,
},
{
name: "Missing root flag",
cli: "--mirror https://tuf-repo.github.com",
wantsErr: true,
},
{
name: "Has all required flags",
cli: "--mirror https://tuf-repo.github.com --root ../verification/embed/tuf-repo.github.com/root.json",
name: "Happy path",
cli: "",
wantsErr: false,
},
{
name: "Happy path",
cli: "--verify-only",
wantsErr: false,
},
{
name: "Custom TUF happy path",
cli: "--tuf-url https://tuf-repo.github.com --tuf-root ../verification/embed/tuf-repo.github.com/root.json",
wantsErr: false,
},
{
name: "Missing tuf-root flag",
cli: "--tuf-url https://tuf-repo.github.com",
wantsErr: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
cmd := NewTUFRootVerifyCmd(f, func() error {
cmd := NewTrustedRootCmd(f, func(_ *Options) error {
return nil
})
argv := strings.Split(tc.cli, " ")
argv := []string{}
if tc.cli != "" {
argv = strings.Split(tc.cli, " ")
}
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
@ -64,33 +72,35 @@ func TestNewTUFRootVerifyCmd(t *testing.T) {
}
}
var newTUFMockClient tufClientInstantiator = func(o *tuf.Options) (*tuf.Client, error) {
return &tuf.Client{}, nil
}
var newTUFErrClient tufClientInstantiator = func(o *tuf.Options) (*tuf.Client, error) {
return nil, fmt.Errorf("failed to create TUF client")
}
func TestTUFRootVerify(t *testing.T) {
func TestGetTrustedRoot(t *testing.T) {
mirror := "https://tuf-repo.github.com"
root := test.NormalizeRelativePath("../verification/embed/tuf-repo.github.com/root.json")
opts := &Options{
TufUrl: mirror,
TufRootPath: root,
}
t.Run("successfully verifies TUF root", func(t *testing.T) {
err := tufRootVerify(newTUFMockClient, mirror, root)
err := getTrustedRoot(tuf.New, opts)
require.NoError(t, err)
})
t.Run("failed to create TUF root", func(t *testing.T) {
err := getTrustedRoot(newTUFErrClient, opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to create TUF client")
})
t.Run("fails because the root cannot be found", func(t *testing.T) {
notFoundRoot := test.NormalizeRelativePath("./does/not/exist/root.json")
err := tufRootVerify(newTUFMockClient, mirror, notFoundRoot)
opts.TufRootPath = test.NormalizeRelativePath("./does/not/exist/root.json")
err := getTrustedRoot(tuf.New, opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to read root file")
})
t.Run("failed to create TUF root", func(t *testing.T) {
err := tufRootVerify(newTUFErrClient, mirror, root)
require.Error(t, err)
require.ErrorContains(t, err, "failed to create TUF client")
})
}

View file

@ -1,87 +0,0 @@
package tufrootverify
import (
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/spf13/cobra"
)
type tufClientInstantiator func(o *tuf.Options) (*tuf.Client, error)
func NewTUFRootVerifyCmd(f *cmdutil.Factory, runF func() error) *cobra.Command {
var mirror string
var root string
var cmd = cobra.Command{
Use: "tuf-root-verify --mirror <mirror-url> --root <root.json>",
Args: cobra.ExactArgs(0),
Short: "Verify the TUF repository from a provided TUF root",
Hidden: true,
Long: heredoc.Docf(`
### NOTE: This feature is currently in beta, and subject to change.
Verify a TUF repository with a local TUF root.
The command requires you provide the %[1]s--mirror%[1]s flag, which should be the URL
of the TUF repository mirror.
The command also requires you provide the %[1]s--root%[1]s flag, which should be the
path to the TUF root file.
GitHub relies on TUF to securely deliver the trust root for our signing authority.
For more information on TUF, see the official documentation: <https://theupdateframework.github.io/>.
`, "`"),
Example: heredoc.Doc(`
# Verify the TUF repository from a provided TUF root
gh attestation tuf-root-verify --mirror https://tuf-repo.github.com --root /path/to/1.root.json
`),
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF()
}
if err := tufRootVerify(tuf.New, mirror, root); err != nil {
return fmt.Errorf("Failed to verify the TUF repository: %w", err)
}
io := f.IOStreams
fmt.Sprintln(io.Out, io.ColorScheme().Green("Successfully verified the TUF repository"))
return nil
},
}
cmd.Flags().StringVarP(&mirror, "mirror", "m", "", "URL to the TUF repository mirror")
cmd.MarkFlagRequired("mirror") //nolint:errcheck
cmd.Flags().StringVarP(&root, "root", "r", "", "Path to the TUF root file on disk")
cmd.MarkFlagRequired("root") //nolint:errcheck
return &cmd
}
func tufRootVerify(makeTUF tufClientInstantiator, mirror, root string) error {
rb, err := os.ReadFile(root)
if err != nil {
return fmt.Errorf("failed to read root file %s: %v", root, err)
}
opts := verification.GitHubTUFOptions()
opts.Root = rb
opts.RepositoryBaseURL = mirror
// The purpose is the verify the TUF root and repository, make
// sure there is no caching enabled
opts.CacheValidity = 0
if _, err = makeTUF(opts); err != nil {
return fmt.Errorf("failed to create TUF client: %v", err)
}
return nil
}

View file

@ -1,7 +1,11 @@
package verification
import (
"bufio"
"bytes"
"crypto/x509"
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
@ -29,9 +33,9 @@ type SigstoreResults struct {
}
type SigstoreConfig struct {
CustomTrustedRoot string
Logger *io.Handler
NoPublicGood bool
TrustedRoot string
Logger *io.Handler
NoPublicGood bool
}
type SigstoreVerifier interface {
@ -65,13 +69,68 @@ func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify
}
issuer := leafCert.Issuer.Organization[0]
// if user provided a custom trusted root file path, use the custom verifier
if v.config.CustomTrustedRoot != "" {
customVerifier, err := newCustomVerifier(v.config.CustomTrustedRoot)
if v.config.TrustedRoot != "" {
customTrustRoots, err := os.ReadFile(v.config.TrustedRoot)
if err != nil {
return nil, "", fmt.Errorf("failed to create custom verifier: %v", err)
return nil, "", fmt.Errorf("unable to read file %s: %v", v.config.TrustedRoot, err)
}
return customVerifier, issuer, nil
reader := bufio.NewReader(bytes.NewReader(customTrustRoots))
var line []byte
var readError error
line, readError = reader.ReadBytes('\n')
for readError == nil {
// Load each trusted root
trustedRoot, err := root.NewTrustedRootFromJSON(line)
if err != nil {
return nil, "", fmt.Errorf("failed to create custom verifier: %v", err)
}
// Compare bundle leafCert issuer with trusted root cert authority
certAuthorities := trustedRoot.FulcioCertificateAuthorities()
for _, certAuthority := range certAuthorities {
lowestCert, err := getLowestCertInChain(&certAuthority)
if err != nil {
return nil, "", err
}
if len(lowestCert.Issuer.Organization) == 0 {
continue
}
if lowestCert.Issuer.Organization[0] == issuer {
// Determine what policy to use with this trusted root.
//
// Note that we are *only* inferring the policy with the
// issuer. We *must* use the trusted root provided.
if issuer == PublicGoodIssuerOrg {
if v.config.NoPublicGood {
return nil, "", fmt.Errorf("Detected public good instance but requested verification without public good instance")
}
verifier, err := newPublicGoodVerifierWithTrustedRoot(trustedRoot)
if err != nil {
return nil, "", err
}
return verifier, issuer, nil
} else if issuer == GitHubIssuerOrg {
verifier, err := newGitHubVerifierWithTrustedRoot(trustedRoot)
if err != nil {
return nil, "", err
}
return verifier, issuer, nil
} else {
// Make best guess at reasonable policy
customVerifier, err := newCustomVerifier(trustedRoot)
if err != nil {
return nil, "", fmt.Errorf("failed to create custom verifier: %v", err)
}
return customVerifier, issuer, nil
}
}
}
line, readError = reader.ReadBytes('\n')
}
return nil, "", fmt.Errorf("unable to use provided trusted roots")
}
if leafCert.Issuer.Organization[0] == PublicGoodIssuerOrg && !v.config.NoPublicGood {
@ -93,6 +152,18 @@ func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify
return nil, "", fmt.Errorf("leaf certificate issuer is not recognized")
}
func getLowestCertInChain(ca *root.CertificateAuthority) (*x509.Certificate, error) {
if ca.Leaf != nil {
return ca.Leaf, nil
} else if len(ca.Intermediates) > 0 {
return ca.Intermediates[0], nil
} else if ca.Root != nil {
return ca.Root, nil
}
return nil, fmt.Errorf("certificate authority had no certificates")
}
func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) *SigstoreResults {
// initialize the processing results before attempting to verify
// with multiple verifiers
@ -143,21 +214,17 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve
}
}
func newCustomVerifier(trustedRootFilePath string) (*verify.SignedEntityVerifier, error) {
trustedRoot, err := root.NewTrustedRootFromPath(trustedRootFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create trusted root from file %s: %v", trustedRootFilePath, err)
}
func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
// All we know about this trust root is its configuration so make some
// educated guesses as to what the policy should be.
verifierConfig := []verify.VerifierOption{}
verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1))
// This requires some independent corroboration of the signing certificate
// (e.g. from Sigstore Fulcio) time, one of:
// - a signed timestamp from a timestamp authority in the trusted root
// - a transparency log entry (e.g. from Sigstore Rekor)
verifierConfig = append(verifierConfig, verify.WithObserverTimestamps(1))
// Infer verification options from contents of trusted root
if len(trustedRoot.TimestampingAuthorities()) > 0 {
verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1))
}
if len(trustedRoot.RekorLogs()) > 0 {
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1))
}
@ -180,6 +247,10 @@ func newGitHubVerifier() (*verify.SignedEntityVerifier, error) {
if err != nil {
return nil, err
}
return newGitHubVerifierWithTrustedRoot(trustedRoot)
}
func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create GitHub verifier: %v", err)
@ -199,6 +270,10 @@ func newPublicGoodVerifier() (*verify.SignedEntityVerifier, error) {
return nil, fmt.Errorf("failed to get trusted root: %v", err)
}
return newPublicGoodVerifierWithTrustedRoot(trustedRoot)
}
func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create Public Good verifier: %v", err)

View file

@ -92,8 +92,8 @@ func TestLiveSigstoreVerifier(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
Logger: io.NewTestHandler(),
CustomTrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
Logger: io.NewTestHandler(),
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
})
res := verifier.Verify(attestations, publicGoodPolicy(t))

View file

@ -18,7 +18,7 @@ type Options struct {
ArtifactPath string
BundlePath string
Config func() (gh.Config, error)
CustomTrustedRoot string
TrustedRoot string
DenySelfHostedRunner bool
DigestAlgorithm string
Limit int

View file

@ -132,9 +132,9 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
}
config := verification.SigstoreConfig{
CustomTrustedRoot: opts.CustomTrustedRoot,
Logger: opts.Logger,
NoPublicGood: opts.NoPublicGood,
TrustedRoot: opts.TrustedRoot,
Logger: opts.Logger,
NoPublicGood: opts.NoPublicGood,
}
opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config)
@ -156,8 +156,8 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo")
verifyCmd.MarkFlagsOneRequired("owner", "repo")
verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type")
verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance")
verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification")
verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Do not verify attestations signed with Sigstore public good instance")
verifyCmd.Flags().StringVarP(&opts.TrustedRoot, "custom-trusted-root", "", "", "Path to a trusted_root.jsonl file; likely for offline verification")
verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
cmdutil.AddFormatFlags(verifyCmd, &opts.exporter)
// policy enforcement flags

View file

@ -220,7 +220,7 @@ func TestNewVerifyCmd(t *testing.T) {
assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath)
assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
assert.Equal(t, tc.wants.CustomTrustedRoot, opts.CustomTrustedRoot)
assert.Equal(t, tc.wants.TrustedRoot, opts.TrustedRoot)
assert.Equal(t, tc.wants.DenySelfHostedRunner, opts.DenySelfHostedRunner)
assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm)
assert.Equal(t, tc.wants.Limit, opts.Limit)