diff --git a/go.mod b/go.mod index 5468dbf1..eec7ba5b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 github.com/notaryproject/tspclient-go v1.0.0 github.com/veraison/go-cose v1.3.0 + go.mozilla.org/pkcs7 v0.9.0 golang.org/x/crypto v0.37.0 ) diff --git a/go.sum b/go.sum index a411058d..8c157d1e 100644 --- a/go.sum +++ b/go.sum @@ -8,5 +8,7 @@ github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7r github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= diff --git a/signature/pkcs7/conformance_test.go b/signature/pkcs7/conformance_test.go new file mode 100644 index 00000000..b790328c --- /dev/null +++ b/signature/pkcs7/conformance_test.go @@ -0,0 +1,84 @@ +// Copyright The Notary Project 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 pkcs7 + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "testing" + + gopkcs7 "go.mozilla.org/pkcs7" + + "github.com/notaryproject/notation-core-go/signature" +) + +// TestConformance asserts that Sign() produces a PKCS#7 SignedData that meets +// the kernel dm-verity profile: RSASSA-PKCS#1 v1.5 over SHA-256, no signed +// attributes, detached content, single signer. +func TestConformance(t *testing.T) { + signer := newRSATestSigner() + encoded, err := NewEnvelope().Sign(&signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: signer, + }) + if err != nil { + t.Fatalf("Sign() error: %v", err) + } + + p7, err := gopkcs7.Parse(encoded) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if got, want := len(p7.Signers), 1; got != want { + t.Fatalf("Signers = %d, want %d", got, want) + } + + // Cert chain must be leaf + root. dm-verity has no intermediates. + if got, want := len(p7.Certificates), 2; got != want { + t.Fatalf("Certificates = %d, want %d (leaf + root)", got, want) + } + if !p7.Certificates[0].Equal(signer.certs[0]) { + t.Errorf("Certificates[0] is not the leaf certificate") + } + if !p7.Certificates[1].Equal(signer.certs[1]) { + t.Errorf("Certificates[1] is not the root certificate") + } + + si := p7.Signers[0] + if !si.DigestAlgorithm.Algorithm.Equal(gopkcs7.OIDDigestAlgorithmSHA256) { + t.Errorf("DigestAlgorithm = %v, want SHA-256", si.DigestAlgorithm.Algorithm) + } + if !si.DigestEncryptionAlgorithm.Algorithm.Equal(gopkcs7.OIDEncryptionAlgorithmRSA) { + t.Errorf("DigestEncryptionAlgorithm = %v, want rsaEncryption", si.DigestEncryptionAlgorithm.Algorithm) + } + if len(si.AuthenticatedAttributes) != 0 { + t.Errorf("AuthenticatedAttributes len = %d, want 0", len(si.AuthenticatedAttributes)) + } + if len(p7.Content) != 0 { + t.Errorf("Content len = %d, want 0 (must be detached)", len(p7.Content)) + } + + // Independently verify the bytes are RSASSA-PKCS#1 v1.5 over SHA-256 + // of the payload. + digest := sha256.Sum256([]byte(testPayload)) + pub, ok := signer.certs[0].PublicKey.(*rsa.PublicKey) + if !ok { + t.Fatalf("leaf public key is %T, want *rsa.PublicKey", signer.certs[0].PublicKey) + } + if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest[:], si.EncryptedDigest); err != nil { + t.Fatalf("rsa.VerifyPKCS1v15 over SHA-256(payload) failed: %v", err) + } +} diff --git a/signature/pkcs7/envelope.go b/signature/pkcs7/envelope.go new file mode 100644 index 00000000..4304e266 --- /dev/null +++ b/signature/pkcs7/envelope.go @@ -0,0 +1,275 @@ +// Copyright The Notary Project 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 pkcs7 provides a PKCS#7/CMS signature envelope for dm-verity +// signing. +package pkcs7 + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + "io" + + "github.com/notaryproject/notation-core-go/signature" + gopkcs7 "go.mozilla.org/pkcs7" +) + +// ErrDetachedNotVerifiable is returned by Envelope.Verify. The dm-verity +// PKCS#7 envelope is detached and Envelope.Verify takes no payload, so it +// cannot perform cryptographic verification. Verify the dm-verity root +// hash out-of-band. +var ErrDetachedNotVerifiable = errors.New("PKCS#7 dm-verity envelope is detached; verify against the dm-verity root hash out-of-band") + +// MediaTypeEnvelope is the PKCS#7 signature envelope media type. +const MediaTypeEnvelope = "application/pkcs7-signature" + +func init() { + if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil { + panic(err) + } +} + +// envelope holds a parsed PKCS#7 signature. raw is canonical; certs and +// sigBytes are caches derived from raw. +type envelope struct { + raw []byte + certs []*x509.Certificate + sigBytes []byte +} + +// NewEnvelope creates a new PKCS#7 envelope. +// +// The dm-verity profile requires a SignerInfo with no signed attributes, +// so this envelope is not wrapped with base.Envelope (which injects a +// signing-time signed attribute). +func NewEnvelope() signature.Envelope { + return &envelope{} +} + +// ParseEnvelope parses PKCS#7 DER bytes into an envelope. +func ParseEnvelope(envelopeBytes []byte) (env signature.Envelope, err error) { + // Convert any panic from the underlying parser to a typed error. + defer func() { + if r := recover(); r != nil { + env = nil + err = &signature.InvalidSignatureError{Msg: fmt.Sprintf("malformed PKCS#7 envelope: %v", r)} + } + }() + p7, err := gopkcs7.Parse(envelopeBytes) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + + // dm-verity profile: exactly one signer with a non-empty signature. + if len(p7.Signers) != 1 { + return nil, &signature.InvalidSignatureError{ + Msg: fmt.Sprintf("dm-verity PKCS#7 envelope requires exactly one signer; got %d", len(p7.Signers)), + } + } + sigBytes := p7.Signers[0].EncryptedDigest + if len(sigBytes) == 0 { + return nil, &signature.InvalidSignatureError{Msg: "dm-verity PKCS#7 envelope has empty signature"} + } + + return &envelope{ + raw: envelopeBytes, + certs: p7.Certificates, + sigBytes: sigBytes, + }, nil +} + +// Sign implements signature.Envelope for the dm-verity profile: RSA-2048 + +// SHA-256 + RSASSA-PKCS#1 v1.5, detached, no signed attributes. The actual +// signing is done by req.Signer; gopkcs7 only wraps the resulting bytes in +// CMS SignedData. Sign verifies the signer's output against certs[0] +// before wrapping. +func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { + if err := validateSignRequest(req); err != nil { + return nil, err + } + + sig, certs, err := req.Signer.Sign(req.Payload.Content) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: fmt.Sprintf("signing failed: %v", err)} + } + if err := verifySignerOutput(req.Payload.Content, sig, certs); err != nil { + return nil, err + } + + sd, err := gopkcs7.NewSignedData(req.Payload.Content) + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + + // dm-verity profile: SHA-256 digest, RSASSA-PKCS#1 v1.5 signature, + // no signed attributes, detached content. + sd.SetDigestAlgorithm(gopkcs7.OIDDigestAlgorithmSHA256) + sd.SetEncryptionAlgorithm(gopkcs7.OIDEncryptionAlgorithmRSA) + adapter := &signerAdapter{sig: sig, certs: certs} + if err := sd.SignWithoutAttr(certs[0], adapter, gopkcs7.SignerInfoConfig{}); err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + for i := 1; i < len(certs); i++ { + sd.AddCertificate(certs[i]) + } + sd.Detach() + + encoded, err := sd.Finish() + if err != nil { + return nil, &signature.InvalidSignatureError{Msg: err.Error()} + } + + // Re-parse to populate caches. + p7, err := gopkcs7.Parse(encoded) + if err != nil { + return nil, &signature.InvalidSignatureError{ + Msg: fmt.Sprintf("self-parse failed after Sign: %v", err), + } + } + e.raw = encoded + e.certs = certs + if len(p7.Signers) > 0 { + e.sigBytes = p7.Signers[0].EncryptedDigest + } + return encoded, nil +} + +// validateSignRequest enforces the dm-verity profile: non-empty payload, +// no signed-attribute fields, RSA-2048 key only. +func validateSignRequest(req *signature.SignRequest) error { + if len(req.Payload.Content) == 0 { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope requires a non-empty Payload.Content"} + } + if !req.SigningTime.IsZero() { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope does not support SigningTime"} + } + if !req.Expiry.IsZero() { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope does not support Expiry"} + } + if req.SigningScheme != "" { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope does not support SigningScheme"} + } + if req.SigningAgent != "" { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope does not support SigningAgent"} + } + if len(req.ExtendedSignedAttributes) > 0 { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope does not support ExtendedSignedAttributes"} + } + + if req.Signer == nil { + return &signature.InvalidSignRequestError{Msg: "dm-verity PKCS#7 envelope requires a non-nil Signer"} + } + keySpec, err := req.Signer.KeySpec() + if err != nil { + return &signature.InvalidSignRequestError{Msg: err.Error()} + } + + // dm-verity profile: RSA-2048 + SHA-256 + RSASSA-PKCS#1 v1.5 only. + if keySpec.Type != signature.KeyTypeRSA { + return &signature.UnsupportedSigningKeyError{ + Msg: fmt.Sprintf("dm-verity PKCS#7 envelope requires an RSA key; got %v", keySpec.Type), + } + } + if keySpec.Size != 2048 { + return &signature.UnsupportedSigningKeyError{ + Msg: fmt.Sprintf("dm-verity PKCS#7 envelope requires RSA-2048; got RSA-%d", keySpec.Size), + } + } + return nil +} + +// verifySignerOutput checks that sig is RSASSA-PKCS#1 v1.5 over SHA-256 of +// payload under certs[0]'s public key. +func verifySignerOutput(payload, sig []byte, certs []*x509.Certificate) error { + if len(certs) == 0 { + return &signature.InvalidSignatureError{Msg: "no certificates returned from signer"} + } + if certs[0] == nil { + return &signature.InvalidSignatureError{Msg: "signer returned a nil leaf certificate"} + } + pub, ok := certs[0].PublicKey.(*rsa.PublicKey) + if !ok { + return &signature.UnsupportedSigningKeyError{ + Msg: fmt.Sprintf("leaf certificate public key is %T, want *rsa.PublicKey", certs[0].PublicKey), + } + } + digest := sha256.Sum256(payload) + if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest[:], sig); err != nil { + return &signature.InvalidSignatureError{ + Msg: fmt.Sprintf("signer did not produce RSASSA-PKCS#1 v1.5 over SHA-256: %v", err), + } + } + return nil +} + +// signerAdapter wraps a pre-computed signature so it can be passed to a +// gopkcs7.SignedData (which expects a crypto.Signer). It is single-use: +// Sign must be called at most once. +type signerAdapter struct { + sig []byte // pre-computed signature from actual signer + certs []*x509.Certificate // certificate chain + used bool // set on first Sign call +} + +// Public returns the leaf certificate's public key. +func (a *signerAdapter) Public() crypto.PublicKey { + if len(a.certs) == 0 { + panic("pkcs7: signerAdapter constructed with empty cert chain") + } + return a.certs[0].PublicKey +} + +// Sign returns the pre-computed signature. The digest and opts arguments +// are ignored. Returns an error if called more than once. +func (a *signerAdapter) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if a.used { + return nil, errors.New("pkcs7: signerAdapter.Sign called more than once") + } + a.used = true + return a.sig, nil +} + +// Verify always returns ErrDetachedNotVerifiable for a populated envelope. +func (e *envelope) Verify() (*signature.EnvelopeContent, error) { + if e.raw == nil { + return nil, &signature.SignatureEnvelopeNotFoundError{} + } + return nil, ErrDetachedNotVerifiable +} + +// Content implements signature.Envelope. +// +// SignerInfo.SignatureAlgorithm is the zero value: signature.Algorithm has +// no constant for RSASSA-PKCS#1 v1.5. Read the certificate chain or the +// CMS digestEncryptionAlgorithm OID instead. +func (e *envelope) Content() (*signature.EnvelopeContent, error) { + if e.raw == nil { + return nil, &signature.SignatureEnvelopeNotFoundError{} + } + return &signature.EnvelopeContent{ + SignerInfo: signature.SignerInfo{ + CertificateChain: e.certs, + Signature: e.sigBytes, + }, + // Payload.ContentType identifies the signed payload's media type, + // not the envelope format. PKCS#7 detached signatures don't carry + // the payload's content type, so it is unknown after parsing. + Payload: signature.Payload{}, + }, nil +} + +var _ signature.Envelope = (*envelope)(nil) diff --git a/signature/pkcs7/envelope_test.go b/signature/pkcs7/envelope_test.go new file mode 100644 index 00000000..081c66b5 --- /dev/null +++ b/signature/pkcs7/envelope_test.go @@ -0,0 +1,329 @@ +// Copyright The Notary Project 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 pkcs7 + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "errors" + "testing" + "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/testhelper" + gopkcs7 "go.mozilla.org/pkcs7" +) + +const testPayload = "test dm-verity root hash payload" + +// newRSATestSigner creates an RSA-2048 test signer using testhelper certs. +func newRSATestSigner() *testPrimitiveSigner { + tuple := testhelper.GetRSACertTuple(2048) + rootCert := testhelper.GetRSARootCertificate().Cert + return &testPrimitiveSigner{ + key: tuple.PrivateKey, + certs: []*x509.Certificate{tuple.Cert, rootCert}, + keySpec: signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}, + } +} + +// testPrimitiveSigner implements signature.Signer for testing. +type testPrimitiveSigner struct { + key crypto.PrivateKey + certs []*x509.Certificate + keySpec signature.KeySpec +} + +func (s *testPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + h := sha256.Sum256(payload) + sig, err := rsa.SignPKCS1v15(rand.Reader, s.key.(*rsa.PrivateKey), crypto.SHA256, h[:]) + if err != nil { + return nil, nil, err + } + return sig, s.certs, nil +} + +func (s *testPrimitiveSigner) KeySpec() (signature.KeySpec, error) { + return s.keySpec, nil +} + +// newSignRequest builds a SignRequest backed by an RSA-2048 test signer. +func newSignRequest() *signature.SignRequest { + return &signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: newRSATestSigner(), + } +} + +// TestSignParseVerifyRoundTrip exercises Sign → Parse → Verify → Content. +func TestSignParseVerifyRoundTrip(t *testing.T) { + encoded, err := NewEnvelope().Sign(newSignRequest()) + if err != nil { + t.Fatalf("Sign() error: %v", err) + } + if len(encoded) == 0 { + t.Fatal("Sign() returned empty bytes") + } + + parsed, err := ParseEnvelope(encoded) + if err != nil { + t.Fatalf("ParseEnvelope() error: %v", err) + } + + if _, err := parsed.Verify(); !errors.Is(err, ErrDetachedNotVerifiable) { + t.Fatalf("Verify() = %v, want ErrDetachedNotVerifiable", err) + } + + content, err := parsed.Content() + if err != nil { + t.Fatalf("Content() error: %v", err) + } + if len(content.SignerInfo.CertificateChain) == 0 { + t.Fatal("Content() returned no certificates") + } + // PKCS#7 detached signatures don't carry the signed payload's content + // type, so Content() reports it as unknown. + if content.Payload.ContentType != "" { + t.Fatalf("ContentType = %q, want empty", content.Payload.ContentType) + } +} + +func TestParseEnvelopeError(t *testing.T) { + _, err := ParseEnvelope([]byte("invalid")) + var target *signature.InvalidSignatureError + if !errors.As(err, &target) { + t.Fatalf("want InvalidSignatureError, got %T: %v", err, err) + } +} + +func TestSignError(t *testing.T) { + req := newSignRequest() + req.Signer = &stubSigner{ + keySpec: signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}, + signErr: errors.New("signing failed"), + } + if _, err := NewEnvelope().Sign(req); err == nil { + t.Fatal("Sign() with failing signer expected error, got nil") + } +} + +func TestVerifyEmptyEnvelope(t *testing.T) { + if _, err := NewEnvelope().Verify(); err == nil { + t.Fatal("Verify() on empty envelope expected error, got nil") + } +} + +func TestEnvelopeRegistration(t *testing.T) { + env, err := signature.NewEnvelope(MediaTypeEnvelope) + if err != nil { + t.Fatalf("NewEnvelope(%q) error: %v", MediaTypeEnvelope, err) + } + if env == nil { + t.Fatal("NewEnvelope() returned nil for registered media type") + } +} + +// TestSignRejectsNonRSA2048 verifies that Sign refuses key specs outside +// the dm-verity profile (RSA-2048 only). +func TestSignRejectsNonRSA2048(t *testing.T) { + cases := []struct { + name string + keySpec signature.KeySpec + }{ + {"ecdsa-p256", signature.KeySpec{Type: signature.KeyTypeEC, Size: 256}}, + {"rsa-3072", signature.KeySpec{Type: signature.KeyTypeRSA, Size: 3072}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewEnvelope().Sign(&signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: &stubSigner{keySpec: tc.keySpec}, + }) + var want *signature.UnsupportedSigningKeyError + if !errors.As(err, &want) { + t.Fatalf("want UnsupportedSigningKeyError, got %T: %v", err, err) + } + }) + } +} + +// stubSigner reports a fixed KeySpec and returns signErr from Sign(). +type stubSigner struct { + keySpec signature.KeySpec + signErr error +} + +func (s *stubSigner) Sign([]byte) ([]byte, []*x509.Certificate, error) { + if s.signErr == nil { + return nil, nil, errors.New("stubSigner.Sign: no signErr configured") + } + return nil, nil, s.signErr +} +func (s *stubSigner) KeySpec() (signature.KeySpec, error) { return s.keySpec, nil } + +// TestSignRejectsSignedAttributeFields verifies that signed-attribute +// fields on the SignRequest are rejected. +func TestSignRejectsSignedAttributeFields(t *testing.T) { + cases := []struct { + name string + mutate func(*signature.SignRequest) + }{ + {"SigningTime", func(r *signature.SignRequest) { r.SigningTime = time.Now() }}, + {"Expiry", func(r *signature.SignRequest) { r.Expiry = time.Now().Add(time.Hour) }}, + {"SigningScheme", func(r *signature.SignRequest) { r.SigningScheme = signature.SigningSchemeX509 }}, + {"SigningAgent", func(r *signature.SignRequest) { r.SigningAgent = "notation/0.0" }}, + {"ExtendedSignedAttributes", func(r *signature.SignRequest) { + r.ExtendedSignedAttributes = []signature.Attribute{{Key: "k", Value: "v"}} + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := newSignRequest() + tc.mutate(req) + _, err := NewEnvelope().Sign(req) + var want *signature.InvalidSignRequestError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignRequestError, got %T: %v", err, err) + } + }) + } +} + +// TestSignRejectsNilSigner verifies that a SignRequest with a nil Signer +// returns a typed error. +func TestSignRejectsNilSigner(t *testing.T) { + _, err := NewEnvelope().Sign(&signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + }) + var want *signature.InvalidSignRequestError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignRequestError, got %T: %v", err, err) + } +} + +// TestSignRejectsBadSignerOutput verifies that a signer returning bytes +// other than RSASSA-PKCS#1 v1.5 over SHA-256 is rejected. +func TestSignRejectsBadSignerOutput(t *testing.T) { + base := newRSATestSigner() + cases := []struct { + name string + sign func(payload []byte) ([]byte, error) + }{ + {"pss-instead-of-pkcs1v15", func(payload []byte) ([]byte, error) { + h := sha256.Sum256(payload) + return rsa.SignPSS(rand.Reader, base.key.(*rsa.PrivateKey), crypto.SHA256, h[:], nil) + }}, + {"random-bytes", func([]byte) ([]byte, error) { + b := make([]byte, 256) // RSA-2048 signature length + _, err := rand.Read(b) + return b, err + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewEnvelope().Sign(&signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: &customSignSigner{certs: base.certs, keySpec: base.keySpec, sign: tc.sign}, + }) + var want *signature.InvalidSignatureError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignatureError, got %T: %v", err, err) + } + }) + } +} + +// customSignSigner delegates Sign() to a caller-supplied function. +type customSignSigner struct { + certs []*x509.Certificate + keySpec signature.KeySpec + sign func(payload []byte) ([]byte, error) +} + +func (s *customSignSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + sig, err := s.sign(payload) + if err != nil { + return nil, nil, err + } + return sig, s.certs, nil +} +func (s *customSignSigner) KeySpec() (signature.KeySpec, error) { return s.keySpec, nil } + +// TestSignRejectsEmptyPayload verifies that an empty Payload.Content is +// rejected with InvalidSignRequestError. +func TestSignRejectsEmptyPayload(t *testing.T) { + req := newSignRequest() + req.Payload.Content = nil + _, err := NewEnvelope().Sign(req) + var want *signature.InvalidSignRequestError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignRequestError, got %T: %v", err, err) + } +} + +// TestSignRejectsNilLeafCertificate verifies that a signer returning a +// nil leaf certificate is rejected with InvalidSignatureError. +func TestSignRejectsNilLeafCertificate(t *testing.T) { + base := newRSATestSigner() + req := &signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: &customSignSigner{ + certs: []*x509.Certificate{nil, base.certs[1]}, + keySpec: base.keySpec, + sign: func(payload []byte) ([]byte, error) { + h := sha256.Sum256(payload) + return rsa.SignPKCS1v15(rand.Reader, base.key.(*rsa.PrivateKey), crypto.SHA256, h[:]) + }, + }, + } + _, err := NewEnvelope().Sign(req) + var want *signature.InvalidSignatureError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignatureError, got %T: %v", err, err) + } +} + +// TestParseEnvelopeRejectsMultipleSigners verifies that the dm-verity +// profile's "exactly one signer" guard fires on multi-signer envelopes. +// Empty-EncryptedDigest is also exercised by FuzzSignaturePkcs7. +func TestParseEnvelopeRejectsMultipleSigners(t *testing.T) { + tuple := testhelper.GetRSACertTuple(2048) + sd, err := gopkcs7.NewSignedData([]byte(testPayload)) + if err != nil { + t.Fatalf("NewSignedData() error: %v", err) + } + sd.SetDigestAlgorithm(gopkcs7.OIDDigestAlgorithmSHA256) + sd.SetEncryptionAlgorithm(gopkcs7.OIDEncryptionAlgorithmRSA) + cfg := gopkcs7.SignerInfoConfig{} + if err := sd.SignWithoutAttr(tuple.Cert, tuple.PrivateKey, cfg); err != nil { + t.Fatalf("SignWithoutAttr() #1 error: %v", err) + } + if err := sd.SignWithoutAttr(tuple.Cert, tuple.PrivateKey, cfg); err != nil { + t.Fatalf("SignWithoutAttr() #2 error: %v", err) + } + sd.Detach() + encoded, err := sd.Finish() + if err != nil { + t.Fatalf("Finish() error: %v", err) + } + + _, err = ParseEnvelope(encoded) + var want *signature.InvalidSignatureError + if !errors.As(err, &want) { + t.Fatalf("want InvalidSignatureError, got %T: %v", err, err) + } +} diff --git a/signature/pkcs7/fuzz_test.go b/signature/pkcs7/fuzz_test.go new file mode 100644 index 00000000..f06ae2ec --- /dev/null +++ b/signature/pkcs7/fuzz_test.go @@ -0,0 +1,46 @@ +// Copyright The Notary Project 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 pkcs7 + +import ( + "testing" + + "github.com/notaryproject/notation-core-go/signature" +) + +func FuzzSignaturePkcs7(f *testing.F) { + // Seed the corpus with a known-good envelope. + seed, err := NewEnvelope().Sign(&signature.SignRequest{ + Payload: signature.Payload{ContentType: MediaTypeEnvelope, Content: []byte(testPayload)}, + Signer: newRSATestSigner(), + }) + if err != nil { + f.Fatalf("seed Sign() error: %v", err) + } + f.Add(seed, true) + f.Add(seed, false) + + f.Fuzz(func(t *testing.T, envelopeBytes []byte, shouldVerify bool) { + e, err := ParseEnvelope(envelopeBytes) + if err != nil { + t.Skip() + } + + if shouldVerify { + _, _ = e.Verify() + } else { + _, _ = e.Content() + } + }) +}