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:
parent
0f2e1ed9d1
commit
f972050dc9
10 changed files with 280 additions and 240 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="}}]}
|
||||
|
|
|
|||
131
pkg/cmd/attestation/trustedroot/trustedroot.go
Normal file
131
pkg/cmd/attestation/trustedroot/trustedroot.go
Normal 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
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue