diff --git a/docs/verification.md b/docs/verification.md index 724a5e3e..3533c1e6 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -83,14 +83,18 @@ Next, we'll create a verifier with some options, which will enable SCT verificat } ``` -Then, we need to prepare the expected artifact digest and certificate identity. Note that these options may be omitted, but only if the options `WithoutIdentitiesUnsafe`/`WithoutArtifactUnsafe` are provided. This is a failsafe to ensure that the caller is aware that simply verifying the bundle is not enough, you must also verify the contents of the bundle against a specific identity and artifact. +Then, we need to prepare the expected artifact digest. Note that this option has an alternative option `WithoutArtifactUnsafe`. This is a failsafe to ensure that the caller is aware that simply verifying the bundle is not enough, you must also verify the contents of the bundle against a specific artifact. ```go digest, err := hex.DecodeString("76176ffa33808b54602c7c35de5c6e9a4deb96066dba6533f50ac234f4f1f4c6b3527515dc17c06fbe2860030f410eee69ea20079bd3a2c6f3dcf3b329b10751") if err != nil { panic(err) } +``` + +In this case, we also need to prepare the expected certificate identity. Note that this option has an alternative option `WithoutIdentitiesUnsafe`. This is a failsafe to ensure that the caller is aware that simply verifying the bundle is not enough, you must also verify the contents of the bundle against a specific identity. If your bundle was signed with a key, and thus does not have a certificate identity, a better choice is to use the `WithKey` option. +```go certID, err := verify.NewShortCertificateIdentity("https://token.actions.githubusercontent.com", "", "", "^https://github.com/sigstore/sigstore-js/") if err != nil { panic(err) @@ -240,4 +244,103 @@ func main() { } ``` +And here is a complete example of verifying a bundle signed with a key: + +```go +package main + +import ( + "crypto" + _ "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "os" + "time" + + "github.com/sigstore/sigstore/pkg/signature" + + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +type verifyTrustedMaterial struct { + root.TrustedMaterial + keyTrustedMaterial root.TrustedMaterial +} + +func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { + return v.keyTrustedMaterial.PublicKeyVerifier(hint) +} + +func main() { + b, err := bundle.LoadJSONFromPath("./examples/bundle-publish.json") + if err != nil { + panic(err) + } + + // This bundle uses public good instance with an added signing key + trustedRoot, err := root.FetchTrustedRoot() + if err != nil { + panic(err) + } + + keyData, err := os.ReadFile("examples/publish_key.pub") + if err != nil { + panic(err) + } + + block, _ := pem.Decode(keyData) + if block == nil { + panic("unable to PEM decode provided key") + } + + pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + panic(err) + } + + verifier, err := signature.LoadVerifier(pubKey, crypto.SHA256) + if err != nil { + panic(err) + } + + newExpiringKey := root.NewExpiringKey(verifier, time.Time{}, time.Time{}) + + trustedMaterial := &verifyTrustedMaterial{ + TrustedMaterial: trustedRoot, + keyTrustedMaterial: root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return newExpiringKey, nil + }), + } + + sev, err := verify.NewSignedEntityVerifier(trustedMaterial, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + if err != nil { + panic(err) + } + + digest, err := hex.DecodeString("76176ffa33808b54602c7c35de5c6e9a4deb96066dba6533f50ac234f4f1f4c6b3527515dc17c06fbe2860030f410eee69ea20079bd3a2c6f3dcf3b329b10751") + if err != nil { + panic(err) + } + + result, err := sev.Verify(b, verify.NewPolicy(verify.WithArtifactDigest("sha512", digest), verify.WithKey())) + if err != nil { + panic(err) + } + + fmt.Println("Verification successful!\n") + + marshaled, err := json.MarshalIndent(result, "", " ") + if err != nil { + panic(err) + } + + fmt.Println(string(marshaled)) +} +``` + To explore a more advanced/configurable verification process, see the CLI implementation in [`cmd/sigstore-go/main.go`](../cmd/sigstore-go/main.go). diff --git a/examples/bundle-publish.json b/examples/bundle-publish.json new file mode 100644 index 00000000..3e991849 --- /dev/null +++ b/examples/bundle-publish.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.1","verificationMaterial":{"publicKey":{"hint":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"},"tlogEntries":[{"logIndex":"18300940","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"intoto","version":"0.0.2"},"integratedTime":"1681839916","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDhoULWkbm7KZ4P4qAWHLw7d9X66AM/ZHNRvKgRahZg1gIhAILdjWLhlzSAy3XoP7sSFJKLwobemh2dtglhAXjSfEvA"},"inclusionProof":null,"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7ImtleWlkIjoiU0hBMjU2OmpsM2J3c3d1ODBQampva0NnaDBvMnc1YzJVNExoUUFFNTdnajljejFrekEiLCJwdWJsaWNLZXkiOiJMUzB0TFMxQ1JVZEpUaUJRVlVKTVNVTWdTMFZaTFMwdExTMEtUVVpyZDBWM1dVaExiMXBKZW1vd1EwRlJXVWxMYjFwSmVtb3dSRUZSWTBSUlowRkZNVTlzWWpONlRVRkdSbmhZUzBocFNXdFJUelZqU2pOWmFHdzFhVFpWVUhBclNXaDFkR1ZDU21KMVNHTkJOVlZ2WjB0dk1FVlhkR3hYZDFjMlMxTmhTMjlVVGtWWlREZEtiRU5SYVZadWEyaENhM1JWWjJjOVBRb3RMUzB0TFVWT1JDQlFWVUpNU1VNZ1MwVlpMUzB0TFMwPSIsInNpZyI6IlRVVlZRMGxDYm10bldWcFFTM3BWY21RNFEyeFJXVlZJTTJNM1FXSk9aVFkxVkVGMU1GVXZTMk5FWmxKQmFWTnlRV2xGUVhWelRua3lXVFZGU2pjM1MzRnhlVzB4SzFCdFdsRXlkMGhRT0hoc05EWTNkMmxDWmxBME9UQXliRVU5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImJjNWFlNjgxZTQ4Yjc1ZTAxN2MyNDdjNjRlY2Y0N2NkNDVjODVlNmNiNzY4ZjQzY2M0OGZhNmM0ZGVlMmFkYWMifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyNDViZDg2ODA0ZTQzM2M2MjEyYWUyYmQ4MGVjNzUwYmE0MWNjOWE0YTlkMTY3YWYyNzM4YzQ1MzI2MDgxOGE4In19fX0="}],"timestampVerificationData":null},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInN1YmplY3QiOlt7Im5hbWUiOiJwa2c6bnBtL3NpZ3N0b3JlQDEuMy4wIiwiZGlnZXN0Ijp7InNoYTUxMiI6Ijc2MTc2ZmZhMzM4MDhiNTQ2MDJjN2MzNWRlNWM2ZTlhNGRlYjk2MDY2ZGJhNjUzM2Y1MGFjMjM0ZjRmMWY0YzZiMzUyNzUxNWRjMTdjMDZmYmUyODYwMDMwZjQxMGVlZTY5ZWEyMDA3OWJkM2EyYzZmM2RjZjNiMzI5YjEwNzUxIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsInByZWRpY2F0ZSI6eyJuYW1lIjoic2lnc3RvcmUiLCJ2ZXJzaW9uIjoiMS4zLjAiLCJyZWdpc3RyeSI6Imh0dHBzOi8vcmVnaXN0cnkubnBtanMub3JnIn19","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEUCIBnkgYZPKzUrd8ClQYUH3c7AbNe65TAu0U/KcDfRAiSrAiEAusNy2Y5EJ77Kqqym1+PmZQ2wHP8xl467wiBfP4902lE=","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}]}} diff --git a/examples/publish_key.pub b/examples/publish_key.pub new file mode 100644 index 00000000..b3dec27e --- /dev/null +++ b/examples/publish_key.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg== +-----END PUBLIC KEY----- diff --git a/pkg/verify/certificate_identity.go b/pkg/verify/certificate_identity.go index b56296eb..1473c8ad 100644 --- a/pkg/verify/certificate_identity.go +++ b/pkg/verify/certificate_identity.go @@ -172,9 +172,5 @@ func (c CertificateIdentity) Verify(actualCert certificate.Summary) error { if err = c.SubjectAlternativeName.Verify(actualCert); err != nil { return err } - if err = certificate.CompareExtensions(c.Extensions, actualCert.Extensions); err != nil { - return err - } - - return nil + return certificate.CompareExtensions(c.Extensions, actualCert.Extensions) } diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go index 45bc014d..06d4323d 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -271,6 +271,7 @@ func (pc PolicyBuilder) BuildConfig() (*PolicyConfig, error) { type PolicyConfig struct { weDoNotExpectAnArtifact bool weDoNotExpectIdentities bool + weExpectSigningKey bool certificateIdentities CertificateIdentities verifyArtifact bool artifact io.Reader @@ -317,6 +318,12 @@ func (p *PolicyConfig) WeExpectIdentities() bool { return !p.weDoNotExpectIdentities } +// WeExpectSigningKey returns true if we expect the SignedEntity to be signed +// with a key and not a certificate. +func (p *PolicyConfig) WeExpectSigningKey() bool { + return p.weExpectSigningKey +} + func NewPolicy(artifactOpt ArtifactPolicyOption, options ...PolicyOption) PolicyBuilder { return PolicyBuilder{artifactPolicy: artifactOpt, policyOptions: options} } @@ -365,12 +372,29 @@ func WithCertificateIdentity(identity CertificateIdentity) PolicyOption { if p.weDoNotExpectIdentities { return errors.New("can't use WithCertificateIdentity while using WithoutIdentitiesUnsafe") } + if p.weExpectSigningKey { + return errors.New("can't use WithCertificateIdentity while using WithKey") + } p.certificateIdentities = append(p.certificateIdentities, identity) return nil } } +// WithKey allows the caller of Verify to require the SignedEntity being +// verified was signed with a key and not a certificate. +func WithKey() PolicyOption { + return func(p *PolicyConfig) error { + if len(p.certificateIdentities) > 0 { + return errors.New("can't use WithKey while using WithCertificateIdentity") + } + + p.weExpectSigningKey = true + p.weDoNotExpectIdentities = true + return nil + } +} + // WithoutArtifactUnsafe allows the caller of Verify to skip checking whether // the SignedEntity was created from, or references, an artifact. // @@ -495,6 +519,10 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V // If the bundle was signed with a long-lived key, and does not have a Fulcio certificate, // then skip the certificate verification steps if leafCert := verificationContent.GetCertificate(); leafCert != nil { + if policy.WeExpectSigningKey() { + return nil, errors.New("expected key signature, not certificate") + } + signedWithCertificate = true // From spec: diff --git a/pkg/verify/signed_entity_test.go b/pkg/verify/signed_entity_test.go index 5e0cc5d7..00193993 100644 --- a/pkg/verify/signed_entity_test.go +++ b/pkg/verify/signed_entity_test.go @@ -243,6 +243,16 @@ func TestVerifyPolicyOptionErors(t *testing.T) { assert.True(t, p.WeExpectAnArtifact()) assert.False(t, p.WeExpectIdentities()) + // --- + + noArtifactKeyHappyPath := verify.NewPolicy(verify.WithoutArtifactUnsafe(), verify.WithKey()) + p, err = noArtifactKeyHappyPath.BuildConfig() + assert.Nil(t, err) + assert.NotNil(t, p) + + assert.True(t, p.WeExpectSigningKey()) + assert.False(t, p.WeExpectIdentities()) + // let's exercise the different error cases! // 1. can't combine WithoutArtifactUnsafe with other Artifact options // technically a hack that requires casting but better safe than sorry: @@ -279,6 +289,11 @@ func TestVerifyPolicyOptionErors(t *testing.T) { _, err = verifier.Verify(entity, badIdentityPolicyCombo) assert.NotNil(t, err) + + // 5. can't expect certificate and key signature + badIdentityPolicyCombo2 := verify.NewPolicy(verify.WithoutArtifactUnsafe(), verify.WithCertificateIdentity(goodCertID), verify.WithKey()) + _, err = badIdentityPolicyCombo2.BuildConfig() + assert.NotNil(t, err) } func TestEntitySignedByPublicGoodWithCertificateIdentityVerifiesSuccessfully(t *testing.T) {