Skip to content

Commit

Permalink
Make built-in types implement the new DigestSigner and DigestVerify i…
Browse files Browse the repository at this point in the history
…nterface (#144)

Signed-off-by: qmuntal <[email protected]>
  • Loading branch information
qmuntal authored Jul 24, 2023
1 parent 88e7f1e commit a5dc571
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 8 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions ecdsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 19 additions & 1 deletion ecdsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
_ "crypto/sha512"
"fmt"

Expand Down Expand Up @@ -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
}
26 changes: 20 additions & 6 deletions rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
}

Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion rsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit a5dc571

Please sign in to comment.