diff --git a/README.md b/README.md index 74a8f77..837fde9 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,18 @@ See [example_test.go](./example_test.go) for more examples. Untagged COSE_Sign1 messages can be signed and verified as above, using `cose.UntaggedSign1Message` instead of `cose.Sign1Message`. +#### Signing and Verification of payload digest + +When `cose.NewSigner` is used with PS{256,384,512} or ES{256,384,512}, the returned signer +can be casted to the `cose.DigestSigner` interface, whose `SignDigest` method signs an +already digested message. + +When `cose.NewVerifier` is used with PS{256,384,512} or ES{256,384,512}, the returned verifier +can be casted to the `cose.DigestVerifier` interface, whose `VerifyDigest` method verifies an +already digested message. + +Please refer to [example_test.go](./example_test.go) for the API usage. + ### About hashing `go-cose` does not import any hash package by its own to avoid linking unnecessary algorithms to the final binary. diff --git a/ecdsa.go b/ecdsa.go index 7e426be..d1ae0e9 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -57,6 +57,13 @@ func (es *ecdsaKeySigner) Sign(rand io.Reader, content []byte) ([]byte, error) { if err != nil { return nil, err } + return es.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (es *ecdsaKeySigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { r, s, err := ecdsa.Sign(rand, es.key, digest) if err != nil { return nil, err @@ -86,6 +93,13 @@ func (es *ecdsaCryptoSigner) Sign(rand io.Reader, content []byte) ([]byte, error if err != nil { return nil, err } + return es.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (es *ecdsaCryptoSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { sigASN1, err := es.signer.Sign(rand, digest, nil) if err != nil { return nil, err @@ -153,7 +167,15 @@ func (ev *ecdsaVerifier) Verify(content []byte, signature []byte) error { if err != nil { return err } + return ev.VerifyDigest(digest, signature) +} +// VerifyDigest verifies message digest with the public key, returning nil +// for success. +// Otherwise, it returns ErrVerification. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 +func (ev *ecdsaVerifier) VerifyDigest(digest []byte, signature []byte) error { // verify signature r, s, err := decodeECDSASignature(ev.key.Curve, signature) if err != nil { diff --git a/ecdsa_test.go b/ecdsa_test.go index 308ca6a..1cb2c12 100644 --- a/ecdsa_test.go +++ b/ecdsa_test.go @@ -219,7 +219,7 @@ func testSignVerify(t *testing.T, alg Algorithm, key crypto.Signer, isCryptoSign // sign / verify round trip // see also conformance_test.go for strict tests. - content := []byte("hello world") + content := []byte("hello world, مرحبا بالعالم") sig, err := signer.Sign(rand.Reader, content) if err != nil { t.Fatalf("Sign() error = %v", err) @@ -232,6 +232,24 @@ func testSignVerify(t *testing.T, alg Algorithm, key crypto.Signer, isCryptoSign if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + // digested sign/verify round trip + dsigner, ok := signer.(DigestSigner) + if !ok { + t.Fatalf("signer is not a DigestSigner") + } + digest := sha256.Sum256(content) + dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) + if err != nil { + t.Fatalf("SignDigest() error = %v", err) + } + dverifier, ok := verifier.(DigestVerifier) + if !ok { + t.Fatalf("verifier is not a DigestVerifier") + } + if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { + t.Fatalf("VerifyDigest() error = %v", err) + } } type ecdsaBadCryptoSigner struct { diff --git a/ed25519_test.go b/ed25519_test.go index d23e65b..20bb555 100644 --- a/ed25519_test.go +++ b/ed25519_test.go @@ -47,6 +47,15 @@ func Test_ed25519Signer(t *testing.T) { if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + _, ok := signer.(DigestSigner) + if ok { + t.Fatalf("signer shouldn't be a DigestSigner") + } + _, ok = verifier.(DigestVerifier) + if ok { + t.Fatalf("verifier shouldn't be a DigestVerifier") + } } func Test_ed25519Verifier_Verify_Success(t *testing.T) { diff --git a/example_test.go b/example_test.go index 07ea624..afdf128 100644 --- a/example_test.go +++ b/example_test.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/sha512" _ "crypto/sha512" "fmt" @@ -202,3 +203,29 @@ func ExampleSign1Untagged() { // Output: // message signed } + +func ExampleDigestSigner() { + // create a signer + privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + panic(err) + } + signer, err := cose.NewSigner(cose.AlgorithmES256, privateKey) + if err != nil { + panic(err) + } + digestSigner, ok := signer.(cose.DigestSigner) + if !ok { + panic("signer does not support digest signing") + } + + // hash payload outside go-cose. + payload := []byte("hello world") + digested := sha512.Sum512(payload) + sig, err := digestSigner.SignDigest(rand.Reader, digested[:]) + + fmt.Println("digest signed") + _ = sig // further process on sig + // Output: + // digest signed +} diff --git a/rsa.go b/rsa.go index b63098c..bb920d6 100644 --- a/rsa.go +++ b/rsa.go @@ -24,14 +24,20 @@ func (rs *rsaSigner) Algorithm() Algorithm { // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 func (rs *rsaSigner) Sign(rand io.Reader, content []byte) ([]byte, error) { - hash := rs.alg.hashFunc() - digest, err := computeHash(hash, content) + digest, err := rs.alg.computeHash(content) if err != nil { return nil, err } + return rs.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (rs *rsaSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { return rs.key.Sign(rand, digest, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 - Hash: hash, + Hash: rs.alg.hashFunc(), }) } @@ -54,12 +60,20 @@ func (rv *rsaVerifier) Algorithm() Algorithm { // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 func (rv *rsaVerifier) Verify(content []byte, signature []byte) error { - hash := rv.alg.hashFunc() - digest, err := computeHash(hash, content) + digest, err := rv.alg.computeHash(content) if err != nil { return err } - if err := rsa.VerifyPSS(rv.key, hash, digest, signature, &rsa.PSSOptions{ + return rv.VerifyDigest(digest, signature) +} + +// VerifyDigest verifies message digest with the public key, returning nil +// for success. +// Otherwise, it returns ErrVerification. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 +func (rv *rsaVerifier) VerifyDigest(digest []byte, signature []byte) error { + if err := rsa.VerifyPSS(rv.key, rv.alg.hashFunc(), digest, signature, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 }); err != nil { return ErrVerification diff --git a/rsa_test.go b/rsa_test.go index e859b78..803406b 100644 --- a/rsa_test.go +++ b/rsa_test.go @@ -36,7 +36,7 @@ func Test_rsaSigner(t *testing.T) { // sign / verify round trip // see also conformance_test.go for strict tests. - content := []byte("hello world") + content := []byte("hello world, مرحبا بالعالم") sig, err := signer.Sign(rand.Reader, content) if err != nil { t.Fatalf("Sign() error = %v", err) @@ -49,6 +49,24 @@ func Test_rsaSigner(t *testing.T) { if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + // digested sign/verify round trip + dsigner, ok := signer.(DigestSigner) + if !ok { + t.Fatalf("signer is not a DigestSigner") + } + digest := sha256.Sum256(content) + dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) + if err != nil { + t.Fatalf("SignDigest() error = %v", err) + } + dverifier, ok := verifier.(DigestVerifier) + if !ok { + t.Fatalf("verifier is not a DigestVerifier") + } + if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { + t.Fatalf("VerifyDigest() error = %v", err) + } } func Test_rsaSigner_SignHashFailure(t *testing.T) { diff --git a/signer.go b/signer.go index 6747546..78e1573 100644 --- a/signer.go +++ b/signer.go @@ -23,6 +23,17 @@ type Signer interface { Sign(rand io.Reader, content []byte) ([]byte, error) } +// DigestSigner is an interface for private keys to sign digested COSE signatures. +type DigestSigner interface { + // Algorithm returns the signing algorithm associated with the private key. + Algorithm() Algorithm + + // SignDigest signs message digest with the private key, possibly using + // entropy from rand. + // The resulting signature should follow RFC 8152 section 8. + SignDigest(rand io.Reader, digest []byte) ([]byte, error) +} + // NewSigner returns a signer with a given signing key. // The signing key can be a golang built-in crypto private key, a key in HSM, or // a remote KMS. @@ -34,6 +45,8 @@ type Signer interface { // public key of type `*rsa.PublicKey`, `*ecdsa.PublicKey`, or // `ed25519.PublicKey` are accepted. // +// The returned signer for rsa and ecdsa keys also implements `cose.DigestSigner`. +// // Note: `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, and `ed25519.PrivateKey` // implement `crypto.Signer`. func NewSigner(alg Algorithm, key crypto.Signer) (Signer, error) { diff --git a/verifier.go b/verifier.go index 1c6e83b..31d1f51 100644 --- a/verifier.go +++ b/verifier.go @@ -22,9 +22,22 @@ type Verifier interface { Verify(content, signature []byte) error } +// DigestVerifier is an interface for public keys to verify digested COSE signatures. +type DigestVerifier interface { + // Algorithm returns the signing algorithm associated with the public key. + Algorithm() Algorithm + + // VerifyDigest verifies message digest with the public key, returning nil + // for success. + // Otherwise, it returns ErrVerification. + VerifyDigest(digest, signature []byte) error +} + // NewVerifier returns a verifier with a given public key. // Only golang built-in crypto public keys of type `*rsa.PublicKey`, // `*ecdsa.PublicKey`, and `ed25519.PublicKey` are accepted. +// +// The returned signer for rsa and ecdsa keys also implements `cose.DigestSigner`. func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) { switch alg { case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512: