diff --git a/README.md b/README.md index 82b56ec..f7fccf3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: diff --git a/countersign.go b/countersign.go new file mode 100644 index 0000000..4b385c7 --- /dev/null +++ b/countersign.go @@ -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) +} diff --git a/countersign_test.go b/countersign_test.go new file mode 100644 index 0000000..2bb4903 --- /dev/null +++ b/countersign_test.go @@ -0,0 +1,2044 @@ +package cose + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "reflect" + "testing" +) + +func TestCountersignature_MarshalCBOR(t *testing.T) { + tests := []struct { + name string + s *Countersignature + want []byte + wantErr string + }{ + { + name: "valid message", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelContentType: 42, + }, + }, + Signature: []byte("bar"), + }, + want: []byte{ + 0x83, // array of size 3 + 0x43, 0xa1, 0x01, 0x26, // protected + 0xa1, 0x03, 0x18, 0x2a, // unprotected + 0x43, 0x62, 0x61, 0x72, // signature + }, + }, + { + name: "nil signature", + s: nil, + wantErr: "cbor: MarshalCBOR on nil Countersignature pointer", + }, + { + name: "nil signature", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelContentType: 42, + }, + }, + Signature: nil, + }, + wantErr: "empty signature", + }, + { + name: "empty signature", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelContentType: 42, + }, + }, + Signature: []byte{}, + }, + wantErr: "empty signature", + }, + { + name: "invalid protected header", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: make(chan bool), + }, + Unprotected: UnprotectedHeader{ + HeaderLabelContentType: 42, + }, + }, + Signature: []byte("bar"), + }, + wantErr: "protected header: header parameter: alg: require int / tstr type", + }, + { + name: "invalid unprotected header", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + "foo": make(chan bool), + }, + }, + Signature: []byte("bar"), + }, + wantErr: "cbor: unsupported type: chan bool", + }, + { + name: "protected has IV and unprotected has PartialIV error", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelIV: []byte(""), + }, + Unprotected: UnprotectedHeader{ + HeaderLabelPartialIV: []byte(""), + }, + }, + Signature: []byte("bar"), + }, + wantErr: "IV (protected) and PartialIV (unprotected) parameters must not both be present", + }, + { + name: "protected has PartialIV and unprotected has IV error", + s: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelPartialIV: []byte(""), + }, + Unprotected: UnprotectedHeader{ + HeaderLabelIV: []byte(""), + }, + }, + Signature: []byte("bar"), + }, + wantErr: "IV (unprotected) and PartialIV (protected) parameters must not both be present", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.MarshalCBOR() + if err != nil && (err.Error() != tt.wantErr) { + t.Errorf("Countersignature.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } else if err == nil && (tt.wantErr != "") { + t.Errorf("Countersignature.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Countersignature.MarshalCBOR() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCountersignature_UnmarshalCBOR(t *testing.T) { + // test nil pointer + t.Run("nil Countersignature pointer", func(t *testing.T) { + var sig *Countersignature + data := []byte{0x83, 0x40, 0xa0, 0x41, 0x00} + if err := sig.UnmarshalCBOR(data); err == nil { + t.Errorf("want error on nil *Countersignature") + } + }) + + // test others + tests := []struct { + name string + data []byte + want Countersignature + wantErr string + }{ + { + name: "valid signature struct", + data: []byte{ + 0x83, + 0x43, 0xa1, 0x01, 0x26, // protected + 0xa1, 0x03, 0x18, 0x2a, // unprotected + 0x43, 0x62, 0x61, 0x72, // signature + }, + want: Countersignature{ + Headers: Headers{ + RawProtected: []byte{0x43, 0xa1, 0x01, 0x26}, + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + RawUnprotected: []byte{0xa1, 0x03, 0x18, 0x2a}, + Unprotected: UnprotectedHeader{ + HeaderLabelContentType: int64(42), + }, + }, + Signature: []byte("bar"), + }, + }, + { + name: "nil CBOR data", + data: nil, + wantErr: "cbor: invalid Signature object", + }, + { + name: "empty CBOR data", + data: []byte{}, + wantErr: "cbor: invalid Signature object", + }, + { + name: "tagged signature", // issue #30 + data: []byte{ + 0x83, + 0x40, 0xa0, // empty headers + 0xcb, 0xa1, 0x00, // tagged signature + }, + wantErr: "cbor: CBOR tag isn't allowed", + }, + { + name: "nil signature", + data: []byte{ + 0x83, + 0x40, 0xa0, // empty headers + 0xf6, // nil signature + }, + wantErr: "empty signature", + }, + { + name: "empty signature", + data: []byte{ + 0x83, + 0x40, 0xa0, // empty headers + 0x40, // empty signature + }, + wantErr: "empty signature", + }, + { + name: "mismatch type", + data: []byte{ + 0x40, + }, + wantErr: "cbor: invalid Signature object", + }, + { + name: "smaller array size", + data: []byte{ + 0x82, + 0x40, 0xa0, // empty headers + }, + wantErr: "cbor: invalid Signature object", + }, + { + name: "larger array size", + data: []byte{ + 0x84, + 0x40, 0xa0, // empty headers + 0x41, 0x00, // signature + 0x40, + }, + wantErr: "cbor: invalid Signature object", + }, + { + name: "signature as a byte array", + data: []byte{ + 0x83, + 0x40, 0xa0, // empty headers + 0x81, 0x00, // signature + }, + wantErr: "cbor: require bstr type", + }, + { + name: "protected has IV and unprotected has PartialIV", + data: []byte{ + 0x83, + 0x46, 0xa1, 0x5, 0x63, 0x66, 0x6f, 0x6f, // protected + 0xa1, 0x6, 0x63, 0x62, 0x61, 0x72, // unprotected + 0x43, 0x62, 0x61, 0x72, // signature + }, + wantErr: "cbor: invalid protected header: protected header: header parameter: IV: require bstr type", + }, + { + name: "protected has PartialIV and unprotected has IV", + data: []byte{ + 0x83, + 0x46, 0xa1, 0x6, 0x63, 0x66, 0x6f, 0x6f, // protected + 0xa1, 0x5, 0x63, 0x62, 0x61, 0x72, // unprotected + 0x43, 0x62, 0x61, 0x72, // signature + }, + wantErr: "cbor: invalid protected header: protected header: header parameter: Partial IV: require bstr type", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got Countersignature + err := got.UnmarshalCBOR(tt.data) + if err != nil && (err.Error() != tt.wantErr) { + t.Errorf("Countersignature.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } else if err == nil && (tt.wantErr != "") { + t.Errorf("Countersignature.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Countersignature.MarshalCBOR() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCountersignature_Sign(t *testing.T) { + // generate key and set up signer / verifier + alg := AlgorithmES256 + key := generateTestECDSAKey(t) + signer, err := NewSigner(alg, key) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + verifier, err := NewVerifier(alg, key.Public()) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + + // sign / verify round trip + type args struct { + parent any + external []byte + } + tests := []struct { + name string + sig *Countersignature + onSign args + onVerify args + wantErr string + check func(t *testing.T, s *Countersignature) + }{ + { + name: "valid message", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "valid message with external", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + }, + { + name: "nil external", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + }, + { + name: "mixed nil / empty external", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + }, + { + name: "nil payload", // payload is detached + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + }, + }, + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: nil, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: nil, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "missing payload", + }, + { + name: "mismatch algorithm", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "algorithm mismatch: signer ES256: header ES512", + }, + { + name: "missing algorithm", + sig: &Countersignature{}, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + check: func(t *testing.T, s *Countersignature) { + got, err := s.Headers.Protected.Algorithm() + if err != nil { + t.Errorf("Countersignature.Headers.Protected.Algorithm() error = %v", err) + } + if got != alg { + t.Errorf("Countersignature.Headers.Protected.Algorithm() = %v, want %v", got, alg) + } + }, + }, + { + name: "missing algorithm with raw protected", + sig: &Countersignature{ + Headers: Headers{ + RawProtected: []byte{0x40}, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "algorithm not found", + }, + { + name: "missing algorithm with externally supplied data", + sig: &Countersignature{}, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + check: func(t *testing.T, s *Countersignature) { + _, err := s.Headers.Protected.Algorithm() + if want := ErrAlgorithmNotFound; err != want { + t.Errorf("Countersignature.Headers.Protected.Algorithm() error = %v, wantErr %v", err, want) + } + }, + }, + { + name: "double signing", + sig: &Countersignature{ + Signature: []byte("foobar"), + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "Countersignature already has signature bytes", + }, + { + name: "nil countersignature", + sig: nil, + onSign: args{}, + onVerify: args{}, + wantErr: "signing nil Countersignature", + }, + { + name: "empty body protected header, zero-length byte string is used", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + { + name: "invalid protected header", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelCritical: []any{}, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelCritical: []any{}, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "protected header: header parameter: crit: empty crit header", + }, + { + name: "countersign a Signature that was not signed is not allowed", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + external: []byte{}, + }, + wantErr: "Signature was not signed yet", + }, + { + name: "countersign a valid SignMessage", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a SignMessage without signatures", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + onVerify: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + wantErr: "SignMessage has no signatures yet", + }, + { + name: "countersign a valid Sign1Message", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: []byte("hello world"), + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: []byte("hello world"), + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a Sign1Message without signature", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + wantErr: "Sign1Message was not signed yet", + }, + { + name: "countersign a valid Countersignature", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a Countersignature without signature is not allowed", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + }, + external: []byte{}, + }, + wantErr: "Countersignature was not signed yet", + }, + { + name: "countersign an unsupported parent", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + onSign: args{ + parent: struct{}{}, + external: []byte{}, + }, + onVerify: args{ + parent: struct{}{}, + external: []byte{}, + }, + wantErr: "unsupported target struct {}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.sig.Sign(rand.Reader, signer, tt.onSign.parent, tt.onSign.external) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Countersignature.Sign() error = %v, wantErr %v", err, tt.wantErr) + } + return + } else if tt.wantErr != "" { + t.Errorf("Countersignature.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.check != nil { + tt.check(t, tt.sig) + } + if err := tt.sig.Verify(verifier, tt.onVerify.parent, tt.onVerify.external); err != nil { + t.Errorf("Countersignature.Verify() error = %v", err) + } + }) + } +} + +func TestCountersign0(t *testing.T) { + // generate key and set up signer / verifier + alg := AlgorithmES256 + key := generateTestECDSAKey(t) + signer, err := NewSigner(alg, key) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + verifier, err := NewVerifier(alg, key.Public()) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + + // sign / verify round trip + type args struct { + parent any + external []byte + } + tests := []struct { + name string + onSign args + onVerify args + wantErr string + }{ + { + name: "valid message", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "valid message with external", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte("foo"), + }, + }, + { + name: "nil external", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + }, + { + name: "mixed nil / empty external", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: nil, + }, + }, + { + name: "nil payload", // payload is detached + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: nil, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: nil, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "missing payload", + }, + { + name: "empty body protected header, zero-length byte string is used", + onSign: args{ + parent: Signature{ + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + { + name: "invalid protected header", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelCritical: []any{}, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + HeaderLabelCritical: []any{}, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + wantErr: "protected header: header parameter: crit: empty crit header", + }, + { + name: "countersign a Signature that was not signed is not allowed", + onSign: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + external: []byte{}, + }, + wantErr: "Signature was not signed yet", + }, + { + name: "countersign a valid SignMessage", + onSign: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a SignMessage without signatures", + onSign: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + onVerify: args{ + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + wantErr: "SignMessage has no signatures yet", + }, + { + name: "countersign a valid Sign1Message", + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: []byte("hello world"), + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Payload: []byte("hello world"), + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a Sign1Message without signature", + onSign: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + onVerify: args{ + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("hello world"), + }, + external: []byte{}, + }, + wantErr: "Sign1Message was not signed yet", + }, + { + name: "countersign a valid Countersignature", + onSign: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + Signature: []byte{ + 0x74, 0xc6, 0xac, 0xa6, 0x7d, 0x7a, 0x00, 0xea, + 0x0f, 0x9b, 0x86, 0xb3, 0x85, 0x7a, 0x7d, 0x36, + 0xd2, 0x77, 0x91, 0x73, 0x40, 0x09, 0x35, 0x4e, + 0x8c, 0x9f, 0xd6, 0x03, 0x37, 0xab, 0x43, 0xf5, + }, + }, + external: []byte{}, + }, + }, + { + name: "countersign a Countersignature without signature is not allowed", + onSign: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + }, + external: []byte{}, + }, + onVerify: args{ + parent: Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{}, + Unprotected: UnprotectedHeader{}, + }, + }, + external: []byte{}, + }, + wantErr: "Countersignature was not signed yet", + }, + { + name: "countersign an unsupported parent", + onSign: args{ + parent: struct{}{}, + external: []byte{}, + }, + onVerify: args{ + parent: struct{}{}, + external: []byte{}, + }, + wantErr: "unsupported target struct {}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sig, err := Countersign0(rand.Reader, signer, tt.onSign.parent, tt.onSign.external) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Countersign0() error = %v, wantErr %v", err, tt.wantErr) + } + return + } else if tt.wantErr != "" { + t.Errorf("Countersign0() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err := VerifyCountersign0(verifier, tt.onVerify.parent, tt.onVerify.external, sig); err != nil { + t.Errorf("VerifyCountersign0() error = %v", err) + } + }) + } +} + +func TestCountersignature_Sign_Internal(t *testing.T) { + tests := []struct { + name string + sig *Countersignature + parent any + external []byte + toBeSigned []byte + }{ + { + // adapted from https://github.com/cose-wg/Examples/blob/master/countersign/signed1-01.json + // by modifying the context to "CounterSignatureV2" (to adjust to RFC 9338), including the + // signature as other_fields and altering the countersignature algorithm. + name: "COSE_Sign1 countersignature conformance test", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: algorithmMock, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + parent: Sign1Message{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEdDSA, + HeaderLabelContentType: 0, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Payload: []byte("This is the content."), + Signature: []byte{ + 0x71, 0x42, 0xfd, 0x2f, 0xf9, 0x6d, 0x56, 0xdb, + 0x85, 0xbe, 0xe9, 0x05, 0xa7, 0x6b, 0xa1, 0xd0, + 0xb7, 0x32, 0x1a, 0x95, 0xc8, 0xc4, 0xd3, 0x60, + 0x7c, 0x57, 0x81, 0x93, 0x2b, 0x7a, 0xfb, 0x87, + 0x11, 0x49, 0x7d, 0xfa, 0x75, 0x1b, 0xf4, 0x0b, + 0x58, 0xb3, 0xbc, 0xc3, 0x23, 0x00, 0xb1, 0x48, + 0x7f, 0x3d, 0xb3, 0x40, 0x85, 0xee, 0xf0, 0x13, + 0xbf, 0x08, 0xf4, 0xa4, 0x4d, 0x6f, 0xef, 0x0d, + }, + }, + toBeSigned: []byte{ + 0x86, // array(6) + 0x72, // text(18) "CounterSignatureV2" + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x56, 0x32, + 0x45, // bytes(5) + 0xa2, 0x01, 0x27, 0x03, 0x00, + 0x47, // bytes(7) + 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, + 0x40, // bytes(0) + 0x54, // bytes(20) "This is the content." + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x2e, + 0x81, // array(1) + 0x58, 0x40, // bytes(64) signature: + 0x71, 0x42, 0xfd, 0x2f, 0xf9, 0x6d, 0x56, 0xdb, + 0x85, 0xbe, 0xe9, 0x05, 0xa7, 0x6b, 0xa1, 0xd0, + 0xb7, 0x32, 0x1a, 0x95, 0xc8, 0xc4, 0xd3, 0x60, + 0x7c, 0x57, 0x81, 0x93, 0x2b, 0x7a, 0xfb, 0x87, + 0x11, 0x49, 0x7d, 0xfa, 0x75, 0x1b, 0xf4, 0x0b, + 0x58, 0xb3, 0xbc, 0xc3, 0x23, 0x00, 0xb1, 0x48, + 0x7f, 0x3d, 0xb3, 0x40, 0x85, 0xee, 0xf0, 0x13, + 0xbf, 0x08, 0xf4, 0xa4, 0x4d, 0x6f, 0xef, 0x0d, + }, + }, + { + // adapted from https://github.com/cose-wg/Examples/blob/master/countersign/signed-01.json + name: "COSE_Signature countersignature conformance test", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: algorithmMock, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + parent: Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEdDSA, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0x8e, 0x1b, 0xe2, 0xf9, 0x45, 0x3d, 0x26, 0x48, + 0x12, 0xe5, 0x90, 0x49, 0x91, 0x32, 0xbe, 0xf3, + 0xfb, 0xf9, 0xee, 0x9d, 0xb2, 0x7c, 0x2c, 0x16, + 0x87, 0x88, 0xe3, 0xb7, 0xeb, 0xe5, 0x06, 0xc0, + 0x4f, 0xd3, 0xd1, 0x9f, 0xaa, 0x9f, 0x51, 0x23, + 0x2a, 0xf5, 0xc9, 0x59, 0xe4, 0xef, 0x47, 0x92, + 0x88, 0x34, 0x64, 0x7f, 0x56, 0xdf, 0xbe, 0x93, + 0x91, 0x12, 0x88, 0x4d, 0x08, 0xef, 0x25, 0x05, + }, + }, + toBeSigned: []byte{ + 0x85, // array(5) + 0x70, // text(16) "CounterSignature" + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x43, // bytes(3) + 0xa1, 0x01, 0x27, + 0x47, // bytes(7) + 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, + 0x40, // bytes(0) + 0x58, 0x40, // bytes(64) signature: + 0x8e, 0x1b, 0xe2, 0xf9, 0x45, 0x3d, 0x26, 0x48, + 0x12, 0xe5, 0x90, 0x49, 0x91, 0x32, 0xbe, 0xf3, + 0xfb, 0xf9, 0xee, 0x9d, 0xb2, 0x7c, 0x2c, 0x16, + 0x87, 0x88, 0xe3, 0xb7, 0xeb, 0xe5, 0x06, 0xc0, + 0x4f, 0xd3, 0xd1, 0x9f, 0xaa, 0x9f, 0x51, 0x23, + 0x2a, 0xf5, 0xc9, 0x59, 0xe4, 0xef, 0x47, 0x92, + 0x88, 0x34, 0x64, 0x7f, 0x56, 0xdf, 0xbe, 0x93, + 0x91, 0x12, 0x88, 0x4d, 0x08, 0xef, 0x25, 0x05, + }, + }, + { + // adapted from https://github.com/cose-wg/Examples/blob/master/countersign/signed-03.json + name: "COSE_Sign countersignature conformance test", + sig: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: algorithmMock, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + parent: SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: 0, + }, + Unprotected: UnprotectedHeader{}, + }, + Payload: []byte("This is the content."), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEdDSA, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0x77, 0xf3, 0xea, 0xcd, 0x11, 0x85, 0x2c, 0x4b, + 0xf9, 0xcb, 0x1d, 0x72, 0xfa, 0xbe, 0x6b, 0x26, + 0xfb, 0xa1, 0xd7, 0x60, 0x92, 0xb2, 0xb5, 0xb7, + 0xec, 0x83, 0xb8, 0x35, 0x57, 0x65, 0x22, 0x64, + 0xe6, 0x96, 0x90, 0xdb, 0xc1, 0x17, 0x2d, 0xdc, + 0x0b, 0xf8, 0x84, 0x11, 0xc0, 0xd2, 0x5a, 0x50, + 0x7f, 0xdb, 0x24, 0x7a, 0x20, 0xc4, 0x0d, 0x5e, + 0x24, 0x5f, 0xab, 0xd3, 0xfc, 0x9e, 0xc1, 0x06, + }, + }, + }, + }, + toBeSigned: []byte{ + 0x85, // array(5) + 0x70, // text(16) "CounterSignature" + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x43, // bytes(3) + 0xa1, 0x03, 0x00, + 0x47, // bytes(7) + 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, + 0x40, // bytes(0) + 0x54, // bytes(20) "This is the content." + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x2e, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + want := make([]byte, 64) + _, err := rand.Read(want) + if err != nil { + t.Fatalf("rand.Read() error = %v", err) + } + signer := newMockSigner(t) + signer.setup(tt.toBeSigned, want) + + sig := tt.sig + if err := sig.Sign(rand.Reader, signer, tt.parent, tt.external); err != nil { + t.Errorf("Countersignature.Sign() error = %v", err) + return + } + if got := sig.Signature; !bytes.Equal(got, want) { + t.Errorf("Countersignature.Sign() signature = %s, want %s", + hex.EncodeToString(got), + hex.EncodeToString(want)) + } + }) + } +} diff --git a/example_test.go b/example_test.go index afdf128..80f909d 100644 --- a/example_test.go +++ b/example_test.go @@ -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 reference + 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 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 +} diff --git a/headers.go b/headers.go index 2da2356..1daaaaa 100644 --- a/headers.go +++ b/headers.go @@ -12,18 +12,20 @@ import ( // // Reference: https://www.iana.org/assignments/cose/cose.xhtml#header-parameters const ( - HeaderLabelAlgorithm int64 = 1 - HeaderLabelCritical int64 = 2 - HeaderLabelContentType int64 = 3 - HeaderLabelKeyID int64 = 4 - HeaderLabelIV int64 = 5 - HeaderLabelPartialIV int64 = 6 - HeaderLabelCounterSignature int64 = 7 - HeaderLabelCounterSignature0 int64 = 9 - HeaderLabelX5Bag int64 = 32 - HeaderLabelX5Chain int64 = 33 - HeaderLabelX5T int64 = 34 - HeaderLabelX5U int64 = 35 + HeaderLabelAlgorithm int64 = 1 + HeaderLabelCritical int64 = 2 + HeaderLabelContentType int64 = 3 + HeaderLabelKeyID int64 = 4 + HeaderLabelIV int64 = 5 + HeaderLabelPartialIV int64 = 6 + HeaderLabelCounterSignature int64 = 7 + HeaderLabelCounterSignature0 int64 = 9 + HeaderLabelCounterSignatureV2 int64 = 11 + HeaderLabelCounterSignature0V2 int64 = 12 + HeaderLabelX5Bag int64 = 32 + HeaderLabelX5Chain int64 = 33 + HeaderLabelX5T int64 = 34 + HeaderLabelX5U int64 = 35 ) // ProtectedHeader contains parameters that are to be cryptographically @@ -197,10 +199,22 @@ func (h *UnprotectedHeader) UnmarshalCBOR(data []byte) error { if err := validateHeaderLabelCBOR(data); err != nil { return err } - var header map[any]any - if err := decMode.Unmarshal(data, &header); err != nil { + + // In order to unmarshal Countersignature structs, it is required to make it + // in two steps instead of one. + var partialHeader map[any]cbor.RawMessage + if err := decMode.Unmarshal(data, &partialHeader); err != nil { return err } + header := make(map[any]any, len(partialHeader)) + for k, v := range partialHeader { + v, err := unmarshalUnprotected(k, v) + if err != nil { + return err + } + header[k] = v + } + if err := validateHeaderParameters(header, false); err != nil { return fmt.Errorf("unprotected header: %w", err) } @@ -208,6 +222,47 @@ func (h *UnprotectedHeader) UnmarshalCBOR(data []byte) error { return nil } +// unmarshalUnprotected produces known structs such as counter signature +// headers, otherwise it defaults to regular unmarshaling to simple types. +func unmarshalUnprotected(key any, value cbor.RawMessage) (any, error) { + label, ok := normalizeLabel(key) + if ok { + switch label { + case HeaderLabelCounterSignature, HeaderLabelCounterSignatureV2: + return unmarshalAsCountersignature(value) + default: + } + } + + return unmarshalAsAny(value) +} + +// unmarshalAsCountersignature produces a Countersignature struct or a list of +// Countersignatures. +func unmarshalAsCountersignature(value cbor.RawMessage) (any, error) { + var result1 Countersignature + err := decMode.Unmarshal(value, &result1) + if err == nil { + return &result1, nil + } + var result2 []*Countersignature + err = decMode.Unmarshal(value, &result2) + if err == nil { + return result2, nil + } + return nil, errors.New("invalid Countersignature object / list of objects") +} + +// unmarshalAsAny produces simple types. +func unmarshalAsAny(value cbor.RawMessage) (any, error) { + var result any + err := decMode.Unmarshal(value, &result) + if err != nil { + return nil, err + } + return result, nil +} + // Headers represents "two buckets of information that are not // considered to be part of the payload itself, but are used for // holding information about content, algorithms, keys, or evaluation @@ -437,6 +492,38 @@ func validateHeaderParameters(h map[any]any, protected bool) error { if hasLabel(h, HeaderLabelIV) { return errors.New("header parameter: IV and PartialIV: parameters must not both be present") } + case HeaderLabelCounterSignature: + if protected { + return errors.New("header parameter: counter signature: not allowed") + } + if _, ok := value.(*Countersignature); !ok { + if _, ok := value.([]*Countersignature); !ok { + return errors.New("header parameter: counter signature is not a Countersignature or a list") + } + } + case HeaderLabelCounterSignature0: + if protected { + return errors.New("header parameter: countersignature0: not allowed") + } + if !canBstr(value) { + return errors.New("header parameter: countersignature0: require bstr type") + } + case HeaderLabelCounterSignatureV2: + if protected { + return errors.New("header parameter: Countersignature version 2: not allowed") + } + if _, ok := value.(*Countersignature); !ok { + if _, ok := value.([]*Countersignature); !ok { + return errors.New("header parameter: Countersignature version 2 is not a Countersignature or a list") + } + } + case HeaderLabelCounterSignature0V2: + if protected { + return errors.New("header parameter: Countersignature0 version 2: not allowed") + } + if !canBstr(value) { + return errors.New("header parameter: Countersignature0 version 2: require bstr type") + } } } return nil diff --git a/headers_test.go b/headers_test.go index 2dedd44..7bcd8cf 100644 --- a/headers_test.go +++ b/headers_test.go @@ -47,10 +47,10 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { name: "various types of integer label", h: ProtectedHeader{ uint(10): 0, - uint8(11): 0, - uint16(12): 0, - uint32(13): 0, - uint64(14): 0, + uint8(13): 0, + uint16(14): 0, + uint32(15): 0, + uint64(16): 0, int(-1): 0, int8(-2): 0, int16(-3): 0, @@ -61,10 +61,10 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { 0x55, // bstr 0xaa, // map 0x0a, 0x00, - 0x0b, 0x00, - 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, + 0x0f, 0x00, + 0x10, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, 0x00, @@ -162,6 +162,20 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { }, wantErr: "protected header: header parameter: content type: require tstr / uint type", }, + { + name: "invalid counter signature", + h: ProtectedHeader{ + HeaderLabelCounterSignature: &Countersignature{}, + }, + wantErr: "protected header: header parameter: counter signature: not allowed", + }, + { + name: "invalid counter signature version 2", + h: ProtectedHeader{ + HeaderLabelCounterSignatureV2: &Countersignature{}, + }, + wantErr: "protected header: header parameter: Countersignature version 2: not allowed", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -333,6 +347,24 @@ func TestProtectedHeader_UnmarshalCBOR(t *testing.T) { }, wantErr: "protected header: header parameter: Partial IV: require bstr type", }, + { + name: "countersignature0 is not allowed", + data: []byte{ + 0x54, 0xa1, 0x09, 0x58, 0x10, + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + }, + wantErr: "protected header: header parameter: countersignature0: not allowed", + }, + { + name: "Countersignature0V2 is not allowed", + data: []byte{ + 0x54, 0xa1, 0x0c, 0x58, 0x10, + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + }, + wantErr: "protected header: header parameter: Countersignature0 version 2: not allowed", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -551,10 +583,10 @@ func TestUnprotectedHeader_MarshalCBOR(t *testing.T) { name: "various types of integer label", h: UnprotectedHeader{ uint(10): 0, - uint8(11): 0, - uint16(12): 0, - uint32(13): 0, - uint64(14): 0, + uint8(13): 0, + uint16(14): 0, + uint32(15): 0, + uint64(16): 0, int(-1): 0, int8(-2): 0, int16(-3): 0, @@ -564,10 +596,10 @@ func TestUnprotectedHeader_MarshalCBOR(t *testing.T) { want: []byte{ 0xaa, // map 0x0a, 0x00, - 0x0b, 0x00, - 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, + 0x0f, 0x00, + 0x10, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, 0x00, @@ -614,6 +646,194 @@ func TestUnprotectedHeader_MarshalCBOR(t *testing.T) { }, wantErr: "unprotected header: header parameter: crit: not allowed", }, + { + name: "malformed counter signature", + h: UnprotectedHeader{ + HeaderLabelCounterSignature: "", + }, + wantErr: "unprotected header: header parameter: counter signature is not a Countersignature or a list", + }, + { + name: "counter signature without signature", + h: UnprotectedHeader{ + HeaderLabelCounterSignature: []*Countersignature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + }, + }, + wantErr: "empty signature", + }, + { + name: "complete counter signature", + h: UnprotectedHeader{ + HeaderLabelCounterSignature: []*Countersignature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + }, + }, + want: []byte{ + 0xa1, 0x07, 0x81, 0x83, 0x43, 0xa1, 0x01, 0x27, 0xa1, + 0x04, 0x42, 0x31, 0x31, 0x58, 0x40, 0xb7, 0xca, 0xcb, + 0xa2, 0x85, 0xc4, 0xcd, 0x3e, 0xd2, 0xf0, 0x14, 0x6f, + 0x41, 0x98, 0x86, 0x14, 0x4c, 0xa6, 0x38, 0xd0, 0x87, + 0xde, 0x12, 0x3d, 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, + 0xab, 0xc4, 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, + 0xb7, 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, 0xf2, + 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + { + name: "malformed Countersignature version 2", + h: UnprotectedHeader{ + HeaderLabelCounterSignatureV2: "", + }, + wantErr: "unprotected header: header parameter: Countersignature version 2 is not a Countersignature or a list", + }, + { + name: "Countersignature version 2 without signature", + h: UnprotectedHeader{ + HeaderLabelCounterSignatureV2: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + }, + }, + wantErr: "empty signature", + }, + { + name: "complete Countersignature version 2", + h: UnprotectedHeader{ + HeaderLabelCounterSignatureV2: &Countersignature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + }, + want: []byte{ + 0xa1, 0x0b, 0x83, 0x43, 0xa1, 0x01, 0x27, 0xa1, 0x04, + 0x42, 0x31, 0x31, 0x58, 0x40, 0xb7, 0xca, 0xcb, 0xa2, + 0x85, 0xc4, 0xcd, 0x3e, 0xd2, 0xf0, 0x14, 0x6f, 0x41, + 0x98, 0x86, 0x14, 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, + 0x12, 0x3d, 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, + 0xc4, 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, 0xfe, + 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, 0xf2, 0x43, + 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + { + name: "complete countersignature0", + h: UnprotectedHeader{ + HeaderLabelCounterSignature0: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + want: []byte{ + 0xa1, 0x09, 0x58, 0x40, + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + { + name: "invalid countersignature0", + h: UnprotectedHeader{ + HeaderLabelCounterSignature0: "11", + }, + wantErr: "unprotected header: header parameter: countersignature0: require bstr type", + }, + { + name: "complete Countersignature0 version 2", + h: UnprotectedHeader{ + HeaderLabelCounterSignature0V2: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + want: []byte{ + 0xa1, 0x0c, 0x58, 0x40, + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + { + name: "invalid Countersignature0 version 2", + h: UnprotectedHeader{ + HeaderLabelCounterSignature0V2: "11", + }, + wantErr: "unprotected header: header parameter: Countersignature0 version 2: require bstr type", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -739,7 +959,110 @@ func TestUnprotectedHeader_UnmarshalCBOR(t *testing.T) { }, wantErr: "unprotected header: header parameter: crit: not allowed", }, + { + name: "single counter signature present", + data: []byte{ + 0xa1, // { + 0x07, 0x83, // / counter signature / 7: [ + 0x43, 0xa1, 0x01, 0x27, // / protected h'a10127' / << { / alg / 1:-8 / EdDSA / } >>, + 0xa1, 0x04, 0x42, 0x31, 0x31, // / unprotected / { / kid / 4: '11' }, + 0x58, 0x40, // bytes(64) + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + want: UnprotectedHeader{ + HeaderLabelCounterSignature: &Countersignature{ + Headers: Headers{ + RawProtected: []byte{0x43, 0xa1, 0x01, 0x27}, + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + RawUnprotected: []byte{0xa1, 0x04, 0x42, 0x31, 0x31}, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + }, + }, + { + name: "CountersignatureV2 in a list", + data: []byte{ + 0xa1, // { + 0x0b, 0x81, 0x83, // / counter signature / 7: [ [ + 0x43, 0xa1, 0x01, 0x27, // / protected h'a10127' / << { / alg / 1:-8 / EdDSA / } >>, + 0xa1, 0x04, 0x42, 0x31, 0x31, // / unprotected / { / kid / 4: '11' }, + 0x58, 0x40, // bytes(64) + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + want: UnprotectedHeader{ + HeaderLabelCounterSignatureV2: []*Countersignature{ + { + Headers: Headers{ + RawProtected: []byte{0x43, 0xa1, 0x01, 0x27}, + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmEd25519, + }, + RawUnprotected: []byte{0xa1, 0x04, 0x42, 0x31, 0x31}, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("11"), + }, + }, + Signature: []byte{ + 0xb7, 0xca, 0xcb, 0xa2, 0x85, 0xc4, 0xcd, 0x3e, + 0xd2, 0xf0, 0x14, 0x6f, 0x41, 0x98, 0x86, 0x14, + 0x4c, 0xa6, 0x38, 0xd0, 0x87, 0xde, 0x12, 0x3d, + 0x40, 0x01, 0x67, 0x30, 0x8a, 0xce, 0xab, 0xc4, + 0xb5, 0xe5, 0xc6, 0xa4, 0x0c, 0x0d, 0xe0, 0xb7, + 0x11, 0x67, 0xa3, 0x91, 0x75, 0xea, 0x56, 0xc1, + 0xfe, 0x96, 0xc8, 0x9e, 0x5e, 0x7d, 0x30, 0xda, + 0xf2, 0x43, 0x8a, 0x45, 0x61, 0x59, 0xa2, 0x0a, + }, + }, + }, + }, + }, + { + name: "counter signature should be object or list", + data: []byte{ + 0xa1, // { + 0x07, 0x42, 0xf0, 0x0d, // / counter signature / 7: h'f00d' + }, + wantErr: "invalid Countersignature object / list of objects", + }, + { + name: "CountersignatureV2 should be object or list", + data: []byte{ + 0xa1, // { + 0x0b, 0x42, 0xf0, 0x0d, // / counter signature / 11: h'f00d' + }, + wantErr: "invalid Countersignature object / list of objects", + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got UnprotectedHeader