Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make built-in types implement the new DigestSigner and DigestVerify interface #144

Merged
merged 7 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ 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 digested payloads
qmuntal marked this conversation as resolved.
Show resolved Hide resolved

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.
qmuntal marked this conversation as resolved.
Show resolved Hide resolved
qmuntal marked this conversation as resolved.
Show resolved Hide resolved

### 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
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
Contributor 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