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

Add support to COSE_Countersignature (RFC 9338) #172

Merged
merged 4 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ These are the required packages for each built-in cose.Algorithm:
- cose.AlgorithmPS384, cose.AlgorithmPS512, cose.AlgorithmES384, cose.AlgorithmES512: `crypto/sha512`
- cose.AlgorithmEdDSA: none

### Countersigning

It is possible to countersign `cose.Sign1Message`, `cose.SignMessage`, `cose.Signature` and
`cose.Countersignature` objects and add them as unprotected headers. In order to do so, first create
a countersignature holder with `cose.NewCountersignature()` and call its `Sign` function passing
the parent object which is going to be countersigned. Then assign the countersignature as an
unprotected header `cose.HeaderLabelCounterSignatureV2`.

When verifying countersignatures, it is necessary to pass the parent object in the `Verify` function
of the countersignature holder.

See [example_test.go](./example_test.go) for examples.

## Features

### Signing and Verifying Objects
Expand All @@ -177,6 +190,11 @@ go-cose supports two different signature structures:
- [cose.SignMessage](https://pkg.go.dev/github.com/veraison/go-cose#SignMessage) implements [COSE_Sign](https://datatracker.ietf.org/doc/html/rfc8152#section-4.1).
> :warning: The COSE_Sign API is currently **EXPERIMENTAL** and may be changed or removed in a later release. In addition, the amount of functional and security testing it has received so far is significantly lower than the COSE_Sign1 API.

### Countersignatures

go-cose supports [COSE_Countersignature](https://tools.ietf.org/html/rfc9338#section-3.1), check [cose.Countersignature](https://pkg.go.dev/github.com/veraison/go-cose#Countersignature).
> :warning: The COSE_Countersignature API is currently **EXPERIMENTAL** and may be changed or removed in a later release.

### Built-in Algorithms

go-cose has built-in supports the following algorithms:
Expand Down
36 changes: 21 additions & 15 deletions countersign.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawM
var payload []byte

switch t := target.(type) {
case *SignMessage:
return countersignToBeSigned(abbreviated, *t, signProtected, external)
case SignMessage:
if len(t.Signatures) == 0 {
return nil, errors.New("SignMessage has no signatures yet")
Expand All @@ -175,6 +177,8 @@ func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawM
return nil, ErrMissingPayload
}
payload = t.Payload
case *Sign1Message:
return countersignToBeSigned(abbreviated, *t, signProtected, external)
case Sign1Message:
if len(t.Signature) == 0 {
return nil, errors.New("Sign1Message was not signed yet")
Expand All @@ -196,6 +200,8 @@ func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawM
return nil, err
}
otherFields = []cbor.RawMessage{signature}
case *Signature:
return countersignToBeSigned(abbreviated, *t, signProtected, external)
case Signature:
bodyProtected, err = t.Headers.MarshalProtected()
if err != nil {
Expand All @@ -205,6 +211,8 @@ func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawM
return nil, errors.New("Signature was not signed yet")
}
payload = t.Signature
case *Countersignature:
return countersignToBeSigned(abbreviated, *t, signProtected, external)
case Countersignature:
bodyProtected, err = t.Headers.MarshalProtected()
if err != nil {
Expand All @@ -219,15 +227,18 @@ func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawM
}

var context string
switch {
case len(otherFields) == 0 && abbreviated == false:
context = "CounterSignature"
case len(otherFields) == 0 && abbreviated == true:
context = "CounterSignature0"
case len(otherFields) > 0 && abbreviated == false:
context = "CounterSignatureV2"
case len(otherFields) > 0 && abbreviated == true:
context = "CounterSignature0V2"
if len(otherFields) == 0 {
if abbreviated {
context = "CounterSignature0"
} else {
context = "CounterSignature"
}
} else {
if abbreviated {
context = "CounterSignature0V2"
} else {
context = "CounterSignatureV2"
}
}

bodyProtected, err = deterministicBinaryString(bodyProtected)
Expand Down Expand Up @@ -273,12 +284,7 @@ func Countersign0(rand io.Reader, signer Signer, parent any, external []byte) ([
if err != nil {
return nil, err
}
sig, err := signer.Sign(rand, toBeSigned)
if err != nil {
return nil, err
}

return sig, nil
return signer.Sign(rand, toBeSigned)
}

// VerifyCountersign0 verifies an abbreviated signature over a parent message
Expand Down
117 changes: 117 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,120 @@ func ExampleDigestSigner() {
// Output:
// digest signed
}

// This example demonstrates signing and verifying countersignatures.
//
// The COSE Countersignature API is EXPERIMENTAL and may be changed or removed in a later
// release.
func ExampleCountersignature() {
// create a signature holder
sigHolder := cose.NewSignature()
sigHolder.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
sigHolder.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("1")

// create message to be signed
msgToSign := cose.NewSignMessage()
msgToSign.Payload = []byte("hello world")
msgToSign.Signatures = append(msgToSign.Signatures, sigHolder)

// create a signer
privateKey, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
signer, _ := cose.NewSigner(cose.AlgorithmES512, privateKey)

// sign message
msgToSign.Sign(rand.Reader, nil, signer)

// create a countersignature holder for the message
msgCountersig := cose.NewCountersignature()
msgCountersig.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
msgCountersig.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("11")

// create a countersigner
counterPrivateKey, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
countersigner, _ := cose.NewSigner(cose.AlgorithmES512, counterPrivateKey)

// countersign message
err := msgCountersig.Sign(rand.Reader, countersigner, msgToSign, nil)
if err != nil {
panic(err)
}

// add countersignature as message unprotected header; notice the
// countersignature should be assigned as value not reference
balena marked this conversation as resolved.
Show resolved Hide resolved
msgToSign.Headers.Unprotected[cose.HeaderLabelCounterSignatureV2] = *msgCountersig

// create a countersignature holder for the signature
sigCountersig := cose.NewCountersignature()
sigCountersig.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
sigCountersig.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("11")

// countersign signature
err = sigCountersig.Sign(rand.Reader, countersigner, sigHolder, nil)
if err != nil {
panic(err)
}

// add countersignature as signature unprotected header; notice the
// countersignature should be assigned as value not reference
sigHolder.Headers.Unprotected[cose.HeaderLabelCounterSignatureV2] = *sigCountersig

sig, err := msgToSign.MarshalCBOR()
if err != nil {
panic(err)
}
fmt.Println("message signed and countersigned")

// create a verifier from a trusted public key
publicKey := counterPrivateKey.Public()
verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey)
if err != nil {
panic(err)
}

// decode COSE_Sign message containing countersignatures
var msgToVerify cose.SignMessage
err = msgToVerify.UnmarshalCBOR(sig)
if err != nil {
panic(err)
}

// unwrap the message countersignature; the example assumes the header is a
// single countersignature, but real code would consider checking if it
// consists in a slice of countersignatures too.
msgCountersigHdr := msgToVerify.Headers.Unprotected[cose.HeaderLabelCounterSignatureV2]
msgCountersigToVerify := msgCountersigHdr.(cose.Countersignature)

// verify message countersignature
err = msgCountersigToVerify.Verify(verifier, msgToVerify, nil)
if err != nil {
panic(err)
}
fmt.Println("message countersignature verified")

// unwrap the signature countersignature; the example assumes the header is a
// single countersignature, but real code would consider checking if it
// consists in a slice of countersignatures too.
sig0 := msgToVerify.Signatures[0]
sigCountersigHdr := sig0.Headers.Unprotected[cose.HeaderLabelCounterSignatureV2]
sigCountersigToVerify := sigCountersigHdr.(cose.Countersignature)

// verify signature countersignature
err = sigCountersigToVerify.Verify(verifier, sig0, nil)
if err != nil {
panic(err)
}
fmt.Println("signature countersignature verified")

// tamper the message and verification should fail
msgToVerify.Payload = []byte("foobar")
err = msgCountersigToVerify.Verify(verifier, msgToVerify, nil)
if err != cose.ErrVerification {
panic(err)
}
fmt.Println("verification error as expected")
// Output:
// message signed and countersigned
// message countersignature verified
// signature countersignature verified
// verification error as expected
}
5 changes: 4 additions & 1 deletion headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ func unmarshalAsCountersignature(value cbor.RawMessage) (any, error) {
func unmarshalAsAny(value cbor.RawMessage) (any, error) {
var result any
err := decMode.Unmarshal(value, &result)
return result, err
if err != nil {
return nil, err
}
return result, nil
}

// Headers represents "two buckets of information that are not
Expand Down