From a0f4538e613d4b5bcf786a0111f320da8e700409 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Thu, 11 Jul 2024 14:46:37 +0200 Subject: [PATCH] Dsse one sig (#226) * Ran go fmt Signed-off-by: Fredrik Skogman * Added test to verify that dsse envelope has exact one signature. This is a bug-fix, as the protobuf spec for the bundl requires exactly one signature to be present in the envelope. Signed-off-by: Fredrik Skogman * Spelling error. Signed-off-by: Fredrik Skogman * Correct year in copyright notice. Signed-off-by: Fredrik Skogman * Lint fixes: removed unnecessary newline. Signed-off-by: Fredrik Skogman * Updates from feeback. Better error name, more explicit about the usage. More explicit test caste name. Signed-off-by: Fredrik Skogman --------- Signed-off-by: Fredrik Skogman --- examples/sigstore-go-signing/main.go | 18 ++-- pkg/testing/data/data.go | 15 ++- pkg/testing/data/sigstoreBundle2Sig.json | 52 ++++++++++ pkg/verify/dsse_test.go | 124 +++++++++++++++++++++++ pkg/verify/signature.go | 11 +- pkg/verify/signed_entity_test.go | 13 +++ 6 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 pkg/testing/data/sigstoreBundle2Sig.json create mode 100644 pkg/verify/dsse_test.go diff --git a/examples/sigstore-go-signing/main.go b/examples/sigstore-go-signing/main.go index 6548aa65..a54e12b0 100644 --- a/examples/sigstore-go-signing/main.go +++ b/examples/sigstore-go-signing/main.go @@ -111,9 +111,9 @@ func main() { if *idToken != "" { fulcioOpts := &sign.FulcioOptions{ - BaseURL: "https://fulcio.sigstage.dev", - Timeout: time.Duration(30 * time.Second), - Retries: 1, + BaseURL: "https://fulcio.sigstage.dev", + Timeout: time.Duration(30 * time.Second), + Retries: 1, } opts.CertificateProvider = sign.NewFulcio(fulcioOpts) opts.CertificateProviderOptions = &sign.CertificateProviderOptions{ @@ -123,9 +123,9 @@ func main() { if *tsa { tsaOpts := &sign.TimestampAuthorityOptions{ - URL: "https://timestamp.githubapp.com/api/v1/timestamp", - Timeout: time.Duration(30 * time.Second), - Retries: 1, + URL: "https://timestamp.githubapp.com/api/v1/timestamp", + Timeout: time.Duration(30 * time.Second), + Retries: 1, } opts.TimestampAuthorities = append(opts.TimestampAuthorities, sign.NewTimestampAuthority(tsaOpts)) @@ -135,9 +135,9 @@ func main() { if *rekor { rekorOpts := &sign.RekorOptions{ - BaseURL: "https://rekor.sigstage.dev", - Timeout: time.Duration(90 * time.Second), - Retries: 1, + BaseURL: "https://rekor.sigstage.dev", + Timeout: time.Duration(90 * time.Second), + Retries: 1, } opts.TransparencyLogs = append(opts.TransparencyLogs, sign.NewRekor(rekorOpts)) } diff --git a/pkg/testing/data/data.go b/pkg/testing/data/data.go index 18084b3e..6f6ada8c 100644 --- a/pkg/testing/data/data.go +++ b/pkg/testing/data/data.go @@ -40,9 +40,14 @@ func Unmarshal[T any](t *testing.T, data []byte) T { //go:embed sigstoreBundle.json var SigstoreBundleRaw []byte +//go:embed sigstoreBundle2Sig.json +var SigstoreBundle2SigRaw []byte + //go:embed sigstore.js@2.0.0-provenanceBundle.json var SigstoreJS200ProvenanceBundleRaw []byte +// TestBundle creates *bundle.ProtobufBundle from a raw byte stream +// containing a JSON encoded protobuf bundle. func TestBundle(t *testing.T, raw []byte) *bundle.ProtobufBundle { var b protobundle.Bundle err := protojson.Unmarshal(raw, &b) @@ -56,15 +61,23 @@ func TestBundle(t *testing.T, raw []byte) *bundle.ProtobufBundle { return bun } -// SigstoreBundle returns a test *sigstore.Bundle +// SigstoreBundle returns a test *sigstore.Bundle. func SigstoreBundle(t *testing.T) *bundle.ProtobufBundle { return TestBundle(t, SigstoreBundleRaw) } +// SigstoreBundle2Sig returns a test *sigstore.Bundle with two signatures. +func SigstoreBundle2Sig(t *testing.T) *bundle.ProtobufBundle { + return TestBundle(t, SigstoreBundle2SigRaw) +} + +// SigstoreJS200ProvenanceBundle returns a test *sigstore.Bundle that +// contains a complete sigstore-js build provenance. func SigstoreJS200ProvenanceBundle(t *testing.T) *bundle.ProtobufBundle { return TestBundle(t, SigstoreJS200ProvenanceBundleRaw) } +// PublicGoodTrustedMaterialRoot retruns a *root.TrustedRoot for PGI. func PublicGoodTrustedMaterialRoot(t *testing.T) *root.TrustedRoot { trustedrootJSON, _ := os.ReadFile("../../examples/trusted-root-public-good.json") trustedRoot, _ := root.NewTrustedRootFromJSON(trustedrootJSON) diff --git a/pkg/testing/data/sigstoreBundle2Sig.json b/pkg/testing/data/sigstoreBundle2Sig.json new file mode 100644 index 00000000..cd81691c --- /dev/null +++ b/pkg/testing/data/sigstoreBundle2Sig.json @@ -0,0 +1,52 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "6800908", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1668034836", + "inclusionPromise": { + "signedEntryTimestamp": "MEYCIQCEx8HKsx9hobZjrNqHCSEJvjMEhc2wU2mUwkI7ButQHAIhAPevmw7piNjE2N1OWHmp9S5kBvlVIg93qu4i9yRaswur" + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU51ZWtORFFXbGhaMEYzU1VKQlowbFZRbTV0V2xKMFpHdFBkR1pQTDB4NVp6UXpOVU5TSzFaSmFTdEJkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BKZUUxVVFUVk5hazEzVFVSTk1WZG9ZMDVOYWtsNFRWUkJOVTFxVFhoTlJFMHhWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWbFZVVTJUM2d2TkRFMFN6QmtRbmd6WXpOWEszUlJOMDVVVTJ4SlZsWXlORmxUWWtJS2JEWldlWFZKVmk5cE1UVkxRMnhUYWxWdk1uRlJkVXRUVlRSRmVtMUlaaklyUlUxcUwxbElXVWhsUWtGRWEwUjRhalpQUTBGVlZYZG5aMFpDVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZVdlZrZERDbFZJVmxnMVlsaG9aWFF5TVdsMFltbzFWM1pvV25GQmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQwaDNXVVJXVWpCU1FWRklMMEpDVlhkRk5FVlNXVzVLY0ZsWE5VRmFSMVp2V1ZjeGJHTnBOV3BpTWpCM1RFRlpTMHQzV1VKQ1FVZEVkbnBCUWdwQlVWRmxZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESjRkbG95YkhWTU1qbG9aRmhTYjAxSlIwdENaMjl5UW1kRlJVRmtXalZCWjFGRENrSklkMFZsWjBJMFFVaFpRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJRVUZIUlZod0t6RUtXRkZCUVVKQlRVRlNla0pHUVdsRlFXdGtTVFk1TkM4NFFqSnlUMlJwZVZsUWJFVnVZMlpTZDJ0MVltOWtUMW8wYW14dE5HYzFNamcxVEd0RFNVaFNjQXB0UjJnMGNEVlBZeXRXYXl0QlMwaE5aSFF3Vm5SRU1pOHJZMkZJVjNsbE1WWnhSRFJ5UjJORFRVRnZSME5EY1VkVFRUUTVRa0ZOUkVFeVkwRk5SMUZEQ2sxQmVVczRkRkUxYlZCRFEybE1hbWxKYzFaelNWcFFXWFpvZGtSU00wUkdiR2sxUVZGNmFsTkRlbU5GVXk5b05XWkNNMGR3WlVSa1FrOTFPWFIxYTJFS2IzZEpkMDVyYjBWWldraFBkR3RoWTB4bGNrdzFNbVZaVTNCV2FtWlljMEV3WjBKTmNVRnViVXhIZDFoMGVtdFdTM0Z4ZWxWdFlscG9RM1k0YlRnMVlncG1kVWxTQ2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIiwic2lnIjoiVFVWVlEwbEdZeTlDZVV4b1EydFNNbGwwUVUxaWJVcHdNakF5V20xYU5GaFdSMWhHUzJrM2NpdHhOMnh6VGtSUVFXbEZRUzlLZDFneVVHbHNRMHgyYTNGRk9VNUtUVVpMVG00eVF6SnFPR05JTDNwNVJtaFJOalYzY21reVNGazkifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNjZiM2RjZmNhNjU5ZTVkNTA1NjAyYzNjOWFmOGZkMmJmNmE0YWZhY2FjMzNiMTc1ZTFkN2UwZWNhYjEwZjg5MCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjhhNzVmNmM4ZGM0ZDlmNDA3Mjg1YmI0OWQzZTAxOWE1ZTY2NmY0MzQ5OWMyNzA3ZDkyODlhYTI3YzNjMmE2N2UifX19fQ==" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [] + }, + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIICnzCCAiagAwIBAgIUBnmZRtdkOtfO/Lyg435CR+VIi+AwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA5MjMwMDM1WhcNMjIxMTA5MjMxMDM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeUE6Ox/414K0dBx3c3W+tQ7NTSlIVV24YSbBl6VyuIV/i15KClSjUo2qQuKSU4EzmHf2+EMj/YHYHeBADkDxj6OCAUUwggFBMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU/VGCUHVX5bXhet21itbj5WvhZqAwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEXp+1XQAABAMARzBFAiEAkdI694/8B2rOdiyYPlEncfRwkubodOZ4jlm4g5285LkCIHRpmGh4p5Oc+Vk+AKHMdt0VtD2/+caHWye1VqD4rGcCMAoGCCqGSM49BAMDA2cAMGQCMAyK8tQ5mPCCiLjiIsVsIZPYvhvDR3DFli5AQzjSCzcES/h5fB3GpeDdBOu9tukaowIwNkoEYZHOtkacLerL52eYSpVjfXsA0gBMqAnmLGwXtzkVKqqzUmbZhCv8m85bfuIR" + }, + { + "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" + } + ] + } + }, + "dsseEnvelope": { + "payload": "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJzbHNhLXByb3ZlbmFuY2UtMC4wLjcudGd6IiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogImJiZmQzNzJmYzliZWViNzc3ZmUzNWQwNWJiMTBjMGM3MGE5NzMzZDM2NmE3NWZlZDdkZDE4ODY2YzczOTFkZTNlZTdlODhiYTc0ZGQ0N2FiZjNlMTVjODQ1ZTU0N2ZjZjBlNWMzZGE4MDg1NGM3NTE1NTQyMjRkM2E2ZDRlNTVmIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9zbHNhLXByb3ZlbmFuY2UvZ2hhQHYwIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9zbHNhLXByb3ZlbmFuY2VAMC4wLjEiCiAgICB9LAogICAgImludm9jYXRpb24iOiB7CiAgICAgICJjb25maWdTb3VyY2UiOiB7CiAgICAgICAgInVyaSI6ICJnaXQraHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9zbHNhLXByb3ZlbmFuY2VAcmVmcy9oZWFkcy9kZW1vIiwKICAgICAgICAiZGlnZXN0IjogewogICAgICAgICAgInNoYTEiOiAiMjljZmYzZGQ2NWY3ODBjMzYwMWJkNDU3YWNiNmZlNGU1OTMxYzgyNSIKICAgICAgICB9LAogICAgICAgICJlbnRyeVBvaW50IjogImRlbW8iCiAgICAgIH0sCiAgICAgICJwYXJhbWV0ZXJzIjoge30sCiAgICAgICJlbnZpcm9ubWVudCI6IHsKICAgICAgICAiR0lUSFVCX0VWRU5UX05BTUUiOiAicHVzaCIsCiAgICAgICAgIkdJVEhVQl9KT0IiOiAicnVuLXByb3ZlbmFuY2UtZGVtbyIsCiAgICAgICAgIkdJVEhVQl9SRUYiOiAicmVmcy9oZWFkcy9kZW1vIiwKICAgICAgICAiR0lUSFVCX1JFRl9UWVBFIjogImJyYW5jaCIsCiAgICAgICAgIkdJVEhVQl9SRVBPU0lUT1JZIjogImdpdGh1Yi9zbHNhLXByb3ZlbmFuY2UiLAogICAgICAgICJHSVRIVUJfUkVQT1NJVE9SWV9PV05FUiI6ICJnaXRodWIiLAogICAgICAgICJHSVRIVUJfUlVOX0FUVEVNUFQiOiAiNCIsCiAgICAgICAgIkdJVEhVQl9SVU5fSUQiOiAiMzAyNDA5MTU0NiIsCiAgICAgICAgIkdJVEhVQl9SVU5fTlVNQkVSIjogIjE3IiwKICAgICAgICAiR0lUSFVCX1NIQSI6ICIyOWNmZjNkZDY1Zjc4MGMzNjAxYmQ0NTdhY2I2ZmU0ZTU5MzFjODI1IiwKICAgICAgICAiR0lUSFVCX1dPUktGTE9XIjogImRlbW8iLAogICAgICAgICJJTUFHRV9PUyI6ICJ1YnVudHUyMCIsCiAgICAgICAgIklNQUdFX1ZFUlNJT04iOiAiMjAyMjA5MDUuMSIsCiAgICAgICAgIlJVTk5FUl9BUkNIIjogIlg2NCIsCiAgICAgICAgIlJVTk5FUl9OQU1FIjogIkdpdEh1YiBBY3Rpb25zIDUwIiwKICAgICAgICAiUlVOTkVSX09TIjogIkxpbnV4IgogICAgICB9CiAgICB9LAogICAgIm1ldGFkYXRhIjogewogICAgICAiYnVpbGRJbnZvY2F0aW9uSWQiOiAiMzAyNDA5MTU0Ni00IiwKICAgICAgImNvbXBsZXRlbmVzcyI6IHsKICAgICAgICAicGFyYW1ldGVycyI6IGZhbHNlLAogICAgICAgICJlbnZpcm9ubWVudCI6IGZhbHNlLAogICAgICAgICJtYXRlcmlhbHMiOiBmYWxzZQogICAgICB9LAogICAgICAicmVwcm9kdWNpYmxlIjogZmFsc2UKICAgIH0sCiAgICAibWF0ZXJpYWxzIjogWwogICAgICB7CiAgICAgICAgInVyaSI6ICJnaXQraHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9zbHNhLXByb3ZlbmFuY2UiLAogICAgICAgICJkaWdlc3QiOiB7CiAgICAgICAgICAic2hhMSI6ICIyOWNmZjNkZDY1Zjc4MGMzNjAxYmQ0NTdhY2I2ZmU0ZTU5MzFjODI1IgogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQo=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIFc/ByLhCkR2YtAMbmJp202ZmZ4XVGXFKi7r+q7lsNDPAiEA/JwX2PilCLvkqE9NJMFKNn2C2j8cH/zyFhQ65wri2HY=", + "keyid": "" + }, + { + "sig": "MEUCIFc/ByLhCkR2YtAMbmJp202ZmZ4XVGXFKi7r+q7lsNDPAiEA/JwX2PilCLvkqE9NJMFKNn2C2j8cH/zyFhAAAAAAAHY=", + "keyid": "" + } + ] + } +} diff --git a/pkg/verify/dsse_test.go b/pkg/verify/dsse_test.go new file mode 100644 index 00000000..5684c247 --- /dev/null +++ b/pkg/verify/dsse_test.go @@ -0,0 +1,124 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" +) + +type dsseSigner struct { + pk *ecdsa.PrivateKey +} + +func (d *dsseSigner) Sign(_ context.Context, data []byte) ([]byte, error) { + digest := sha256.Sum256(data) + return d.pk.Sign(rand.Reader, digest[:], nil) +} + +func (d *dsseSigner) KeyID() (string, error) { + return "", nil +} + +type envelopeContent struct { + EnvelopeContent + e *dsse.Envelope +} + +func (e *envelopeContent) RawEnvelope() *dsse.Envelope { + return e.e +} + +func TestVerifyEnvelopeSignatureCount(t *testing.T) { + privk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + pubk := privk.Public().(*ecdsa.PublicKey) + assert.NoError(t, err) + + var s = dsseSigner{ + pk: privk, + } + + testcases := []struct { + count int + fail bool + }{ + { + count: 0, + fail: true, + }, + { + count: 1, + fail: false, + }, + { + count: 2, + fail: true, + }, + } + + for _, tc := range testcases { + t.Run(fmt.Sprintf("Test DSSE verify with %d signatures", tc.count), + func(t *testing.T) { + var e *dsse.Envelope + + if tc.count == 0 { + // Need to create the envelope manually + e = &dsse.Envelope{ + PayloadType: "test-payload-type", + // b64(test-payload) + Payload: "dGVzdC1wYXlsb2Fk", + } + } else { + var signers []dsse.Signer + + for i := 0; i < tc.count; i++ { + signers = append(signers, &s) + } + + es, err := dsse.NewEnvelopeSigner(signers...) + assert.NoError(t, err) + e, err = es.SignPayload(context.Background(), + "test-payload-type", + []byte("test-payload")) + assert.NoError(t, err) + } + sigver, err := signature.LoadECDSAVerifier(pubk, crypto.SHA256) + assert.NoError(t, err) + err = verifyEnvelope( + sigver, + &envelopeContent{ + e: e, + }, + ) + + if tc.fail { + assert.True(t, errors.Is(err, ErrDSSEInvalidSignatureCount)) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index 2cadd126..ba5d2120 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -31,6 +31,8 @@ import ( "github.com/sigstore/sigstore/pkg/signature/options" ) +var ErrDSSEInvalidSignatureCount = errors.New("exactly one signature is required") + func VerifySignature(sigContent SignatureContent, verificationContent VerificationContent, trustedMaterial root.TrustedMaterial) error { // nolint: revive var verifier signature.Verifier var err error @@ -100,6 +102,13 @@ func getSignatureVerifier(verificationContent VerificationContent, tm root.Trust } func verifyEnvelope(verifier signature.Verifier, envelope EnvelopeContent) error { + dsseEnv := envelope.RawEnvelope() + + // A DSSE envelope in a Sigstore bundle MUST only contain one + // signature, even though DSSE is more permissive. + if len(dsseEnv.Signatures) != 1 { + return ErrDSSEInvalidSignatureCount + } pub, err := verifier.PublicKey() if err != nil { return fmt.Errorf("could not fetch verifier public key: %w", err) @@ -113,7 +122,7 @@ func verifyEnvelope(verifier signature.Verifier, envelope EnvelopeContent) error return fmt.Errorf("could not load envelope verifier: %w", err) } - _, err = envVerifier.Verify(context.TODO(), envelope.RawEnvelope()) + _, err = envVerifier.Verify(context.TODO(), dsseEnv) if err != nil { return fmt.Errorf("could not verify envelope: %w", err) } diff --git a/pkg/verify/signed_entity_test.go b/pkg/verify/signed_entity_test.go index bd28496f..5e0cc5d7 100644 --- a/pkg/verify/signed_entity_test.go +++ b/pkg/verify/signed_entity_test.go @@ -15,6 +15,7 @@ package verify_test import ( + "errors" "strings" "testing" "unicode" @@ -367,3 +368,15 @@ func ensureKeysBeginWithLowercase(t *testing.T, obj interface{}) { } } } + +func TestSigstoreBundle2Sig(t *testing.T) { + tr := data.PublicGoodTrustedMaterialRoot(t) + entity := data.SigstoreBundle2Sig(t) + + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + assert.NoError(t, err) + + res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.True(t, errors.Is(err, verify.ErrDSSEInvalidSignatureCount)) + assert.Nil(t, res) +}