Skip to content

Commit

Permalink
Add support to COSE_Countersignature (RFC 9338) (#172)
Browse files Browse the repository at this point in the history
Signed-off-by: Guilherme Balena Versiani <guibv@mailbox.org>
  • Loading branch information
balena authored Sep 30, 2023
1 parent 6e436d2 commit e7ac36d
Show file tree
Hide file tree
Showing 6 changed files with 2,921 additions and 26 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ 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` or, if preferred, maintain it as a
detached countersignature.

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 +191,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
305 changes: 305 additions & 0 deletions countersign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
package cose

import (
"errors"
"fmt"
"io"

"github.com/fxamacker/cbor/v2"
)

// Countersignature represents a decoded COSE_Countersignature.
//
// Reference: https://tools.ietf.org/html/rfc9338#section-3.1
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
type Countersignature Signature

// NewCountersignature returns a Countersignature with header initialized.
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func NewCountersignature() *Countersignature {
return (*Countersignature)(NewSignature())
}

// MarshalCBOR encodes Countersignature into a COSE_Countersignature object.
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func (s *Countersignature) MarshalCBOR() ([]byte, error) {
if s == nil {
return nil, errors.New("cbor: MarshalCBOR on nil Countersignature pointer")
}
// COSE_Countersignature share the exact same format as COSE_Signature
return (*Signature)(s).MarshalCBOR()
}

// UnmarshalCBOR decodes a COSE_Countersignature object into Countersignature.
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func (s *Countersignature) UnmarshalCBOR(data []byte) error {
if s == nil {
return errors.New("cbor: UnmarshalCBOR on nil Countersignature pointer")
}
// COSE_Countersignature share the exact same format as COSE_Signature
return (*Signature)(s).UnmarshalCBOR(data)
}

// Sign signs a Countersignature using the provided Signer.
// Signing a COSE_Countersignature requires the parent message to be completely
// fulfilled.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func (s *Countersignature) Sign(rand io.Reader, signer Signer, parent any, external []byte) error {
if s == nil {
return errors.New("signing nil Countersignature")
}
if len(s.Signature) > 0 {
return errors.New("Countersignature already has signature bytes")
}

// check algorithm if present.
// `alg` header MUST present if there is no externally supplied data.
alg := signer.Algorithm()
if err := s.Headers.ensureSigningAlgorithm(alg, external); err != nil {
return err
}

// sign the message
toBeSigned, err := s.toBeSigned(parent, external)
if err != nil {
return err
}
sig, err := signer.Sign(rand, toBeSigned)
if err != nil {
return err
}

s.Signature = sig
return nil
}

// Verify verifies the countersignature, returning nil on success or a suitable error
// if verification fails.
// Verifying a COSE_Countersignature requires the parent message.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
//
// # Experimental
//
// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
// later release.
func (s *Countersignature) Verify(verifier Verifier, parent any, external []byte) error {
if s == nil {
return errors.New("verifying nil Countersignature")
}
if len(s.Signature) == 0 {
return ErrEmptySignature
}

// check algorithm if present.
// `alg` header MUST present if there is no externally supplied data.
alg := verifier.Algorithm()
err := s.Headers.ensureVerificationAlgorithm(alg, external)
if err != nil {
return err
}

// verify the message
toBeSigned, err := s.toBeSigned(parent, external)
if err != nil {
return err
}
return verifier.Verify(toBeSigned, s.Signature)
}

// toBeSigned returns ToBeSigned from COSE_Countersignature object.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3
func (s *Countersignature) toBeSigned(target any, external []byte) ([]byte, error) {
var signProtected cbor.RawMessage
signProtected, err := s.Headers.MarshalProtected()
if err != nil {
return nil, err
}
return countersignToBeSigned(false, target, signProtected, external)
}

// countersignToBeSigned constructs Countersign_structure, computes and returns ToBeSigned.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3
func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawMessage, external []byte) ([]byte, error) {
// create a Countersign_structure and populate it with the appropriate fields.
//
// Countersign_structure = [
// context : "CounterSignature" / "CounterSignature0" /
// "CounterSignatureV2" / "CounterSignature0V2" /,
// body_protected : empty_or_serialized_map,
// ? sign_protected : empty_or_serialized_map,
// external_aad : bstr,
// payload : bstr,
// ? other_fields : [+ bstr ]
// ]

var err error
var bodyProtected cbor.RawMessage
var otherFields []cbor.RawMessage
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")
}
bodyProtected, err = t.Headers.MarshalProtected()
if err != nil {
return nil, err
}
if t.Payload == nil {
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")
}
bodyProtected, err = t.Headers.MarshalProtected()
if err != nil {
return nil, err
}
if t.Payload == nil {
return nil, ErrMissingPayload
}
payload = t.Payload
signature, err := encMode.Marshal(t.Signature)
if err != nil {
return nil, err
}
signature, err = deterministicBinaryString(signature)
if err != nil {
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 {
return nil, err
}
if len(t.Signature) == 0 {
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 {
return nil, err
}
if len(t.Signature) == 0 {
return nil, errors.New("Countersignature was not signed yet")
}
payload = t.Signature
default:
return nil, fmt.Errorf("unsupported target %T", target)
}

var context string
if len(otherFields) == 0 {
if abbreviated {
context = "CounterSignature0"
} else {
context = "CounterSignature"
}
} else {
if abbreviated {
context = "CounterSignature0V2"
} else {
context = "CounterSignatureV2"
}
}

bodyProtected, err = deterministicBinaryString(bodyProtected)
if err != nil {
return nil, err
}
signProtected, err = deterministicBinaryString(signProtected)
if err != nil {
return nil, err
}
if external == nil {
external = []byte{}
}
countersigStructure := []any{
context, // context
bodyProtected, // body_protected
signProtected, // sign_protected
external, // external_aad
payload, // payload
}
if len(otherFields) > 0 {
countersigStructure = append(countersigStructure, otherFields)
}

// create the value ToBeSigned by encoding the Countersign_structure to a byte
// string.
return encMode.Marshal(countersigStructure)
}

// Countersign0 performs an abbreviated signature over a parent message using
// the provided Signer.
//
// The parent message must be completely fulfilled prior signing.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.2
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func Countersign0(rand io.Reader, signer Signer, parent any, external []byte) ([]byte, error) {
toBeSigned, err := countersignToBeSigned(true, parent, []byte{0x40}, external)
if err != nil {
return nil, err
}
return signer.Sign(rand, toBeSigned)
}

// VerifyCountersign0 verifies an abbreviated signature over a parent message
// using the provided Verifier.
//
// Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.2
//
// # Experimental
//
// Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or
// removed in a later release.
func VerifyCountersign0(verifier Verifier, parent any, external, signature []byte) error {
toBeSigned, err := countersignToBeSigned(true, parent, []byte{0x40}, external)
if err != nil {
return err
}
return verifier.Verify(toBeSigned, signature)
}
Loading

0 comments on commit e7ac36d

Please sign in to comment.