diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index 4a6d753cc8e..e17886088b8 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -112,7 +112,7 @@ type Signer struct { signature.SignerVerifier } -func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*Signer, error) { +func NewSignerWithAdapter(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier, fulcioSigner signature.SignerVerifier) (*Signer, error) { fClient, err := NewClient(ko.FulcioURL) if err != nil { return nil, fmt.Errorf("creating Fulcio client: %w", err) @@ -167,7 +167,7 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV } flow = flowNormal } - Resp, err := GetCert(ctx, signer, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. + Resp, err := GetCert(ctx, fulcioSigner, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. if err != nil { return nil, fmt.Errorf("retrieving cert: %w", err) } @@ -182,6 +182,10 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV return f, nil } +func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*Signer, error) { + return NewSignerWithAdapter(ctx, ko, signer, signer) +} + func (f *Signer) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) { //nolint: revive return f.SignerVerifier.PublicKey() } diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 8646bb298bd..1760b888625 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -26,8 +26,8 @@ import ( "github.com/sigstore/sigstore/pkg/signature" ) -func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*fulcio.Signer, error) { - fs, err := fulcio.NewSigner(ctx, ko, signer) +func NewSignerWithAdapter(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier, fulcioSigner signature.SignerVerifier) (*fulcio.Signer, error) { + fs, err := fulcio.NewSignerWithAdapter(ctx, ko, signer, fulcioSigner) if err != nil { return nil, err } @@ -46,3 +46,7 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV return fs, nil } + +func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*fulcio.Signer, error) { + return NewSignerWithAdapter(ctx, ko, signer, signer) +} diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb4..73223f3574a 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -138,7 +138,12 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout) defer cancel() - sv, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + svOptions := []signature.LoadOption{ + signatureoptions.WithHash(crypto.SHA256), + signatureoptions.WithED25519ph(), + } + + sv, err := signerFromKeyOptsWithSVOpts(ctx, signOpts.Cert, signOpts.CertChain, ko, svOptions...) if err != nil { return fmt.Errorf("getting signer: %w", err) } @@ -261,7 +266,12 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti if err != nil { return err } - s = irekor.NewSigner(s, rClient) + + hashAlgorithm, err := getHashAlgorithmFromSignerVerifier(sv) + if err != nil { + return err + } + s = irekor.NewSigner(s, rClient, hashAlgorithm) } ociSig, _, err := s.Sign(ctx, bytes.NewReader(payload)) @@ -391,8 +401,8 @@ func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier }, nil } -func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc) (*SignerVerifier, error) { - k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc) +func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, opts ...signature.LoadOption) (*SignerVerifier, error) { + k, err := sigs.SignerVerifierFromKeyRefWithOpts(ctx, keyRef, passFunc, opts...) if err != nil { return nil, fmt.Errorf("reading key: %w", err) } @@ -521,12 +531,12 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return certSigner, nil } -func signerFromNewKey() (*SignerVerifier, error) { +func signerFromNewKey(svOpts ...signature.LoadOption) (*SignerVerifier, error) { privKey, err := cosign.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("generating cert: %w", err) } - sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + sv, err := signature.LoadSignerVerifierWithOpts(privKey, svOpts...) if err != nil { return nil, err } @@ -542,12 +552,17 @@ func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) err error ) + fulcioSV, err := adaptSignerVerifierToFulcio(sv) + if err != nil { + return nil, fmt.Errorf("adapting signer verifier to Fulcio: %w", err) + } + if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { + if k, err = fulcio.NewSignerWithAdapter(ctx, ko, sv, fulcioSV); err != nil { return nil, fmt.Errorf("getting key from Fulcio: %w", err) } } else { - if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { + if k, err = fulcioverifier.NewSignerWithAdapter(ctx, ko, sv, fulcioSV); err != nil { return nil, fmt.Errorf("getting key from Fulcio: %w", err) } } @@ -559,7 +574,7 @@ func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) }, nil } -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, error) { +func signerFromKeyOptsWithSVOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts, svOpts ...signature.LoadOption) (*SignerVerifier, error) { var sv *SignerVerifier var err error genKey := false @@ -567,11 +582,11 @@ func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath strin case ko.Sk: sv, err = signerFromSecurityKey(ctx, ko.Slot) case ko.KeyRef != "": - sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc) + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, svOpts...) default: genKey = true ui.Infof(ctx, "Generating ephemeral keys...") - sv, err = signerFromNewKey() + sv, err = signerFromNewKey(svOpts...) } if err != nil { return nil, err @@ -584,6 +599,10 @@ func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath strin return sv, nil } +func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, error) { + return signerFromKeyOptsWithSVOpts(ctx, certPath, certChainPath, ko) +} + type SignerVerifier struct { Cert []byte Chain []byte @@ -610,6 +629,27 @@ func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) { return pemBytes, nil } +// adaptSignerVerifierToFulcio adapts, if necessary, the SignerVerifier to be used to interact with Fulcio. +// +// This is needed in particular for ED25519 keys with the pre-hashed version of +// the algorithm, which is not supported by Fulcio. This function creates a +// ED25519 SignerVerifier based on that instead. +func adaptSignerVerifierToFulcio(sv *SignerVerifier) (*SignerVerifier, error) { + if ed25519phSV, ok := sv.SignerVerifier.(*signature.ED25519phSignerVerifier); ok { + signerVerifier, err := ed25519phSV.ToED25519SignerVerifier() + if err != nil { + return nil, err + } + + return &SignerVerifier{ + SignerVerifier: signerVerifier, + Cert: sv.Cert, + Chain: sv.Chain, + }, nil + } + return sv, nil +} + func fetchLocalSignedPayload(sig oci.Signature) (*cosign.LocalSignedPayload, error) { signedPayload := &cosign.LocalSignedPayload{} var err error diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 5a7b960b927..ecf7abf4c20 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -17,6 +17,10 @@ package sign import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" @@ -39,9 +43,28 @@ import ( protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) +func getHashAlgorithmFromSignerVerifier(sv *SignerVerifier) (crypto.Hash, error) { + publicKey, err := sv.SignerVerifier.PublicKey() + if err != nil { + return crypto.Hash(0), err + } + + switch publicKey.(type) { + case *ecdsa.PublicKey: + return crypto.SHA256, nil + case *rsa.PublicKey: + return crypto.SHA256, nil + case ed25519.PublicKey: + return crypto.SHA512, nil + default: + return crypto.Hash(0), fmt.Errorf("unsupported public key type") + } +} + // nolint func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) ([]byte, error) { var payload internal.HashReader @@ -50,8 +73,30 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout) defer cancel() + svOptions := []signature.LoadOption{ + signatureoptions.WithHash(crypto.SHA256), + } + // Use ED25519 pre-hashed version only when uploading to tlog to maintain + // backwards compatibility. When self-managed keys are used this keeps the + // behavior consistent with older cosign clients, which will still be able + // to verify the newer signatures. + if tlogUpload { + svOptions = append(svOptions, signatureoptions.WithED25519ph()) + } + + sv, err := signerFromKeyOptsWithSVOpts(ctx, "", "", ko, svOptions...) + if err != nil { + return nil, err + } + defer sv.Close() + + hashAlgorithm, err := getHashAlgorithmFromSignerVerifier(sv) + if err != nil { + return nil, err + } + if payloadPath == "-" { - payload = internal.NewHashReader(os.Stdin, sha256.New()) + payload = internal.NewHashReader(os.Stdin, hashAlgorithm) } else { ui.Infof(ctx, "Using payload from: %s", payloadPath) f, err := os.Open(filepath.Clean(payloadPath)) @@ -59,18 +104,12 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string if err != nil { return nil, err } - payload = internal.NewHashReader(f, sha256.New()) + payload = internal.NewHashReader(f, hashAlgorithm) } if err != nil { return nil, err } - sv, err := SignerFromKeyOpts(ctx, "", "", ko) - if err != nil { - return nil, err - } - defer sv.Close() - sig, err := sv.SignMessage(&payload, signatureoptions.WithContext(ctx)) if err != nil { return nil, fmt.Errorf("signing blob: %w", err) @@ -135,7 +174,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string if err != nil { return nil, err } - rekorEntry, err = cosign.TLogUpload(ctx, rekorClient, sig, &payload, rekorBytes) + rekorEntry, err = cosign.TLogUploadWithCustomHash(ctx, rekorClient, sig, &payload, rekorBytes) if err != nil { return nil, err } diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index f5fc86bfddf..abf30d50557 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -43,6 +43,7 @@ import ( sigs "github.com/sigstore/cosign/v2/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -182,11 +183,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } + svOpts := []signature.LoadOption{ + signatureoptions.WithHash(crypto.SHA256), + signatureoptions.WithED25519ph(), + } + // Keys are optional! var pubKey signature.Verifier switch { case keyRef != "": - pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) + pubKey, err = sigs.PublicKeyFromKeyRefWithOpts(ctx, keyRef, svOpts...) if err != nil { return fmt.Errorf("loading public key: %w", err) } @@ -220,7 +226,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return fmt.Errorf("getting Fulcio intermediates: %w", err) } - pubKey, err = cosign.ValidateAndUnpackCert(cert, co) + pubKey, err = cosign.ValidateAndUnpackCertWithOpts(cert, co, cosign.WithSignerVerifierOptions(svOpts...)) if err != nil { return err } @@ -230,7 +236,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + pubKey, err = cosign.ValidateAndUnpackCertWithOpts(cert, co, cosign.WithChain(chain), cosign.WithSignerVerifierOptions(svOpts...)) if err != nil { return err } @@ -267,7 +273,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { for _, img := range images { if c.LocalImage { - verified, bundleVerified, err := cosign.VerifyLocalImageSignatures(ctx, img, co) + verified, bundleVerified, err := cosign.VerifyLocalImageSignaturesWithOpts(ctx, img, co, svOpts...) if err != nil { return err } @@ -283,7 +289,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("resolving attachment type %s for image %s: %w", c.Attachment, img, err) } - verified, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) + verified, bundleVerified, err := cosign.VerifyImageSignaturesWithOpts(ctx, ref, co, svOpts...) if err != nil { return cosignError.WrapError(err) } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index bf97f9e2a86..ff670443e45 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -40,6 +40,8 @@ import ( sigs "github.com/sigstore/cosign/v2/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) func isb64(data []byte) bool { @@ -166,10 +168,15 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } + svOpts := []signature.LoadOption{ + signatureoptions.WithHash(crypto.SHA256), + signatureoptions.WithED25519ph(), + } + // Keys are optional! switch { case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) + co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithOpts(ctx, c.KeyRef, svOpts...) if err != nil { return fmt.Errorf("loading public key: %w", err) } @@ -214,7 +221,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { bundleCert, err := loadCertFromPEM(certBytes) if err != nil { // check if cert is actually a public key - co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) + co.SigVerifier, err = sigs.LoadPublicKeyRawWithOpts(certBytes, svOpts...) if err != nil { return fmt.Errorf("loading verifier from bundle: %w", err) } @@ -297,7 +304,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { if err != nil { return err } - if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { + if _, err = cosign.VerifyBlobSignatureWithOpts(ctx, signature, co, svOpts...); err != nil { return err } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 9dd7629ade0..7b1580ed123 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -18,7 +18,6 @@ package verify import ( "context" "crypto" - "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" @@ -143,7 +142,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st return err } - payload = internal.NewHashReader(f, sha256.New()) + payload = internal.NewHashReader(f, crypto.SHA256) if _, err := io.ReadAll(&payload); err != nil { return err } diff --git a/go.mod b/go.mod index 78540992179..1f962c80947 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/sigstore/fulcio v1.6.6 github.com/sigstore/protobuf-specs v0.4.0 github.com/sigstore/rekor v1.3.9 - github.com/sigstore/sigstore v1.8.12 + github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7 github.com/sigstore/sigstore-go v0.7.0 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 @@ -69,7 +69,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.2.2 // indirect - cloud.google.com/go/kms v1.20.4 // indirect + cloud.google.com/go/kms v1.20.5 // indirect cloud.google.com/go/longrunning v0.6.2 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -255,7 +255,7 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.step.sm/crypto v0.57.0 // indirect + go.step.sm/crypto v0.57.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect @@ -279,3 +279,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/sigstore/rekor => github.com/trail-of-forks/rekor v0.0.0-20250205163412-8fe29f9a710f diff --git a/go.sum b/go.sum index 1489156cf11..53c9dfcc1ae 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A= -cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc= +cloud.google.com/go/kms v1.20.5 h1:aQQ8esAIVZ1atdJRxihhdxGQ64/zEbJoJnCz/ydSmKg= +cloud.google.com/go/kms v1.20.5/go.mod h1:C5A8M1sv2YWYy1AE6iSrnddSG9lRGdJq5XEdBy28Lmw= cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1 h1:mRwydyTyhtRX2wXS3mqYWzR2qlv6KsmoKXmlz5vInjg= @@ -622,10 +622,8 @@ github.com/sigstore/fulcio v1.6.6 h1:XaMYX6TNT+8n7Npe8D94nyZ7/ERjEsNGFC+REdi/wzw github.com/sigstore/fulcio v1.6.6/go.mod h1:BhQ22lwaebDgIxVBEYOOqLRcN5+xOV+C9bh/GUXRhOk= github.com/sigstore/protobuf-specs v0.4.0 h1:yoZbdh0kZYKOSiVbYyA8J3f2wLh5aUk2SQB7LgAfIdU= github.com/sigstore/protobuf-specs v0.4.0/go.mod h1:FKW5NYhnnFQ/Vb9RKtQk91iYd0MKJ9AxyqInEwU6+OI= -github.com/sigstore/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU= -github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM= -github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc= -github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= +github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7 h1:dKgngAj90pDWGKB/yBt/AmSvNFzLOPvYGfEgEKRQWc4= +github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7/go.mod h1:2lXojNsjZjkqu1//FWxq7qUcPB8Lq1KsR5hc+GkcC/4= github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48= github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE= @@ -698,6 +696,8 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHT github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/trail-of-forks/rekor v0.0.0-20250205163412-8fe29f9a710f h1:YQB4Uk5B6CSMEHUvN9+R8G26gccayvZLnnF74b3SstY= +github.com/trail-of-forks/rekor v0.0.0-20250205163412-8fe29f9a710f/go.mod h1:bt8ddrKNlDb/Ub7XYuJPx/2Ro2OJwVesMhBHxQ81OC0= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= @@ -758,8 +758,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU= -go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go= +go.step.sm/crypto v0.57.1 h1:bt7ugfc0m2/nJ9/uhQOtXRW3xQr8zJwL087FLQk9mvc= +go.step.sm/crypto v0.57.1/go.mod h1:wL25/Mh7edmo36AA93hf9agP493Zt3y4QBzB1wzwOjc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/internal/pkg/cosign/common.go b/internal/pkg/cosign/common.go index a1aa8ebc35f..44027402e24 100644 --- a/internal/pkg/cosign/common.go +++ b/internal/pkg/cosign/common.go @@ -15,6 +15,7 @@ package cosign import ( + "crypto" "errors" "hash" "io" @@ -39,14 +40,17 @@ func FileExists(filename string) (bool, error) { // HashReader hashes while it reads. type HashReader struct { - r io.Reader - h hash.Hash + r io.Reader + h hash.Hash + ch crypto.Hash } -func NewHashReader(r io.Reader, h hash.Hash) HashReader { +func NewHashReader(r io.Reader, ch crypto.Hash) HashReader { + h := ch.New() return HashReader{ - r: io.TeeReader(r, h), - h: h, + r: io.TeeReader(r, h), + h: h, + ch: ch, } } @@ -67,3 +71,6 @@ func (h *HashReader) BlockSize() int { return h.h.BlockSize() } // Write implements hash.Hash func (h *HashReader) Write(p []byte) (int, error) { return 0, errors.New("not implemented") } //nolint: revive + +// HashFunc implements cosign.NamedHash +func (h *HashReader) HashFunc() crypto.Hash { return h.ch } diff --git a/internal/pkg/cosign/common_test.go b/internal/pkg/cosign/common_test.go index 4a54109e435..4f38e07865c 100644 --- a/internal/pkg/cosign/common_test.go +++ b/internal/pkg/cosign/common_test.go @@ -17,6 +17,7 @@ package cosign import ( "bytes" + "crypto" "crypto/sha256" "io" "os" @@ -55,7 +56,7 @@ func Test_FileExists(t *testing.T) { func Test_HashReader(t *testing.T) { input := []byte("hello world") - r := NewHashReader(bytes.NewReader(input), sha256.New()) + r := NewHashReader(bytes.NewReader(input), crypto.SHA256) got, err := io.ReadAll(&r) if err != nil { diff --git a/internal/pkg/cosign/rekor/signer.go b/internal/pkg/cosign/rekor/signer.go index 2fa5e2595ef..612f65c6710 100644 --- a/internal/pkg/cosign/rekor/signer.go +++ b/internal/pkg/cosign/rekor/signer.go @@ -17,7 +17,6 @@ package rekor import ( "context" "crypto" - "crypto/sha256" "encoding/base64" "fmt" "io" @@ -49,7 +48,8 @@ func uploadToTlog(rekorBytes []byte, rClient *client.Rekor, upload tlogUploadFn) type signerWrapper struct { inner cosign.Signer - rClient *client.Rekor + rClient *client.Rekor + hashAlgorithm crypto.Hash } var _ cosign.Signer = (*signerWrapper)(nil) @@ -91,11 +91,11 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa } bundle, err := uploadToTlog(rekorBytes, rs.rClient, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - checkSum := sha256.New() + checkSum := cosignv1.NewCryptoNamedHash(rs.hashAlgorithm) if _, err := checkSum.Write(payloadBytes); err != nil { return nil, err } - return cosignv1.TLogUpload(ctx, r, sigBytes, checkSum, b) + return cosignv1.TLogUploadWithCustomHash(ctx, r, sigBytes, checkSum, b) }) if err != nil { return nil, nil, err @@ -110,9 +110,10 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa } // NewSigner returns a `cosign.Signer` which uploads the signature to Rekor -func NewSigner(inner cosign.Signer, rClient *client.Rekor) cosign.Signer { +func NewSigner(inner cosign.Signer, rClient *client.Rekor, hashAlgorithm crypto.Hash) cosign.Signer { return &signerWrapper{ - inner: inner, - rClient: rClient, + inner: inner, + rClient: rClient, + hashAlgorithm: hashAlgorithm, } } diff --git a/internal/pkg/cosign/rekor/signer_test.go b/internal/pkg/cosign/rekor/signer_test.go index 5f3dfa02351..7d6adbd0d9a 100644 --- a/internal/pkg/cosign/rekor/signer_test.go +++ b/internal/pkg/cosign/rekor/signer_test.go @@ -56,7 +56,7 @@ func TestSigner(t *testing.T) { }}}, } - testSigner := NewSigner(payloadSigner, &mClient) + testSigner := NewSigner(payloadSigner, &mClient, crypto.SHA256) testPayload := "test payload" diff --git a/pkg/cosign/keys.go b/pkg/cosign/keys.go index ed5bbc16d4b..527cdc5d174 100644 --- a/pkg/cosign/keys.go +++ b/pkg/cosign/keys.go @@ -210,6 +210,10 @@ func PemToECDSAKey(pemBytes []byte) (*ecdsa.PublicKey, error) { // LoadPrivateKey loads a cosign PEM private key encrypted with the given passphrase, // and returns a SignerVerifier instance. The private key must be in the PKCS #8 format. func LoadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { + return LoadPrivateKeyWithOpts(key, pass) +} + +func LoadPrivateKeyWithOpts(key []byte, pass []byte, opts ...signature.LoadOption) (signature.SignerVerifier, error) { // Decrypt first p, _ := pem.Decode(key) if p == nil { @@ -227,14 +231,5 @@ func LoadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { if err != nil { return nil, fmt.Errorf("parsing private key: %w", err) } - switch pk := pk.(type) { - case *rsa.PrivateKey: - return signature.LoadRSAPKCS1v15SignerVerifier(pk, crypto.SHA256) - case *ecdsa.PrivateKey: - return signature.LoadECDSASignerVerifier(pk, crypto.SHA256) - case ed25519.PrivateKey: - return signature.LoadED25519SignerVerifier(pk) - default: - return nil, errors.New("unsupported key type") - } + return signature.LoadSignerVerifierWithOpts(pk, opts...) } diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 83d6f61f179..fd445837082 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -54,6 +54,24 @@ import ( // This is the rekor transparency log public key target name var rekorTargetStr = `rekor.pub` +type NamedHash interface { + hash.Hash + crypto.SignerOpts +} + +type CryptoNamedHash struct { + hash.Hash + hashType crypto.Hash +} + +func (h CryptoNamedHash) HashFunc() crypto.Hash { + return h.hashType +} + +func NewCryptoNamedHash(hashType crypto.Hash) NamedHash { + return CryptoNamedHash{Hash: hashType.New(), hashType: hashType} +} + // TransparencyLogPubKey contains the ECDSA verification key and the current status // of the key according to TUF metadata, whether it's active or expired. type TransparencyLogPubKey struct { @@ -169,9 +187,28 @@ func rekorPubsFromClient(rekorClient *client.Rekor) (*TrustedTransparencyLogPubK return &publicKeys, nil } +type SHA256NamedHash struct { + hash.Hash +} + +func (h SHA256NamedHash) HashFunc() crypto.Hash { + return crypto.SHA256 +} + +func WrapSHA256Hash(hash hash.Hash) NamedHash { + return SHA256NamedHash{Hash: hash} +} + // TLogUpload will upload the signature, public key and payload to the transparency log. func TLogUpload(ctx context.Context, rekorClient *client.Rekor, signature []byte, sha256CheckSum hash.Hash, pemBytes []byte) (*models.LogEntryAnon, error) { - re := rekorEntry(sha256CheckSum, signature, pemBytes) + cryptoChecksum := WrapSHA256Hash(sha256CheckSum) + return TLogUploadWithCustomHash(ctx, rekorClient, signature, cryptoChecksum, pemBytes) +} + +// TLogUploadWithCustomHash will upload the signature, public key and payload to +// the transparency log. Clients can use this to specify a custom hash function. +func TLogUploadWithCustomHash(ctx context.Context, rekorClient *client.Rekor, signature []byte, checksum NamedHash, pemBytes []byte) (*models.LogEntryAnon, error) { + re := rekorEntry(checksum, signature, pemBytes) returnVal := models.Hashedrekord{ APIVersion: swag.String(re.APIVersion()), Spec: re.HashedRekordObj, @@ -230,16 +267,26 @@ func doUpload(ctx context.Context, rekorClient *client.Rekor, pe models.Proposed return nil, errors.New("bad response from server") } -func rekorEntry(sha256CheckSum hash.Hash, signature, pubKey []byte) hashedrekord_v001.V001Entry { - // TODO: Signatures created on a digest using a hash algorithm other than SHA256 will fail - // upload right now. Plumb information on the hash algorithm used when signing from the - // SignerVerifier to use for the HashedRekordObj.Data.Hash.Algorithm. +func rekorEntryHashAlgorithm(checksum crypto.SignerOpts) string { + switch checksum.HashFunc() { + case crypto.SHA256: + return models.HashedrekordV001SchemaDataHashAlgorithmSha256 + case crypto.SHA384: + return models.HashedrekordV001SchemaDataHashAlgorithmSha384 + case crypto.SHA512: + return models.HashedrekordV001SchemaDataHashAlgorithmSha512 + default: + return models.HashedrekordV001SchemaDataHashAlgorithmSha256 + } +} + +func rekorEntry(checksum NamedHash, signature, pubKey []byte) hashedrekord_v001.V001Entry { return hashedrekord_v001.V001Entry{ HashedRekordObj: models.HashedrekordV001Schema{ Data: &models.HashedrekordV001SchemaData{ Hash: &models.HashedrekordV001SchemaDataHash{ - Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), - Value: swag.String(hex.EncodeToString(sha256CheckSum.Sum(nil))), + Algorithm: swag.String(rekorEntryHashAlgorithm(checksum)), + Value: swag.String(hex.EncodeToString(checksum.Sum(nil))), }, }, Signature: &models.HashedrekordV001SchemaSignature{ @@ -392,7 +439,7 @@ func proposedEntries(b64Sig string, payload, pubKey []byte) ([]models.ProposedEn } proposedEntry = []models.ProposedEntry{dsseEntry, intotoEntry} } else { - sha256CheckSum := sha256.New() + sha256CheckSum := NewCryptoNamedHash(crypto.SHA256) if _, err := sha256CheckSum.Write(payload); err != nil { return nil, err } diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3a6ee79b461..b821a5e4ea1 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -19,7 +19,9 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/sha256" + "crypto/sha512" "crypto/x509" "encoding/asn1" "encoding/base64" @@ -167,6 +169,60 @@ type CheckOpts struct { ExperimentalOCI11 bool } +type validateCertOpts struct { + chain []*x509.Certificate + svOpts []signature.LoadOption + pool *x509.CertPool +} + +type ValidateAndUnpackCertOption func(*validateCertOpts) + +func WithChain(chain []*x509.Certificate) ValidateAndUnpackCertOption { + return func(o *validateCertOpts) { + o.chain = chain + } +} + +func WithPool(pool *x509.CertPool) ValidateAndUnpackCertOption { + return func(o *validateCertOpts) { + o.pool = pool + } +} + +func WithSignerVerifierOptions(svOpts ...signature.LoadOption) ValidateAndUnpackCertOption { + return func(o *validateCertOpts) { + o.svOpts = svOpts + } +} + +func makeValidateAndUnpackCertOpts(co *CheckOpts, opts ...ValidateAndUnpackCertOption) (*validateCertOpts, error) { + o := &validateCertOpts{} + for _, opt := range opts { + opt(o) + } + + // Pool Option has precedence over chain option + if o.pool == nil && o.chain != nil { + if len(o.chain) == 0 { + return nil, errors.New("no chain provided to validate certificate") + } + rootPool := x509.NewCertPool() + rootPool.AddCert(o.chain[len(o.chain)-1]) + co.RootCerts = rootPool + + subPool := x509.NewCertPool() + for _, c := range o.chain[:len(o.chain)-1] { + subPool.AddCert(c) + } + o.pool = subPool + } + // If no pool or chain is provided, use the one from the CheckOpts + if o.pool == nil { + o.pool = co.IntermediateCerts + } + return o, nil +} + // This is a substitutable signature verification function that can be used for verifying // attestations of blobs. type signatureVerificationFn func( @@ -208,7 +264,7 @@ func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig pa if err != nil { return err } - signature, err := base64.StdEncoding.DecodeString(b64sig) + decodedSignature, err := base64.StdEncoding.DecodeString(b64sig) if err != nil { return err } @@ -216,21 +272,66 @@ func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig pa if err != nil { return err } - return verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)) + // For compatibility reasons, if ED25519ph is used, we try both ED25519 and ED25519ph. + // Refusing to verify ED25519 signatures (used e.g. by rekord entries) would break compatibility. + // The signature algorithm to use should be uniquely determined before this point. + verificationErr := verifier.VerifySignature(bytes.NewReader(decodedSignature), bytes.NewReader(payload), options.WithContext(ctx)) + if verificationErr == nil { + return nil + } + + switch verifier.(type) { + case *signature.ED25519phVerifier: + publicKey, err := verifier.PublicKey() + if err != nil { + return err + } + + if edPublicKey, ok := publicKey.(ed25519.PublicKey); ok { + altVerifier, err := signature.LoadED25519Verifier(edPublicKey) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Failed to verify signature with ED25519ph, falling back to ED25519 for backward compatibility.\n") + verificationErr = altVerifier.VerifySignature(bytes.NewReader(decodedSignature), bytes.NewReader(payload), options.WithContext(ctx)) + } + } + + return verificationErr } // ValidateAndUnpackCert creates a Verifier from a certificate. Verifies that the // certificate chains up to a trusted root using intermediate certificate chain coming from CheckOpts. // Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { - return ValidateAndUnpackCertWithIntermediates(cert, co, co.IntermediateCerts) + return ValidateAndUnpackCertWithOpts(cert, co) } // ValidateAndUnpackCertWithIntermediates creates a Verifier from a certificate. Verifies that the // certificate chains up to a trusted root using intermediate cert passed as separate argument. // Optionally verifies the subject and issuer of the certificate. func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpts, intermediateCerts *x509.CertPool) (signature.Verifier, error) { - verifier, err := signature.LoadVerifier(cert.PublicKey, crypto.SHA256) + return ValidateAndUnpackCertWithOpts(cert, co, WithPool(intermediateCerts)) +} + +// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate +// chains up to the provided root. Chain should start with the parent of the certificate and end with the root. +// Optionally verifies the subject and issuer of the certificate. +func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { + return ValidateAndUnpackCertWithOpts(cert, co, WithChain(chain)) +} + +// ValidateAndUnpackCertWithOpts creates a Verifier from a certificate. Verifies that the certificate +// chains up to a trusted root. Optionally verifies the subject and issuer of the certificate. +// Accept chain and signerVerifierOptions as optional parameters. +func ValidateAndUnpackCertWithOpts(cert *x509.Certificate, co *CheckOpts, opts ...ValidateAndUnpackCertOption) (signature.Verifier, error) { + o, err := makeValidateAndUnpackCertOpts(co, opts...) + if err != nil { + return nil, err + } + + verifier, err := signature.LoadVerifierWithOpts(cert.PublicKey, o.svOpts...) if err != nil { return nil, fmt.Errorf("invalid certificate found on signature: %w", err) } @@ -250,7 +351,7 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt } // Now verify the cert, then the signature. - chains, err := TrustedCert(cert, co.RootCerts, intermediateCerts) + chains, err := TrustedCert(cert, co.RootCerts, o.pool) if err != nil { return nil, err @@ -415,26 +516,6 @@ func validateCertExtensions(ce CertExtensions, co *CheckOpts) error { return nil } -// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate -// chains up to the provided root. Chain should start with the parent of the certificate and end with the root. -// Optionally verifies the subject and issuer of the certificate. -func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { - if len(chain) == 0 { - return nil, errors.New("no chain provided to validate certificate") - } - rootPool := x509.NewCertPool() - rootPool.AddCert(chain[len(chain)-1]) - co.RootCerts = rootPool - - subPool := x509.NewCertPool() - for _, c := range chain[:len(chain)-1] { - subPool.AddCert(c) - } - co.IntermediateCerts = subPool - - return ValidateAndUnpackCert(cert, co) -} - func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() @@ -489,9 +570,13 @@ func (fos *fakeOCISignatures) Get() ([]oci.Signature, error) { // Note that if co.ExperimentlOCI11 is set, we will attempt to verify // signatures using the experimental OCI 1.1 behavior. func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return VerifyImageSignaturesWithOpts(ctx, signedImgRef, co) +} + +func VerifyImageSignaturesWithOpts(ctx context.Context, signedImgRef name.Reference, co *CheckOpts, svOpts ...signature.LoadOption) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { // Try first using OCI 1.1 behavior if experimental flag is set. if co.ExperimentalOCI11 { - verified, bundleVerified, err := verifyImageSignaturesExperimentalOCI(ctx, signedImgRef, co) + verified, bundleVerified, err := verifyImageSignaturesExperimentalOCI(ctx, signedImgRef, co, svOpts...) if err == nil { return verified, bundleVerified, nil } @@ -536,12 +621,18 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co } } - return verifySignatures(ctx, sigs, h, co) + return verifySignatures(ctx, sigs, h, co, svOpts...) } // VerifyLocalImageSignatures verifies signatures from a saved, local image, without any network calls, returning the verified signatures. // If there were no valid signatures, we return an error. func VerifyLocalImageSignatures(ctx context.Context, path string, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return VerifyLocalImageSignaturesWithOpts(ctx, path, co) +} + +// VerifyLocalImageSignaturesWithOpts verifies signatures from a saved, local image, without any network calls, returning the verified signatures. +// If there were no valid signatures, we return an error. +func VerifyLocalImageSignaturesWithOpts(ctx context.Context, path string, co *CheckOpts, svOpts ...signature.LoadOption) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { // Enforce this up front. if co.RootCerts == nil && co.SigVerifier == nil { return nil, false, errors.New("one of verifier or root certs is required") @@ -585,10 +676,10 @@ func VerifyLocalImageSignatures(ctx context.Context, path string, co *CheckOpts) return nil, false, fmt.Errorf("no signatures associated with the image saved in %s", path) } - return verifySignatures(ctx, sigs, h, co) + return verifySignatures(ctx, sigs, h, co, svOpts...) } -func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { +func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *CheckOpts, svOpts ...signature.LoadOption) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { sl, err := sigs.Get() if err != nil { return nil, false, err @@ -616,7 +707,7 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C return } - verified, err := VerifyImageSignature(ctx, sig, h, co) + verified, err := VerifyImageSignatureWithOpts(ctx, sig, h, co, svOpts...) bundlesVerified[index] = verified if err != nil { t.Done(err) @@ -664,7 +755,7 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C // we are in experimental mode). // 3. If a certificate is provided, check it's expiration using the transparency log timestamp. func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, - verifyFn signatureVerificationFn, co *CheckOpts) ( + verifyFn signatureVerificationFn, co *CheckOpts, svOpts ...signature.LoadOption) ( bundleVerified bool, err error) { var acceptableRFC3161Time, acceptableRekorBundleTime *time.Time // Timestamps for the signature we accept, or nil if not applicable. @@ -751,7 +842,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, if pool == nil { pool = co.IntermediateCerts } - verifier, err = ValidateAndUnpackCertWithIntermediates(cert, co, pool) + verifier, err = ValidateAndUnpackCertWithOpts(cert, co, WithPool(pool), WithSignerVerifierOptions(svOpts...)) if err != nil { return false, err } @@ -828,13 +919,23 @@ func keyBytes(sig oci.Signature, co *CheckOpts) ([]byte, error) { // VerifyBlobSignature verifies a blob signature. func VerifyBlobSignature(ctx context.Context, sig oci.Signature, co *CheckOpts) (bundleVerified bool, err error) { + return VerifyBlobSignatureWithOpts(ctx, sig, co) +} + +// VerifyBlobSignature verifies a blob signature. +func VerifyBlobSignatureWithOpts(ctx context.Context, sig oci.Signature, co *CheckOpts, svOpts ...signature.LoadOption) (bundleVerified bool, err error) { // The hash of the artifact is unused. - return verifyInternal(ctx, sig, v1.Hash{}, verifyOCISignature, co) + return verifyInternal(ctx, sig, v1.Hash{}, verifyOCISignature, co, svOpts...) } // VerifyImageSignature verifies a signature func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) { - return verifyInternal(ctx, sig, h, verifyOCISignature, co) + return VerifyImageSignatureWithOpts(ctx, sig, h, co) +} + +// VerifyImageSignature verifies a signature +func VerifyImageSignatureWithOpts(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts, svOpts ...signature.LoadOption) (bundleVerified bool, err error) { + return verifyInternal(ctx, sig, h, verifyOCISignature, co, svOpts...) } func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name.Reference, co *CheckOpts) (oci.Signatures, error) { @@ -1110,12 +1211,23 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { if err != nil { return false, fmt.Errorf("computing bundle hash: %w", err) } - h := sha256.Sum256(payload) - payloadHash := hex.EncodeToString(h[:]) - if alg != "sha256" { + var payloadHash string + switch alg.HashFunc() { + case crypto.SHA256: + h := sha256.Sum256(payload) + payloadHash = hex.EncodeToString(h[:]) + case crypto.SHA384: + h := sha512.Sum384(payload) + payloadHash = hex.EncodeToString(h[:]) + case crypto.SHA512: + h := sha512.Sum512(payload) + payloadHash = hex.EncodeToString(h[:]) + default: return false, fmt.Errorf("unexpected algorithm: %q", alg) - } else if bundlehash != payloadHash { + } + + if bundlehash != payloadHash { return false, fmt.Errorf("matching bundle to payload: bundle=%q, payload=%q", bundlehash, payloadHash) } return true, nil @@ -1236,25 +1348,36 @@ func extractEntryImpl(bundleBody string) (rekor_types.EntryImpl, error) { return rekor_types.UnmarshalEntry(pe) } -func bundleHash(bundleBody, _ string) (string, string, error) { +func HashAlgorithmToCryptoHash(hashAlgorithm string) crypto.Hash { + switch hashAlgorithm { + case "sha384": + return crypto.SHA384 + case "sha512": + return crypto.SHA512 + default: + return crypto.SHA256 + } +} + +func bundleHash(bundleBody, _ string) (crypto.Hash, string, error) { ei, err := extractEntryImpl(bundleBody) if err != nil { - return "", "", err + return crypto.Hash(0), "", err } switch entry := ei.(type) { case *dsse_v001.V001Entry: - return *entry.DSSEObj.EnvelopeHash.Algorithm, *entry.DSSEObj.EnvelopeHash.Value, nil + return HashAlgorithmToCryptoHash(*entry.DSSEObj.EnvelopeHash.Algorithm), *entry.DSSEObj.EnvelopeHash.Value, nil case *hashedrekord_v001.V001Entry: - return *entry.HashedRekordObj.Data.Hash.Algorithm, *entry.HashedRekordObj.Data.Hash.Value, nil + return HashAlgorithmToCryptoHash(*entry.HashedRekordObj.Data.Hash.Algorithm), *entry.HashedRekordObj.Data.Hash.Value, nil case *intoto_v001.V001Entry: - return *entry.IntotoObj.Content.Hash.Algorithm, *entry.IntotoObj.Content.Hash.Value, nil + return HashAlgorithmToCryptoHash(*entry.IntotoObj.Content.Hash.Algorithm), *entry.IntotoObj.Content.Hash.Value, nil case *intoto_v002.V002Entry: - return *entry.IntotoObj.Content.Hash.Algorithm, *entry.IntotoObj.Content.Hash.Value, nil + return HashAlgorithmToCryptoHash(*entry.IntotoObj.Content.Hash.Algorithm), *entry.IntotoObj.Content.Hash.Value, nil case *rekord_v001.V001Entry: - return *entry.RekordObj.Data.Hash.Algorithm, *entry.RekordObj.Data.Hash.Value, nil + return HashAlgorithmToCryptoHash(*entry.RekordObj.Data.Hash.Algorithm), *entry.RekordObj.Data.Hash.Value, nil default: - return "", "", errors.New("unsupported type") + return crypto.Hash(0), "", errors.New("unsupported type") } } @@ -1363,7 +1486,7 @@ func correctAnnotations(wanted, have map[string]interface{}) bool { // verifyImageSignaturesExperimentalOCI does all the main cosign checks in a loop, returning the verified signatures. // If there were no valid signatures, we return an error, using OCI 1.1+ behavior. -func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { +func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name.Reference, co *CheckOpts, svOpts ...signature.LoadOption) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { // Enforce this up front. if co.RootCerts == nil && co.SigVerifier == nil { return nil, false, errors.New("one of verifier or root certs is required") @@ -1416,5 +1539,5 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name } } - return verifySignatures(ctx, sigs, h, co) + return verifySignatures(ctx, sigs, h, co, svOpts...) } diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index dfac964725d..3be333a90c9 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -31,6 +31,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" + "github.com/sigstore/sigstore/pkg/signature/options" ) // LoadPublicKey is a wrapper for VerifierForKeyRef, hardcoding SHA256 as the hash algorithm @@ -41,7 +42,18 @@ func LoadPublicKey(ctx context.Context, keyRef string) (verifier signature.Verif // VerifierForKeyRef parses the given keyRef, loads the key and returns an appropriate // verifier using the provided hash algorithm func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto.Hash) (verifier signature.Verifier, err error) { + return VerifierForKeyRefWithOpts(ctx, keyRef, options.WithHash(hashAlgorithm)) +} + +// VerifierForKeyRefWithOpts parses the given keyRef, loads the key and returns an appropriate +// verifier using the provided hash algorithm and options +func VerifierForKeyRefWithOpts(ctx context.Context, keyRef string, opts ...signature.LoadOption) (verifier signature.Verifier, err error) { // The key could be plaintext, in a file, at a URL, or in KMS. + hashAlgorithm := crypto.SHA256 + for _, o := range opts { + o.ApplyHash(&hashAlgorithm) + } + var perr *kms.ProviderNotFoundError kmsKey, err := kms.Get(ctx, keyRef, hashAlgorithm) switch { @@ -69,10 +81,10 @@ func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto. return nil, fmt.Errorf("pem to public key: %w", err) } - return signature.LoadVerifier(pubKey, hashAlgorithm) + return signature.LoadVerifierWithOpts(pubKey, opts...) } -func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, error) { +func loadKey(keyPath string, pf cosign.PassFunc, opts ...signature.LoadOption) (signature.SignerVerifier, error) { kb, err := blob.LoadFileOrURL(keyPath) if err != nil { return nil, err @@ -84,16 +96,21 @@ func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, erro return nil, err } } - return cosign.LoadPrivateKey(kb, pass) + return cosign.LoadPrivateKeyWithOpts(kb, pass, opts...) } // LoadPublicKeyRaw loads a verifier from a PEM-encoded public key func LoadPublicKeyRaw(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, error) { + return LoadPublicKeyRawWithOpts(raw, options.WithHash(hashAlgorithm)) +} + +// LoadPublicKeyRawWithOpts loads a verifier from a PEM-encoded public key with options +func LoadPublicKeyRawWithOpts(raw []byte, opts ...signature.LoadOption) (signature.Verifier, error) { pub, err := cryptoutils.UnmarshalPEMToPublicKey(raw) if err != nil { return nil, err } - return signature.LoadVerifier(pub, hashAlgorithm) + return signature.LoadVerifierWithOpts(pub, opts...) } func SignerFromKeyRef(ctx context.Context, keyRef string, pf cosign.PassFunc) (signature.Signer, error) { @@ -101,6 +118,10 @@ func SignerFromKeyRef(ctx context.Context, keyRef string, pf cosign.PassFunc) (s } func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.PassFunc) (signature.SignerVerifier, error) { + return SignerVerifierFromKeyRefWithOpts(ctx, keyRef, pf) +} + +func SignerVerifierFromKeyRefWithOpts(ctx context.Context, keyRef string, pf cosign.PassFunc, opts ...signature.LoadOption) (signature.SignerVerifier, error) { switch { case strings.HasPrefix(keyRef, pkcs11key.ReferenceScheme): pkcs11UriConfig := pkcs11key.NewPkcs11UriConfig() @@ -129,7 +150,7 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass } if len(s.Data) > 0 { - return cosign.LoadPrivateKey(s.Data["cosign.key"], s.Data["cosign.password"]) + return cosign.LoadPrivateKeyWithOpts(s.Data["cosign.key"], s.Data["cosign.password"], opts...) } case strings.HasPrefix(keyRef, gitlab.ReferenceScheme): split := strings.Split(keyRef, "://") @@ -150,7 +171,7 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass return nil, err } - return cosign.LoadPrivateKey([]byte(pk), []byte(pass)) + return cosign.LoadPrivateKeyWithOpts([]byte(pk), []byte(pass), opts...) } if strings.Contains(keyRef, "://") { @@ -165,14 +186,18 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass // ProviderNotFoundError is okay; loadKey handles other URL schemes } - return loadKey(keyRef, pf) + return loadKey(keyRef, pf, opts...) } func PublicKeyFromKeyRef(ctx context.Context, keyRef string) (signature.Verifier, error) { - return PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, crypto.SHA256) + return PublicKeyFromKeyRefWithOpts(ctx, keyRef, options.WithHash(crypto.SHA256)) } func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlgorithm crypto.Hash) (signature.Verifier, error) { + return PublicKeyFromKeyRefWithOpts(ctx, keyRef, options.WithHash(hashAlgorithm)) +} + +func PublicKeyFromKeyRefWithOpts(ctx context.Context, keyRef string, opts ...signature.LoadOption) (signature.Verifier, error) { if strings.HasPrefix(keyRef, kubernetes.KeyReference) { s, err := kubernetes.GetKeyPairSecret(ctx, keyRef) if err != nil { @@ -180,7 +205,7 @@ func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlg } if len(s.Data) > 0 { - return LoadPublicKeyRaw(s.Data["cosign.pub"], hashAlgorithm) + return LoadPublicKeyRawWithOpts(s.Data["cosign.pub"], opts...) } } @@ -219,11 +244,11 @@ func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlg } if len(pubKey) > 0 { - return LoadPublicKeyRaw([]byte(pubKey), hashAlgorithm) + return LoadPublicKeyRawWithOpts([]byte(pubKey), opts...) } } - return VerifierForKeyRef(ctx, keyRef, hashAlgorithm) + return VerifierForKeyRefWithOpts(ctx, keyRef, opts...) } func PublicKeyPem(key signature.PublicKeyProvider, pkOpts ...signature.PublicKeyOption) ([]byte, error) {