Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is interface also an integration point with remote KMS?

any time you have hardware isolated keys, you need a signer interface to request signatures from them.

type DigestSigner interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Does this interface support signing digest from any Digest Algorithm ? Meaning should go-cose not restrict the digest signing to ONLY a known set of digest algorithms? This interface relies on caller to just pass digest as a substitute to content[] ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Does this interface support signing digest from any Digest Algorithm ? Meaning should go-cose not restrict the digest signing to ONLY a known set of digest algorithms

How could we restrict that? Everyone is free to implement the interface as needed, Go doesn't allow intercepting interface implementations to do custom validations. Also, in this PR we don't use this interface anywhere, users would have to construct the cose.Sign1Message themselves, so it's an experts-only feature.

This interface relies on caller to just pass digest as a substitute to content[] ?

Yes

// 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