diff --git a/.github/.codecov.yml b/.github/.codecov.yml index 7c6d863..4b598be 100644 --- a/.github/.codecov.yml +++ b/.github/.codecov.yml @@ -1,5 +1,6 @@ coverage: status: + patch: off project: default: - target: 89% \ No newline at end of file + target: 89% diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9693a83..8e8e9e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.18, 1.19] + go-version: [1.21, 1.22] runs-on: ubuntu-latest steps: - name: Install Go diff --git a/.gitignore b/.gitignore index 4d0f184..b04f6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ vendor/ .DS_Store /cose-fuzz.zip /workdir/ + +# Editor files +.vscode/ diff --git a/CODEOWNERS b/CODEOWNERS index 767969e..c3ebac9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # To be kept in sync with: [community/OWNERS](https://github.com/veraison/community/blob/main/OWNERS) # and the GitHub Team: [go-cose-maintainers](https://github.com/orgs/veraison/teams/go-cose-maintainers) -* henkbirkholz qmuntal roywill setrofim shizhMSFT simonfrost-arm SteveLasker thomas-fossati yogeshbdeshpande +* @henkbirkholz @qmuntal @roywill @setrofim @shizhMSFT @simonfrost-arm @SteveLasker @thomas-fossati @yogeshbdeshpande diff --git a/README.md b/README.md index 74a8f77..0de72d3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A golang library for the [COSE specification][cose-spec] ## Project Status -**Current Release**: [go-cose v1.0.0][current-release] +**Current Release**: [go-cose v1.1.0][current-release] The project was *initially* forked from the upstream [mozilla-services/go-cose][mozilla-go-cose] project, however the Veraison and Mozilla maintainers have agreed to retire the mozilla-services/go-cose project and focus on [veraison/go-cose][veraison-go-cose] as the active project. @@ -137,6 +137,18 @@ See [example_test.go](./example_test.go) for more examples. Untagged COSE_Sign1 messages can be signed and verified as above, using `cose.UntaggedSign1Message` instead of `cose.Sign1Message`. +#### Signing and Verification of payload digest + +When `cose.NewSigner` is used with PS{256,384,512} or ES{256,384,512}, the returned signer +can be casted to the `cose.DigestSigner` interface, whose `SignDigest` method signs an +already digested message. + +When `cose.NewVerifier` is used with PS{256,384,512} or ES{256,384,512}, the returned verifier +can be casted to the `cose.DigestVerifier` interface, whose `VerifyDigest` method verifies an +already digested message. + +Please refer to [example_test.go](./example_test.go) for the API usage. + ### About hashing `go-cose` does not import any hash package by its own to avoid linking unnecessary algorithms to the final binary. @@ -154,7 +166,21 @@ These are the required packages for each built-in cose.Algorithm: - cose.AlgorithmPS256, cose.AlgorithmES256: `crypto/sha256` - cose.AlgorithmPS384, cose.AlgorithmPS512, cose.AlgorithmES384, cose.AlgorithmES512: `crypto/sha512` -- cose.AlgorithmEd25519: none +- 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 @@ -165,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: @@ -212,4 +243,4 @@ go test -fuzz=FuzzSign1 [mozilla-contributors]: https://github.com/mozilla-services/go-cose/graphs/contributors [mozilla-go-cose]: http://github.com/mozilla-services/go-cose [veraison-go-cose]: https://github.com/veraison/go-cose -[current-release]: https://github.com/veraison/go-cose/releases/tag/v1.0.0 +[current-release]: https://github.com/veraison/go-cose/releases/tag/v1.1.0 diff --git a/algorithm.go b/algorithm.go index 7b95ed7..40d5256 100644 --- a/algorithm.go +++ b/algorithm.go @@ -35,11 +35,38 @@ const ( AlgorithmES512 Algorithm = -36 // PureEdDSA by RFC 8152. + // + // Deprecated: use AlgorithmEdDSA instead, which has + // the same value but with a more accurate name. AlgorithmEd25519 Algorithm = -8 + + // PureEdDSA by RFC 8152. + AlgorithmEdDSA Algorithm = -8 + + // Reserved value. + AlgorithmReserved Algorithm = 0 +) + +// Algorithms known, but not supported by this library. +// +// Signers and Verifiers requiring the algorithms below are not +// directly supported by this library. They need to be provided +// as an external [cose.Signer] or [cose.Verifier] implementation. +// +// An example use case where RS256 is allowed and used is in +// WebAuthn: https://www.w3.org/TR/webauthn-2/#sctn-sample-registration. +const ( + // RSASSA-PKCS1-v1_5 using SHA-256 by RFC 8812. + AlgorithmRS256 Algorithm = -257 + + // RSASSA-PKCS1-v1_5 using SHA-384 by RFC 8812. + AlgorithmRS384 Algorithm = -258 + + // RSASSA-PKCS1-v1_5 using SHA-512 by RFC 8812. + AlgorithmRS512 Algorithm = -259 ) // Algorithm represents an IANA algorithm entry in the COSE Algorithms registry. -// Algorithms with string values are not supported. // // # See Also // @@ -57,18 +84,26 @@ func (a Algorithm) String() string { return "PS384" case AlgorithmPS512: return "PS512" + case AlgorithmRS256: + return "RS256" + case AlgorithmRS384: + return "RS384" + case AlgorithmRS512: + return "RS512" case AlgorithmES256: return "ES256" case AlgorithmES384: return "ES384" case AlgorithmES512: return "ES512" - case AlgorithmEd25519: + case AlgorithmEdDSA: // As stated in RFC 8152 8.2, only the pure EdDSA version is used for // COSE. return "EdDSA" + case AlgorithmReserved: + return "Reserved" default: - return "unknown algorithm value " + strconv.Itoa(int(a)) + return "Algorithm(" + strconv.Itoa(int(a)) + ")" } } diff --git a/algorithm_test.go b/algorithm_test.go index 7ccafeb..e2985cf 100644 --- a/algorithm_test.go +++ b/algorithm_test.go @@ -12,53 +12,24 @@ import ( func TestAlgorithm_String(t *testing.T) { // run tests tests := []struct { - name string alg Algorithm want string }{ - { - name: "PS256", - alg: AlgorithmPS256, - want: "PS256", - }, - { - name: "PS384", - alg: AlgorithmPS384, - want: "PS384", - }, - { - name: "PS512", - alg: AlgorithmPS512, - want: "PS512", - }, - { - name: "ES256", - alg: AlgorithmES256, - want: "ES256", - }, - { - name: "ES384", - alg: AlgorithmES384, - want: "ES384", - }, - { - name: "ES512", - alg: AlgorithmES512, - want: "ES512", - }, - { - name: "Ed25519", - alg: AlgorithmEd25519, - want: "EdDSA", - }, - { - name: "unknown algorithm", - alg: 0, - want: "unknown algorithm value 0", - }, + {AlgorithmPS256, "PS256"}, + {AlgorithmPS384, "PS384"}, + {AlgorithmPS512, "PS512"}, + {AlgorithmRS256, "RS256"}, + {AlgorithmRS384, "RS384"}, + {AlgorithmRS512, "RS512"}, + {AlgorithmES256, "ES256"}, + {AlgorithmES384, "ES384"}, + {AlgorithmES512, "ES512"}, + {AlgorithmEdDSA, "EdDSA"}, + {AlgorithmReserved, "Reserved"}, + {7, "Algorithm(7)"}, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.want, func(t *testing.T) { if got := tt.alg.String(); got != tt.want { t.Errorf("Algorithm.String() = %v, want %v", got, tt.want) } @@ -131,7 +102,7 @@ func TestAlgorithm_computeHash(t *testing.T) { }, { name: "Ed25519", - alg: AlgorithmEd25519, + alg: AlgorithmEdDSA, wantErr: ErrUnavailableHashFunc, }, { diff --git a/cbor.go b/cbor.go index 15bdc54..883f52b 100644 --- a/cbor.go +++ b/cbor.go @@ -95,7 +95,7 @@ func deterministicBinaryString(data cbor.RawMessage) (cbor.RawMessage, error) { } // fast path: return immediately if bstr is already deterministic - if err := decModeWithTagsForbidden.Valid(data); err != nil { + if err := decModeWithTagsForbidden.Wellformed(data); err != nil { return nil, err } ai := data[0] & 0x1f diff --git a/conformance_test.go b/conformance_test.go index cb76135..ae4e719 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -308,6 +308,12 @@ func mustNameToAlg(name string) cose.Algorithm { return cose.AlgorithmPS384 case "PS512": return cose.AlgorithmPS512 + case "RS256": + return cose.AlgorithmRS256 + case "RS384": + return cose.AlgorithmRS384 + case "RS512": + return cose.AlgorithmRS512 case "ES256": return cose.AlgorithmES256 case "ES384": 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/cwt.go b/cwt.go new file mode 100644 index 0000000..e2aa9a2 --- /dev/null +++ b/cwt.go @@ -0,0 +1,20 @@ +package cose + +// https://www.iana.org/assignments/cwt/cwt.xhtml#claims-registry +const ( + CWTClaimIssuer int64 = 1 + CWTClaimSubject int64 = 2 + CWTClaimAudience int64 = 3 + CWTClaimExpirationTime int64 = 4 + CWTClaimNotBefore int64 = 5 + CWTClaimIssuedAt int64 = 6 + CWTClaimCWTID int64 = 7 + CWTClaimConfirmation int64 = 8 + CWTClaimScope int64 = 9 + + // TODO: the rest upon request +) + +// CWTClaims contains parameters that are to be cryptographically +// protected. +type CWTClaims map[any]any diff --git a/cwt_test.go b/cwt_test.go new file mode 100644 index 0000000..93dd0ae --- /dev/null +++ b/cwt_test.go @@ -0,0 +1,82 @@ +package cose_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + + "github.com/veraison/go-cose" +) + +// This example demonstrates signing and verifying COSE_Sign1 signatures. +func ExampleCWTMessage() { + // create message to be signed + msgToSign := cose.NewSign1Message() + msgToSign.Payload = []byte("hello world") + msgToSign.Headers.Protected.SetAlgorithm(cose.AlgorithmES512) + + msgToSign.Headers.Protected.SetType("application/cwt") + claims := cose.CWTClaims{ + cose.CWTClaimIssuer: "issuer.example", + cose.CWTClaimSubject: "subject.example", + } + msgToSign.Headers.Protected.SetCWTClaims(claims) + + msgToSign.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("1") + + // create a signer + privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + panic(err) + } + signer, err := cose.NewSigner(cose.AlgorithmES512, privateKey) + if err != nil { + panic(err) + } + + // sign message + err = msgToSign.Sign(rand.Reader, nil, signer) + if err != nil { + panic(err) + } + sig, err := msgToSign.MarshalCBOR() + // uncomment to review EDN + // coseSign1Diagnostic, err := cbor.Diagnose(sig) + // fmt.Println(coseSign1Diagnostic) + if err != nil { + panic(err) + } + fmt.Println("message signed") + + // create a verifier from a trusted public key + publicKey := privateKey.Public() + verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey) + if err != nil { + panic(err) + } + + // verify message + var msgToVerify cose.Sign1Message + err = msgToVerify.UnmarshalCBOR(sig) + if err != nil { + panic(err) + } + err = msgToVerify.Verify(nil, verifier) + if err != nil { + panic(err) + } + fmt.Println("message verified") + + // tamper the message and verification should fail + msgToVerify.Payload = []byte("foobar") + err = msgToVerify.Verify(nil, verifier) + if err != cose.ErrVerification { + panic(err) + } + fmt.Println("verification error as expected") + // Output: + // message signed + // message verified + // verification error as expected +} diff --git a/ecdsa.go b/ecdsa.go index 7e426be..d1ae0e9 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -57,6 +57,13 @@ func (es *ecdsaKeySigner) Sign(rand io.Reader, content []byte) ([]byte, error) { if err != nil { return nil, err } + return es.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (es *ecdsaKeySigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { r, s, err := ecdsa.Sign(rand, es.key, digest) if err != nil { return nil, err @@ -86,6 +93,13 @@ func (es *ecdsaCryptoSigner) Sign(rand io.Reader, content []byte) ([]byte, error if err != nil { return nil, err } + return es.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (es *ecdsaCryptoSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { sigASN1, err := es.signer.Sign(rand, digest, nil) if err != nil { return nil, err @@ -153,7 +167,15 @@ func (ev *ecdsaVerifier) Verify(content []byte, signature []byte) error { if err != nil { return err } + return ev.VerifyDigest(digest, signature) +} +// VerifyDigest verifies message digest with the public key, returning nil +// for success. +// Otherwise, it returns ErrVerification. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 +func (ev *ecdsaVerifier) VerifyDigest(digest []byte, signature []byte) error { // verify signature r, s, err := decodeECDSASignature(ev.key.Curve, signature) if err != nil { diff --git a/ecdsa_test.go b/ecdsa_test.go index 308ca6a..e549c02 100644 --- a/ecdsa_test.go +++ b/ecdsa_test.go @@ -172,18 +172,6 @@ func generateTestECDSAKey(t *testing.T) *ecdsa.PrivateKey { return key } -func Test_customCurveKeySigner(t *testing.T) { - // https://github.com/veraison/go-cose/issues/59 - pCustom := *elliptic.P256().Params() - pCustom.Name = "P-custom" - pCustom.BitSize /= 2 - key, err := ecdsa.GenerateKey(&pCustom, rand.Reader) - if err != nil { - t.Fatalf("ecdsa.GenerateKey() error = %v", err) - } - testSignVerify(t, AlgorithmES256, key, false) -} - func Test_ecdsaKeySigner(t *testing.T) { key := generateTestECDSAKey(t) testSignVerify(t, AlgorithmES256, key, false) @@ -219,7 +207,7 @@ func testSignVerify(t *testing.T, alg Algorithm, key crypto.Signer, isCryptoSign // sign / verify round trip // see also conformance_test.go for strict tests. - content := []byte("hello world") + content := []byte("hello world, مرحبا بالعالم") sig, err := signer.Sign(rand.Reader, content) if err != nil { t.Fatalf("Sign() error = %v", err) @@ -232,6 +220,24 @@ func testSignVerify(t *testing.T, alg Algorithm, key crypto.Signer, isCryptoSign if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + // digested sign/verify round trip + dsigner, ok := signer.(DigestSigner) + if !ok { + t.Fatalf("signer is not a DigestSigner") + } + digest := sha256.Sum256(content) + dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) + if err != nil { + t.Fatalf("SignDigest() error = %v", err) + } + dverifier, ok := verifier.(DigestVerifier) + if !ok { + t.Fatalf("verifier is not a DigestVerifier") + } + if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { + t.Fatalf("VerifyDigest() error = %v", err) + } } type ecdsaBadCryptoSigner struct { diff --git a/ed25519.go b/ed25519.go index ae88740..ebbf638 100644 --- a/ed25519.go +++ b/ed25519.go @@ -13,7 +13,7 @@ type ed25519Signer struct { // Algorithm returns the signing algorithm associated with the private key. func (es *ed25519Signer) Algorithm() Algorithm { - return AlgorithmEd25519 + return AlgorithmEdDSA } // Sign signs message content with the private key, possibly using entropy from @@ -34,7 +34,7 @@ type ed25519Verifier struct { // Algorithm returns the signing algorithm associated with the public key. func (ev *ed25519Verifier) Algorithm() Algorithm { - return AlgorithmEd25519 + return AlgorithmEdDSA } // Verify verifies message content with the public key, returning nil for diff --git a/ed25519_test.go b/ed25519_test.go index d23e65b..4ecddaf 100644 --- a/ed25519_test.go +++ b/ed25519_test.go @@ -17,7 +17,7 @@ func generateTestEd25519Key(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey func Test_ed25519Signer(t *testing.T) { // generate key - alg := AlgorithmEd25519 + alg := AlgorithmEdDSA _, key := generateTestEd25519Key(t) // set up signer @@ -47,11 +47,20 @@ func Test_ed25519Signer(t *testing.T) { if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + _, ok := signer.(DigestSigner) + if ok { + t.Fatalf("signer shouldn't be a DigestSigner") + } + _, ok = verifier.(DigestVerifier) + if ok { + t.Fatalf("verifier shouldn't be a DigestVerifier") + } } func Test_ed25519Verifier_Verify_Success(t *testing.T) { // generate key - alg := AlgorithmEd25519 + alg := AlgorithmEdDSA _, key := generateTestEd25519Key(t) // generate a valid signature @@ -77,7 +86,7 @@ func Test_ed25519Verifier_Verify_Success(t *testing.T) { func Test_ed25519Verifier_Verify_KeyMismatch(t *testing.T) { // generate key - alg := AlgorithmEd25519 + alg := AlgorithmEdDSA _, key := generateTestEd25519Key(t) // generate a valid signature @@ -97,7 +106,7 @@ func Test_ed25519Verifier_Verify_KeyMismatch(t *testing.T) { func Test_ed25519Verifier_Verify_InvalidSignature(t *testing.T) { // generate key - alg := AlgorithmEd25519 + alg := AlgorithmEdDSA vk, sk := generateTestEd25519Key(t) // generate a valid signature with a tampered one diff --git a/errors.go b/errors.go index 8c240e2..770dc9e 100644 --- a/errors.go +++ b/errors.go @@ -13,5 +13,11 @@ var ( ErrNoSignatures = errors.New("no signatures attached") ErrUnavailableHashFunc = errors.New("hash function is not available") ErrVerification = errors.New("verification error") + ErrInvalidKey = errors.New("invalid key") ErrInvalidPubKey = errors.New("invalid public key") + ErrInvalidPrivKey = errors.New("invalid private key") + ErrNotPrivKey = errors.New("not a private key") + ErrOpNotSupported = errors.New("key_op not supported by key") + ErrEC2NoPub = errors.New("cannot create PrivateKey from EC2 key: missing x or y") + ErrOKPNoPub = errors.New("cannot create PrivateKey from OKP key: missing x") ) diff --git a/example_test.go b/example_test.go index 07ea624..ffc2482 100644 --- a/example_test.go +++ b/example_test.go @@ -4,7 +4,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - _ "crypto/sha512" + "crypto/sha512" "fmt" "github.com/veraison/go-cose" @@ -202,3 +202,146 @@ func ExampleSign1Untagged() { // Output: // message signed } + +func ExampleDigestSigner() { + // create a signer + privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + panic(err) + } + signer, err := cose.NewSigner(cose.AlgorithmES256, privateKey) + if err != nil { + panic(err) + } + digestSigner, ok := signer.(cose.DigestSigner) + if !ok { + panic("signer does not support digest signing") + } + + // hash payload outside go-cose. + payload := []byte("hello world") + digested := sha512.Sum512(payload) + sig, err := digestSigner.SignDigest(rand.Reader, digested[:]) + + fmt.Println("digest signed") + _ = sig // further process on sig + // Output: + // digest signed +} + +// 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/fuzz_test.go b/fuzz_test.go index 90db8aa..aa5db19 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -24,7 +24,7 @@ import ( var supportedAlgorithms = [...]cose.Algorithm{ cose.AlgorithmPS256, cose.AlgorithmPS384, cose.AlgorithmPS512, cose.AlgorithmES256, cose.AlgorithmES384, cose.AlgorithmES512, - cose.AlgorithmEd25519, + cose.AlgorithmEdDSA, } func FuzzSign1Message_UnmarshalCBOR(f *testing.F) { @@ -58,7 +58,7 @@ func FuzzSign1Message_UnmarshalCBOR(f *testing.F) { enc, _ := cbor.CanonicalEncOptions().EncMode() dec, _ := cbor.DecOptions{IntDec: cbor.IntDecConvertSigned}.DecMode() isCanonical := func(b []byte) bool { - var tmp interface{} + var tmp any err := dec.Unmarshal(b, &tmp) if err != nil { return false @@ -181,7 +181,7 @@ func newSignerWithEphemeralKey(alg cose.Algorithm) (sv signVerifier, err error) key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) case cose.AlgorithmES512: key, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - case cose.AlgorithmEd25519: + case cose.AlgorithmEdDSA: _, key, err = ed25519.GenerateKey(rand.Reader) default: err = cose.ErrAlgorithmNotSupported diff --git a/go.mod b/go.mod index 609875d..fad879c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,12 @@ module github.com/veraison/go-cose -go 1.18 +go 1.21 -require github.com/fxamacker/cbor/v2 v2.4.0 +require github.com/fxamacker/cbor/v2 v2.5.0 require github.com/x448/float16 v0.8.4 // indirect + +retract ( + v1.2.1 // contains retractions only + v1.2.0 // published in error +) diff --git a/go.sum b/go.sum index f424051..0f986a9 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= -github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= diff --git a/headers.go b/headers.go index 7074936..19c2b3f 100644 --- a/headers.go +++ b/headers.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math/big" + "strings" "github.com/fxamacker/cbor/v2" ) @@ -12,23 +13,27 @@ 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 + HeaderLabelCWTClaims int64 = 15 + HeaderLabelType int64 = 16 + HeaderLabelX5Bag int64 = 32 + HeaderLabelX5Chain int64 = 33 + HeaderLabelX5T int64 = 34 + HeaderLabelX5U int64 = 35 ) // ProtectedHeader contains parameters that are to be cryptographically // protected. -type ProtectedHeader map[interface{}]interface{} +type ProtectedHeader map[any]any // MarshalCBOR encodes the protected header into a CBOR bstr object. // A zero-length header is encoded as a zero-length string rather than as a @@ -42,7 +47,7 @@ func (h ProtectedHeader) MarshalCBOR() ([]byte, error) { if err != nil { return nil, fmt.Errorf("protected header: %w", err) } - encoded, err = encMode.Marshal(map[interface{}]interface{}(h)) + encoded, err = encMode.Marshal(map[any]any(h)) if err != nil { return nil, err } @@ -53,7 +58,8 @@ func (h ProtectedHeader) MarshalCBOR() ([]byte, error) { // UnmarshalCBOR decodes a CBOR bstr object into ProtectedHeader. // // ProtectedHeader is an empty_or_serialized_map where -// empty_or_serialized_map = bstr .cbor header_map / bstr .size 0 +// +// empty_or_serialized_map = bstr .cbor header_map / bstr .size 0 func (h *ProtectedHeader) UnmarshalCBOR(data []byte) error { if h == nil { return errors.New("cbor: UnmarshalCBOR on nil ProtectedHeader pointer") @@ -74,7 +80,7 @@ func (h *ProtectedHeader) UnmarshalCBOR(data []byte) error { if err := validateHeaderLabelCBOR(encoded); err != nil { return err } - var header map[interface{}]interface{} + var header map[any]any if err := decMode.Unmarshal(encoded, &header); err != nil { return err } @@ -93,11 +99,35 @@ func (h *ProtectedHeader) UnmarshalCBOR(data []byte) error { return nil } -// SetAlgorithm sets the algorithm value to the algorithm header. +// SetAlgorithm sets the algorithm value of the protected header. func (h ProtectedHeader) SetAlgorithm(alg Algorithm) { h[HeaderLabelAlgorithm] = alg } +// SetType sets the type of the cose object in the protected header. +func (h ProtectedHeader) SetType(typ any) (any, error) { + if !canTstr(typ) && !canUint(typ) { + return typ, errors.New("header parameter: type: require tstr / uint type") + } + h[HeaderLabelType] = typ + return typ, nil +} + +// SetCWTClaims sets the CWT Claims value of the protected header. +func (h ProtectedHeader) SetCWTClaims(claims CWTClaims) (CWTClaims, error) { + iss, hasIss := claims[1] + if hasIss && !canTstr(iss) { + return claims, errors.New("cwt claim: iss: require tstr") + } + sub, hasSub := claims[2] + if hasSub && !canTstr(sub) { + return claims, errors.New("cwt claim: sub: require tstr") + } + // TODO: validate claims, other claims + h[HeaderLabelCWTClaims] = claims + return claims, nil +} + // Algorithm gets the algorithm value from the algorithm header. func (h ProtectedHeader) Algorithm() (Algorithm, error) { value, ok := h[HeaderLabelAlgorithm] @@ -117,8 +147,10 @@ func (h ProtectedHeader) Algorithm() (Algorithm, error) { return Algorithm(alg), nil case int64: return Algorithm(alg), nil + case string: + return AlgorithmReserved, fmt.Errorf("Algorithm(%q)", alg) default: - return 0, ErrInvalidAlgorithm + return AlgorithmReserved, ErrInvalidAlgorithm } } @@ -126,7 +158,7 @@ func (h ProtectedHeader) Algorithm() (Algorithm, error) { // processing a message is required to understand. // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-3.1 -func (h ProtectedHeader) Critical() ([]interface{}, error) { +func (h ProtectedHeader) Critical() ([]any, error) { value, ok := h[HeaderLabelCritical] if !ok { return nil, nil @@ -135,12 +167,12 @@ func (h ProtectedHeader) Critical() ([]interface{}, error) { if err != nil { return nil, err } - return value.([]interface{}), nil + return value.([]any), nil } // ensureCritical ensures all critical headers are present in the protected bucket. -func ensureCritical(value interface{}, headers map[interface{}]interface{}) error { - labels, ok := value.([]interface{}) +func ensureCritical(value any, headers map[any]any) error { + labels, ok := value.([]any) if !ok { return errors.New("invalid crit header") } @@ -161,7 +193,7 @@ func ensureCritical(value interface{}, headers map[interface{}]interface{}) erro // UnprotectedHeader contains parameters that are not cryptographically // protected. -type UnprotectedHeader map[interface{}]interface{} +type UnprotectedHeader map[any]any // MarshalCBOR encodes the unprotected header into a CBOR map object. // A zero-length header is encoded as a zero-length map (encoded as h'a0'). @@ -172,7 +204,7 @@ func (h UnprotectedHeader) MarshalCBOR() ([]byte, error) { if err := validateHeaderParameters(h, false); err != nil { return nil, fmt.Errorf("unprotected header: %w", err) } - return encMode.Marshal(map[interface{}]interface{}(h)) + return encMode.Marshal(map[any]any(h)) } // UnmarshalCBOR decodes a CBOR map object into UnprotectedHeader. @@ -194,10 +226,22 @@ func (h *UnprotectedHeader) UnmarshalCBOR(data []byte) error { if err := validateHeaderLabelCBOR(data); err != nil { return err } - var header map[interface{}]interface{} - 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) } @@ -205,6 +249,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 @@ -212,22 +297,22 @@ func (h *UnprotectedHeader) UnmarshalCBOR(data []byte) error { // // It is represented by CDDL fragments: // -// Headers = ( -// protected : empty_or_serialized_map, -// unprotected : header_map -// ) +// Headers = ( +// protected : empty_or_serialized_map, +// unprotected : header_map +// ) // -// header_map = { -// Generic_Headers, -// * label => values -// } +// header_map = { +// Generic_Headers, +// * label => values +// } // -// label = int / tstr -// values = any +// label = int / tstr +// values = any // -// empty_or_serialized_map = bstr .cbor header_map / bstr .size 0 +// empty_or_serialized_map = bstr .cbor header_map / bstr .size 0 // -// See Also +// # See Also // // https://tools.ietf.org/html/rfc8152#section-3 type Headers struct { @@ -373,14 +458,14 @@ func (h *Headers) ensureIV() error { } // hasLabel returns true if h contains label. -func hasLabel(h map[interface{}]interface{}, label interface{}) bool { +func hasLabel(h map[any]any, label any) bool { _, ok := h[label] return ok } // validateHeaderParameters validates all headers conform to the spec. -func validateHeaderParameters(h map[interface{}]interface{}, protected bool) error { - existing := make(map[interface{}]struct{}, len(h)) +func validateHeaderParameters(h map[any]any, protected bool) error { + existing := make(map[any]struct{}, len(h)) for label, value := range h { // Validate that all header labels are integers or strings. // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 @@ -401,8 +486,8 @@ func validateHeaderParameters(h map[interface{}]interface{}, protected bool) err // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-3.1 switch label { case HeaderLabelAlgorithm: - _, is_alg := value.(Algorithm) - if !is_alg && !canInt(value) && !canTstr(value) { + _, isAlg := value.(Algorithm) + if !isAlg && !canInt(value) && !canTstr(value) { return errors.New("header parameter: alg: require int / tstr type") } case HeaderLabelCritical: @@ -412,10 +497,44 @@ func validateHeaderParameters(h map[interface{}]interface{}, protected bool) err if err := ensureCritical(value, h); err != nil { return fmt.Errorf("header parameter: crit: %w", err) } + case HeaderLabelType: + isTstr := canTstr(value) + if !isTstr && !canUint(value) { + return errors.New("header parameter: type: require tstr / uint type") + } + if isTstr { + v := value.(string) + if len(v) == 0 { + return errors.New("header parameter: type: require non-empty string") + } + if v[0] == ' ' || v[len(v)-1] == ' ' { + return errors.New("header parameter: type: require no leading/trailing whitespace") + } + // Basic check that the content type is of form type/subtype. + // We don't check the precise definition though (RFC 6838 Section 4.2). + if strings.Count(v, "/") != 1 { + return errors.New("header parameter: type: require text of form type/subtype") + } + } case HeaderLabelContentType: - if !canTstr(value) && !canUint(value) { + isTstr := canTstr(value) + if !isTstr && !canUint(value) { return errors.New("header parameter: content type: require tstr / uint type") } + if isTstr { + v := value.(string) + if len(v) == 0 { + return errors.New("header parameter: content type: require non-empty string") + } + if v[0] == ' ' || v[len(v)-1] == ' ' { + return errors.New("header parameter: content type: require no leading/trailing whitespace") + } + // Basic check that the content type is of form type/subtype. + // We don't check the precise definition though (RFC 6838 Section 4.2). + if strings.Count(v, "/") != 1 { + return errors.New("header parameter: content type: require text of form type/subtype") + } + } case HeaderLabelKeyID: if !canBstr(value) { return errors.New("header parameter: kid: require bstr type") @@ -434,13 +553,45 @@ func validateHeaderParameters(h map[interface{}]interface{}, protected bool) err 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 } // canUint reports whether v can be used as a CBOR uint type. -func canUint(v interface{}) bool { +func canUint(v any) bool { switch v := v.(type) { case uint, uint8, uint16, uint32, uint64: return true @@ -459,7 +610,7 @@ func canUint(v interface{}) bool { } // canInt reports whether v can be used as a CBOR int type. -func canInt(v interface{}) bool { +func canInt(v any) bool { switch v.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: @@ -469,20 +620,20 @@ func canInt(v interface{}) bool { } // canTstr reports whether v can be used as a CBOR tstr type. -func canTstr(v interface{}) bool { +func canTstr(v any) bool { _, ok := v.(string) return ok } // canBstr reports whether v can be used as a CBOR bstr type. -func canBstr(v interface{}) bool { +func canBstr(v any) bool { _, ok := v.([]byte) return ok } // normalizeLabel tries to cast label into a int64 or a string. // Returns (nil, false) if the label type is not valid. -func normalizeLabel(label interface{}) (interface{}, bool) { +func normalizeLabel(label any) (any, bool) { switch v := label.(type) { case int: label = int64(v) @@ -514,7 +665,7 @@ func normalizeLabel(label interface{}) (interface{}, bool) { // headerLabelValidator is used to validate the header label of a COSE header. type headerLabelValidator struct { - value interface{} + value any } // String prints the value without brackets `{}`. Useful in error printing. @@ -553,7 +704,7 @@ func (discardedCBORMessage) UnmarshalCBOR(data []byte) error { // validateHeaderLabelCBOR validates if all header labels are integers or // strings of a CBOR map object. // -// label = int / tstr +// label = int / tstr // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 func validateHeaderLabelCBOR(data []byte) error { diff --git a/headers_test.go b/headers_test.go index 1bc505d..efb11f9 100644 --- a/headers_test.go +++ b/headers_test.go @@ -1,6 +1,8 @@ package cose import ( + "errors" + "math" "reflect" "testing" ) @@ -16,7 +18,7 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { name: "valid header", h: ProtectedHeader{ HeaderLabelAlgorithm: AlgorithmES256, - HeaderLabelCritical: []interface{}{ + HeaderLabelCritical: []any{ HeaderLabelContentType, "foo", }, @@ -33,6 +35,39 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { }, }, { + name: "header with MinInt64 alg", + h: ProtectedHeader{ + HeaderLabelAlgorithm: math.MinInt64, + }, + want: []byte{ + 0x4b, // bstr + 0xa1, // map + 0x01, 0x3b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // alg + }, + }, + { + name: "canonical ordering", + h: ProtectedHeader{ + HeaderLabelAlgorithm: 1, + HeaderLabelCritical: []any{HeaderLabelAlgorithm}, + HeaderLabelContentType: 16, + HeaderLabelKeyID: []byte{1, 2, 3}, + HeaderLabelIV: []byte{1, 2, 3}, + 0x46: 0x47, + 0x66: 0x67, + }, + want: []byte{ + 0x58, 0x1a, // bstr + 0xa7, // map + 0x01, 0x01, // alg + 0x02, 0x81, 0x01, // crit + 0x03, 0x10, // cty + 0x04, 0x43, 0x01, 0x02, 0x03, // kid + 0x05, 0x43, 0x01, 0x02, 0x03, // iv + 0x18, 0x46, 0x18, 0x47, // 0x46: 0x47 + 0x18, 0x66, 0x18, 0x67, // 0x66: 0x67 + }, + }, { name: "nil header", h: nil, want: []byte{0x40}, @@ -46,10 +81,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, @@ -60,10 +95,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, @@ -83,7 +118,7 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { { name: "empty critical", h: ProtectedHeader{ - HeaderLabelCritical: []interface{}{}, + HeaderLabelCritical: []any{}, }, wantErr: "protected header: header parameter: crit: empty crit header", }, @@ -97,7 +132,7 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { { name: "missing header marked as critical", h: ProtectedHeader{ - HeaderLabelCritical: []interface{}{ + HeaderLabelCritical: []any{ HeaderLabelContentType, }, }, @@ -106,7 +141,7 @@ func TestProtectedHeader_MarshalCBOR(t *testing.T) { { name: "critical header contains non-label element", h: ProtectedHeader{ - HeaderLabelCritical: []interface{}{[]uint8{}}, + HeaderLabelCritical: []any{[]uint8{}}, }, wantErr: "protected header: header parameter: crit: require int / tstr type, got '[]uint8': []", }, @@ -161,6 +196,55 @@ 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", + }, + { + name: "content type empty", + h: ProtectedHeader{ + HeaderLabelContentType: "", + }, + wantErr: "protected header: header parameter: content type: require non-empty string", + }, + { + name: "content type leading space", + h: ProtectedHeader{ + HeaderLabelContentType: " a/b", + }, + wantErr: "protected header: header parameter: content type: require no leading/trailing whitespace", + }, + { + name: "content type trailing space", + h: ProtectedHeader{ + HeaderLabelContentType: "a/b ", + }, + wantErr: "protected header: header parameter: content type: require no leading/trailing whitespace", + }, + { + name: "content type no slash", + h: ProtectedHeader{ + HeaderLabelContentType: "ab", + }, + wantErr: "protected header: header parameter: content type: require text of form type/subtype", + }, + { + name: "content type too many slashes", + h: ProtectedHeader{ + HeaderLabelContentType: "a/b/c", + }, + wantErr: "protected header: header parameter: content type: require text of form type/subtype", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -198,7 +282,7 @@ func TestProtectedHeader_UnmarshalCBOR(t *testing.T) { }, want: ProtectedHeader{ HeaderLabelAlgorithm: AlgorithmES256, - HeaderLabelCritical: []interface{}{ + HeaderLabelCritical: []any{ HeaderLabelContentType, "foo", }, @@ -332,6 +416,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) { @@ -422,13 +524,20 @@ func TestProtectedHeader_Algorithm(t *testing.T) { h: ProtectedHeader{ HeaderLabelAlgorithm: "foo", }, + wantErr: errors.New("Algorithm(\"foo\")"), + }, + { + name: "invalid algorithm", + h: ProtectedHeader{ + HeaderLabelAlgorithm: 2.5, + }, wantErr: ErrInvalidAlgorithm, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.h.Algorithm() - if err != tt.wantErr { + if tt.wantErr != nil && err.Error() != tt.wantErr.Error() { t.Errorf("ProtectedHeader.Algorithm() error = %v, wantErr %v", err, tt.wantErr) return } @@ -443,21 +552,21 @@ func TestProtectedHeader_Critical(t *testing.T) { tests := []struct { name string h ProtectedHeader - want []interface{} + want []any wantErr string }{ { name: "valid header", h: ProtectedHeader{ HeaderLabelAlgorithm: AlgorithmES256, - HeaderLabelCritical: []interface{}{ + HeaderLabelCritical: []any{ HeaderLabelContentType, "foo", }, HeaderLabelContentType: "text/plain", "foo": "bar", }, - want: []interface{}{ + want: []any{ HeaderLabelContentType, "foo", }, @@ -482,7 +591,7 @@ func TestProtectedHeader_Critical(t *testing.T) { { name: "empty critical", h: ProtectedHeader{ - HeaderLabelCritical: []interface{}{}, + HeaderLabelCritical: []any{}, }, wantErr: "empty crit header", }, @@ -543,10 +652,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, @@ -556,10 +665,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, @@ -606,6 +715,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) { @@ -731,7 +1028,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 diff --git a/key.go b/key.go new file mode 100644 index 0000000..f3dbbb9 --- /dev/null +++ b/key.go @@ -0,0 +1,948 @@ +package cose + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "errors" + "fmt" + "math/big" + "reflect" + "strconv" +) + +const ( + KeyLabelOKPCurve int64 = -1 + KeyLabelOKPX int64 = -2 + KeyLabelOKPD int64 = -4 + + KeyLabelEC2Curve int64 = -1 + KeyLabelEC2X int64 = -2 + KeyLabelEC2Y int64 = -3 + KeyLabelEC2D int64 = -4 + + KeyLabelSymmetricK int64 = -1 +) + +const ( + keyLabelKeyType int64 = 1 + keyLabelKeyID int64 = 2 + keyLabelAlgorithm int64 = 3 + keyLabelKeyOps int64 = 4 + keyLabelBaseIV int64 = 5 +) + +// KeyOp represents a key_ops value used to restrict purposes for which a Key +// may be used. +// +// https://datatracker.ietf.org/doc/html/rfc8152#section-7.1 +type KeyOp int64 + +const ( + // Reserved value. + KeyOpReserved KeyOp = 0 + + // The key is used to create signatures. Requires private key fields. + KeyOpSign KeyOp = 1 + + // The key is used for verification of signatures. + KeyOpVerify KeyOp = 2 + + // The key is used for key transport encryption. + KeyOpEncrypt KeyOp = 3 + + // The key is used for key transport decryption. Requires private key fields. + KeyOpDecrypt KeyOp = 4 + + // The key is used for key wrap encryption. + KeyOpWrapKey KeyOp = 5 + + // The key is used for key wrap decryption. + KeyOpUnwrapKey KeyOp = 6 + + // The key is used for deriving keys. Requires private key fields. + KeyOpDeriveKey KeyOp = 7 + + // The key is used for deriving bits not to be used as a key. Requires + // private key fields. + KeyOpDeriveBits KeyOp = 8 + + // The key is used for creating MACs. + KeyOpMACCreate KeyOp = 9 + + // The key is used for validating MACs. + KeyOpMACVerify KeyOp = 10 +) + +// KeyOpFromString returns the KeyOp corresponding to the specified name. +// The values are taken from https://www.rfc-editor.org/rfc/rfc7517#section-4.3 +func KeyOpFromString(val string) (KeyOp, bool) { + switch val { + case "sign": + return KeyOpSign, true + case "verify": + return KeyOpVerify, true + case "encrypt": + return KeyOpEncrypt, true + case "decrypt": + return KeyOpDecrypt, true + case "wrapKey": + return KeyOpWrapKey, true + case "unwrapKey": + return KeyOpUnwrapKey, true + case "deriveKey": + return KeyOpDeriveKey, true + case "deriveBits": + return KeyOpDeriveBits, true + default: + return KeyOpReserved, false + } +} + +// String returns a string representation of the KeyType. Note does not +// represent a valid value of the corresponding serialized entry, and must not +// be used as such. (The values returned _mostly_ correspond to those accepted +// by KeyOpFromString, except for MAC create/verify, which are not defined by +// RFC7517). +func (ko KeyOp) String() string { + switch ko { + case KeyOpSign: + return "sign" + case KeyOpVerify: + return "verify" + case KeyOpEncrypt: + return "encrypt" + case KeyOpDecrypt: + return "decrypt" + case KeyOpWrapKey: + return "wrapKey" + case KeyOpUnwrapKey: + return "unwrapKey" + case KeyOpDeriveKey: + return "deriveKey" + case KeyOpDeriveBits: + return "deriveBits" + case KeyOpMACCreate: + return "MAC create" + case KeyOpMACVerify: + return "MAC verify" + case KeyOpReserved: + return "Reserved" + default: + return "unknown key_op value " + strconv.Itoa(int(ko)) + } +} + +// KeyType identifies the family of keys represented by the associated Key. +// +// https://datatracker.ietf.org/doc/html/rfc8152#section-13 +type KeyType int64 + +const ( + KeyTypeReserved KeyType = 0 + KeyTypeOKP KeyType = 1 + KeyTypeEC2 KeyType = 2 + KeyTypeSymmetric KeyType = 4 +) + +// String returns a string representation of the KeyType. Note does not +// represent a valid value of the corresponding serialized entry, and must +// not be used as such. +func (kt KeyType) String() string { + switch kt { + case KeyTypeOKP: + return "OKP" + case KeyTypeEC2: + return "EC2" + case KeyTypeSymmetric: + return "Symmetric" + case KeyTypeReserved: + return "Reserved" + default: + return "unknown key type value " + strconv.Itoa(int(kt)) + } +} + +// Curve represents the EC2/OKP key's curve. +// +// https://datatracker.ietf.org/doc/html/rfc8152#section-13.1 +type Curve int64 + +const ( + // Reserved value + CurveReserved Curve = 0 + + // NIST P-256 also known as secp256r1 + CurveP256 Curve = 1 + + // NIST P-384 also known as secp384r1 + CurveP384 Curve = 2 + + // NIST P-521 also known as secp521r1 + CurveP521 Curve = 3 + + // X25519 for use w/ ECDH only + CurveX25519 Curve = 4 + + // X448 for use w/ ECDH only + CurveX448 Curve = 5 + + // Ed25519 for use /w EdDSA only + CurveEd25519 Curve = 6 + + // Ed448 for use /w EdDSA only + CurveEd448 Curve = 7 +) + +// String returns a string representation of the Curve. Note does not +// represent a valid value of the corresponding serialized entry, and must +// not be used as such. +func (c Curve) String() string { + switch c { + case CurveP256: + return "P-256" + case CurveP384: + return "P-384" + case CurveP521: + return "P-521" + case CurveX25519: + return "X25519" + case CurveX448: + return "X448" + case CurveEd25519: + return "Ed25519" + case CurveEd448: + return "Ed448" + case CurveReserved: + return "Reserved" + default: + return "unknown curve value " + strconv.Itoa(int(c)) + } +} + +// Key represents a COSE_Key structure, as defined by RFC8152. +// Note: currently, this does NOT support RFC8230 (RSA algorithms). +type Key struct { + // Type identifies the family of keys for this structure, and thus, + // which of the key-type-specific parameters need to be set. + Type KeyType + // ID is the identification value matched to the kid in the message. + ID []byte + // Algorithm is used to restrict the algorithm that is used with the + // key. If it is set, the application MUST verify that it matches the + // algorithm for which the Key is being used. + Algorithm Algorithm + // Ops can be set to restrict the set of operations that the Key is used for. + Ops []KeyOp + // BaseIV is the Base IV to be xor-ed with Partial IVs. + BaseIV []byte + + // Any additional parameter (label,value) pairs. + Params map[any]any +} + +// NewKeyOKP returns a Key created using the provided Octet Key Pair data. +func NewKeyOKP(alg Algorithm, x, d []byte) (*Key, error) { + if alg != AlgorithmEdDSA { + return nil, fmt.Errorf("unsupported algorithm %q", alg) + } + + key := &Key{ + Type: KeyTypeOKP, + Algorithm: alg, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + }, + } + if x != nil { + key.Params[KeyLabelOKPX] = x + } + if d != nil { + key.Params[KeyLabelOKPD] = d + } + if err := key.validate(KeyOpReserved); err != nil { + return nil, err + } + return key, nil +} + +// ParamBytes returns the value of the parameter with the given label, if it +// exists and is of type []byte or can be converted to []byte. +func (k *Key) ParamBytes(label any) ([]byte, bool) { + v, ok, err := decodeBytes(k.Params, label) + return v, ok && err == nil +} + +// ParamInt returns the value of the parameter with the given label, if it +// exists and is of type int64 or can be converted to int64. +func (k *Key) ParamInt(label any) (int64, bool) { + v, ok, err := decodeInt(k.Params, label) + return v, ok && err == nil +} + +// ParamUint returns the value of the parameter with the given label, if it +// exists and is of type uint64 or can be converted to uint64. +func (k *Key) ParamUint(label any) (uint64, bool) { + v, ok, err := decodeUint(k.Params, label) + return v, ok && err == nil +} + +// ParamString returns the value of the parameter with the given label, if it +// exists and is of type string or can be converted to string. +func (k *Key) ParamString(label any) (string, bool) { + v, ok, err := decodeString(k.Params, label) + return v, ok && err == nil +} + +// ParamBool returns the value of the parameter with the given label, if it +// exists and is of type bool or can be converted to bool. +func (k *Key) ParamBool(label any) (bool, bool) { + v, ok, err := decodeBool(k.Params, label) + return v, ok && err == nil +} + +// OKP returns the Octet Key Pair parameters for the key. +func (k *Key) OKP() (crv Curve, x []byte, d []byte) { + v, ok := k.ParamInt(KeyLabelOKPCurve) + if ok { + crv = Curve(v) + } + x, _ = k.ParamBytes(KeyLabelOKPX) + d, _ = k.ParamBytes(KeyLabelOKPD) + return +} + +// NewKeyEC2 returns a Key created using the provided elliptic curve key +// data. +func NewKeyEC2(alg Algorithm, x, y, d []byte) (*Key, error) { + var curve Curve + + switch alg { + case AlgorithmES256: + curve = CurveP256 + case AlgorithmES384: + curve = CurveP384 + case AlgorithmES512: + curve = CurveP521 + default: + return nil, fmt.Errorf("unsupported algorithm %q", alg) + } + + key := &Key{ + Type: KeyTypeEC2, + Algorithm: alg, + Params: map[any]any{ + KeyLabelEC2Curve: curve, + }, + } + if x != nil { + key.Params[KeyLabelEC2X] = x + } + if y != nil { + key.Params[KeyLabelEC2Y] = y + } + if d != nil { + key.Params[KeyLabelEC2D] = d + } + if err := key.validate(KeyOpReserved); err != nil { + return nil, err + } + return key, nil +} + +// EC2 returns the Elliptic Curve parameters for the key. +func (k *Key) EC2() (crv Curve, x []byte, y, d []byte) { + v, ok := k.ParamInt(KeyLabelEC2Curve) + if ok { + crv = Curve(v) + } + x, _ = k.ParamBytes(KeyLabelEC2X) + y, _ = k.ParamBytes(KeyLabelEC2Y) + d, _ = k.ParamBytes(KeyLabelEC2D) + return +} + +// NewKeySymmetric returns a Key created using the provided Symmetric key +// bytes. +func NewKeySymmetric(k []byte) *Key { + return &Key{ + Type: KeyTypeSymmetric, + Params: map[any]any{ + KeyLabelSymmetricK: k, + }, + } +} + +// Symmetric returns the Symmetric parameters for the key. +func (key *Key) Symmetric() (k []byte) { + k, _ = key.ParamBytes(KeyLabelSymmetricK) + return +} + +// NewKeyFromPublic returns a Key created using the provided crypto.PublicKey. +// Supported key formats are: *ecdsa.PublicKey and ed25519.PublicKey +func NewKeyFromPublic(pub crypto.PublicKey) (*Key, error) { + switch vk := pub.(type) { + case *ecdsa.PublicKey: + alg := algorithmFromEllipticCurve(vk.Curve) + + if alg == AlgorithmReserved { + return nil, fmt.Errorf("unsupported curve: %v", vk.Curve) + } + + return NewKeyEC2(alg, vk.X.Bytes(), vk.Y.Bytes(), nil) + case ed25519.PublicKey: + return NewKeyOKP(AlgorithmEdDSA, []byte(vk), nil) + default: + return nil, ErrInvalidPubKey + } +} + +// NewKeyFromPrivate returns a Key created using provided crypto.PrivateKey. +// Supported key formats are: *ecdsa.PrivateKey and ed25519.PrivateKey +func NewKeyFromPrivate(priv crypto.PrivateKey) (*Key, error) { + switch sk := priv.(type) { + case *ecdsa.PrivateKey: + alg := algorithmFromEllipticCurve(sk.Curve) + + if alg == AlgorithmReserved { + return nil, fmt.Errorf("unsupported curve: %v", sk.Curve) + } + + return NewKeyEC2(alg, sk.X.Bytes(), sk.Y.Bytes(), sk.D.Bytes()) + case ed25519.PrivateKey: + return NewKeyOKP(AlgorithmEdDSA, []byte(sk[32:]), []byte(sk[:32])) + default: + return nil, ErrInvalidPrivKey + } +} + +var ( + // The following errors are used multiple times + // in Key.validate. We declare them here to avoid + // duplication. They are not considered public errors. + errCoordOverflow = fmt.Errorf("%w: overflowing coordinate", ErrInvalidKey) + errReqParamsMissing = fmt.Errorf("%w: required parameters missing", ErrInvalidKey) + errInvalidCurve = fmt.Errorf("%w: curve not supported for the given key type", ErrInvalidKey) +) + +// Validate ensures that the parameters set inside the Key are internally +// consistent (e.g., that the key type is appropriate to the curve). +// It also checks that the key is valid for the requested operation. +func (k Key) validate(op KeyOp) error { + switch k.Type { + case KeyTypeEC2: + crv, x, y, d := k.EC2() + switch op { + case KeyOpVerify: + if len(x) == 0 || len(y) == 0 { + return ErrEC2NoPub + } + case KeyOpSign: + if len(d) == 0 { + return ErrNotPrivKey + } + } + if crv == CurveReserved || (len(x) == 0 && len(y) == 0 && len(d) == 0) { + return errReqParamsMissing + } + if size := curveSize(crv); size > 0 { + // RFC 8152 Section 13.1.1 says that x and y leading zero octets MUST be preserved, + // but the Go crypto/elliptic package trims them. So we relax the check + // here to allow for omitted leading zero octets, we will add them back + // when marshaling. + if len(x) > size || len(y) > size || len(d) > size { + return errCoordOverflow + } + } + switch crv { + case CurveX25519, CurveX448, CurveEd25519, CurveEd448: + return errInvalidCurve + default: + // ok -- a key may contain a currently unsupported curve + // see https://www.rfc-editor.org/rfc/rfc8152#section-13.1.1 + } + case KeyTypeOKP: + crv, x, d := k.OKP() + switch op { + case KeyOpVerify: + if len(x) == 0 { + return ErrOKPNoPub + } + case KeyOpSign: + if len(d) == 0 { + return ErrNotPrivKey + } + } + if crv == CurveReserved || (len(x) == 0 && len(d) == 0) { + return errReqParamsMissing + } + if (len(x) > 0 && len(x) != ed25519.PublicKeySize) || (len(d) > 0 && len(d) != ed25519.SeedSize) { + return errCoordOverflow + } + switch crv { + case CurveP256, CurveP384, CurveP521: + return errInvalidCurve + default: + // ok -- a key may contain a currently unsupported curve + // see https://www.rfc-editor.org/rfc/rfc8152#section-13.2 + } + case KeyTypeSymmetric: + k := k.Symmetric() + if len(k) == 0 { + return errReqParamsMissing + } + case KeyTypeReserved: + return fmt.Errorf("%w: kty value 0", ErrInvalidKey) + default: + // Unknown key type, we can't validate custom parameters. + } + + // If Algorithm is set, it must match the specified key parameters. + if k.Algorithm != AlgorithmReserved { + expectedAlg, err := k.deriveAlgorithm() + if err != nil { + return err + } + + if k.Algorithm != expectedAlg { + return fmt.Errorf( + "found algorithm %q (expected %q)", + k.Algorithm.String(), + expectedAlg.String(), + ) + } + } + + return nil +} + +func (k Key) canOp(op KeyOp) bool { + if k.Ops == nil { + return true + } + for _, kop := range k.Ops { + if kop == op { + return true + } + } + return false +} + +// MarshalCBOR encodes Key into a COSE_Key object. +func (k *Key) MarshalCBOR() ([]byte, error) { + tmp := map[any]any{ + keyLabelKeyType: k.Type, + } + if k.ID != nil { + tmp[keyLabelKeyID] = k.ID + } + if k.Algorithm != AlgorithmReserved { + tmp[keyLabelAlgorithm] = k.Algorithm + } + if k.Ops != nil { + tmp[keyLabelKeyOps] = k.Ops + } + if k.BaseIV != nil { + tmp[keyLabelBaseIV] = k.BaseIV + } + existing := make(map[any]struct{}, len(k.Params)) + for label, v := range k.Params { + lbl, ok := normalizeLabel(label) + if !ok { + return nil, fmt.Errorf("invalid label type %T", label) + } + if _, ok := existing[lbl]; ok { + return nil, fmt.Errorf("duplicate label %v", lbl) + } + existing[lbl] = struct{}{} + tmp[lbl] = v + } + if k.Type == KeyTypeEC2 { + // If EC2 key, ensure that x and y are padded to the correct size. + crv, x, y, _ := k.EC2() + if size := curveSize(crv); size > 0 { + if 0 < len(x) && len(x) < size { + tmp[KeyLabelEC2X] = append(make([]byte, size-len(x), size), x...) + } + if 0 < len(y) && len(y) < size { + tmp[KeyLabelEC2Y] = append(make([]byte, size-len(y), size), y...) + } + } + } + return encMode.Marshal(tmp) +} + +// UnmarshalCBOR decodes a COSE_Key object into Key. +func (k *Key) UnmarshalCBOR(data []byte) error { + var tmp map[any]any + if err := decMode.Unmarshal(data, &tmp); err != nil { + return err + } + + *k = Key{} + kty, exist, err := decodeInt(tmp, keyLabelKeyType) + if !exist { + return errors.New("kty: missing") + } + if err != nil { + return fmt.Errorf("kty: %w", err) + } + k.Type = KeyType(kty) + if k.Type == KeyTypeReserved { + return errors.New("kty: invalid value 0") + } + k.ID, _, err = decodeBytes(tmp, keyLabelKeyID) + if err != nil { + return fmt.Errorf("kid: %w", err) + } + alg, _, err := decodeInt(tmp, keyLabelAlgorithm) + if err != nil { + return fmt.Errorf("alg: %w", err) + } + k.Algorithm = Algorithm(alg) + key_ops, err := decodeSlice(tmp, keyLabelKeyOps) + if err != nil { + return fmt.Errorf("key_ops: %w", err) + } + if len(key_ops) > 0 { + k.Ops = make([]KeyOp, len(key_ops)) + for i, op := range key_ops { + switch op := op.(type) { + case int64: + k.Ops[i] = KeyOp(op) + case string: + var ok bool + if k.Ops[i], ok = KeyOpFromString(op); !ok { + return fmt.Errorf("key_ops: unknown entry value %q", op) + } + default: + return fmt.Errorf("key_ops: invalid entry type %T", op) + } + } + } + k.BaseIV, _, err = decodeBytes(tmp, keyLabelBaseIV) + if err != nil { + return fmt.Errorf("base_iv: %w", err) + } + + delete(tmp, keyLabelKeyType) + delete(tmp, keyLabelKeyID) + delete(tmp, keyLabelAlgorithm) + delete(tmp, keyLabelKeyOps) + delete(tmp, keyLabelBaseIV) + + if len(tmp) > 0 { + k.Params = make(map[any]any, len(tmp)) + for lbl, v := range tmp { + switch lbl := lbl.(type) { + case int64: + if (k.Type == KeyTypeEC2 || k.Type == KeyTypeOKP) && + (lbl == KeyLabelEC2Curve || lbl == KeyLabelOKPCurve) { + v = Curve(v.(int64)) + } + k.Params[lbl] = v + case string: + k.Params[lbl] = v + default: + return fmt.Errorf("invalid label type %T", lbl) + } + } + } + return k.validate(KeyOpReserved) +} + +// PublicKey returns a crypto.PublicKey generated using Key's parameters. +func (k *Key) PublicKey() (crypto.PublicKey, error) { + if err := k.validate(KeyOpVerify); err != nil { + return nil, err + } + alg, err := k.deriveAlgorithm() + if err != nil { + return nil, err + } + + switch alg { + case AlgorithmES256, AlgorithmES384, AlgorithmES512: + var curve elliptic.Curve + + switch alg { + case AlgorithmES256: + curve = elliptic.P256() + case AlgorithmES384: + curve = elliptic.P384() + case AlgorithmES512: + curve = elliptic.P521() + } + + _, x, y, _ := k.EC2() + + pub := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)} + pub.X.SetBytes(x) + pub.Y.SetBytes(y) + + return pub, nil + case AlgorithmEdDSA: + _, x, _ := k.OKP() + return ed25519.PublicKey(x), nil + default: + return nil, ErrAlgorithmNotSupported + } +} + +// PrivateKey returns a crypto.PrivateKey generated using Key's parameters. +// Compressed point is not supported for EC2 keys. +func (k *Key) PrivateKey() (crypto.PrivateKey, error) { + if err := k.validate(KeyOpSign); err != nil { + return nil, err + } + alg, err := k.deriveAlgorithm() + if err != nil { + return nil, err + } + + switch alg { + case AlgorithmES256, AlgorithmES384, AlgorithmES512: + _, x, y, d := k.EC2() + if len(x) == 0 || len(y) == 0 { + return nil, fmt.Errorf("%w: compressed point not supported", ErrInvalidPrivKey) + } + + var curve elliptic.Curve + switch alg { + case AlgorithmES256: + curve = elliptic.P256() + case AlgorithmES384: + curve = elliptic.P384() + case AlgorithmES512: + curve = elliptic.P521() + } + + bx := new(big.Int).SetBytes(x) + by := new(big.Int).SetBytes(y) + bd := new(big.Int).SetBytes(d) + + return &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{Curve: curve, X: bx, Y: by}, + D: bd, + }, nil + case AlgorithmEdDSA: + _, x, d := k.OKP() + if len(x) == 0 { + return ed25519.NewKeyFromSeed(d), nil + } + + buf := make([]byte, ed25519.PrivateKeySize) + + copy(buf, d) + copy(buf[32:], x) + + return ed25519.PrivateKey(buf), nil + default: + return nil, ErrAlgorithmNotSupported + } +} + +// AlgorithmOrDefault returns the Algorithm associated with Key. If Key.Algorithm is +// set, that is what is returned. Otherwise, the algorithm is inferred using +// Key.Curve. This method does NOT validate that Key.Algorithm, if set, aligns +// with Key.Curve. +func (k *Key) AlgorithmOrDefault() (Algorithm, error) { + if k.Algorithm != AlgorithmReserved { + return k.Algorithm, nil + } + + return k.deriveAlgorithm() +} + +// Signer returns a Signer created using Key. +func (k *Key) Signer() (Signer, error) { + if !k.canOp(KeyOpSign) { + return nil, ErrOpNotSupported + } + priv, err := k.PrivateKey() + if err != nil { + return nil, err + } + + alg, err := k.AlgorithmOrDefault() + if err != nil { + return nil, err + } + + signer, ok := priv.(crypto.Signer) + if !ok { + return nil, ErrInvalidPrivKey + } + + return NewSigner(alg, signer) +} + +// Verifier returns a Verifier created using Key. +func (k *Key) Verifier() (Verifier, error) { + if !k.canOp(KeyOpVerify) { + return nil, ErrOpNotSupported + } + pub, err := k.PublicKey() + if err != nil { + return nil, err + } + + alg, err := k.AlgorithmOrDefault() + if err != nil { + return nil, err + } + + return NewVerifier(alg, pub) +} + +// deriveAlgorithm derives the intended algorithm for the key from its curve. +// The deriviation is based on the recommendation in RFC8152 that SHA-256 is +// only used with P-256, etc. For other combinations, the Algorithm in the Key +// must be explicitly set,so that this derivation is not used. +func (k *Key) deriveAlgorithm() (Algorithm, error) { + switch k.Type { + case KeyTypeEC2: + crv, _, _, _ := k.EC2() + switch crv { + case CurveP256: + return AlgorithmES256, nil + case CurveP384: + return AlgorithmES384, nil + case CurveP521: + return AlgorithmES512, nil + default: + return AlgorithmReserved, fmt.Errorf( + "unsupported curve %q for key type EC2", crv.String()) + } + case KeyTypeOKP: + crv, _, _ := k.OKP() + switch crv { + case CurveEd25519: + return AlgorithmEdDSA, nil + default: + return AlgorithmReserved, fmt.Errorf( + "unsupported curve %q for key type OKP", crv.String()) + } + default: + // Symmetric algorithms are not supported in the current inmplementation. + return AlgorithmReserved, fmt.Errorf("unexpected key type %q", k.Type.String()) + } +} + +func algorithmFromEllipticCurve(c elliptic.Curve) Algorithm { + switch c { + case elliptic.P256(): + return AlgorithmES256 + case elliptic.P384(): + return AlgorithmES384 + case elliptic.P521(): + return AlgorithmES512 + default: + return AlgorithmReserved + } +} + +func curveSize(crv Curve) int { + var bitSize int + switch crv { + case CurveP256: + bitSize = elliptic.P256().Params().BitSize + case CurveP384: + bitSize = elliptic.P384().Params().BitSize + case CurveP521: + bitSize = elliptic.P521().Params().BitSize + } + return (bitSize + 7) / 8 +} + +func decodeBytes(dic map[any]any, lbl any) (b []byte, ok bool, err error) { + val, ok := dic[lbl] + if !ok { + return nil, false, nil + } + if b, ok = val.([]byte); ok { + return b, true, nil + } + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("invalid type: expected []uint8, got %T", val) + } + }() + return reflect.ValueOf(val).Bytes(), true, nil +} + +func decodeInt(dic map[any]any, lbl any) (int64, bool, error) { + val, ok := dic[lbl] + if !ok { + return 0, false, nil + } + if b, ok := val.(int64); ok { + return b, true, nil + } + if v := reflect.ValueOf(val); v.CanInt() { + return v.Int(), true, nil + } + return 0, true, fmt.Errorf("invalid type: expected int64, got %T", val) +} + +func decodeUint(dic map[any]any, lbl any) (uint64, bool, error) { + val, ok := dic[lbl] + if !ok { + return 0, false, nil + } + if b, ok := val.(uint64); ok { + return b, true, nil + } + v := reflect.ValueOf(val) + if v.CanUint() { + return v.Uint(), true, nil + } + if v.CanInt() { + if b := v.Int(); b >= 0 { + return uint64(b), true, nil + } + } + return 0, true, fmt.Errorf("invalid type: expected uint64, got %T", val) +} + +func decodeString(dic map[any]any, lbl any) (string, bool, error) { + val, ok := dic[lbl] + if !ok { + return "", false, nil + } + if b, ok := val.(string); ok { + return b, true, nil + } + if v := reflect.ValueOf(val); v.Kind() == reflect.String { + return v.String(), true, nil + } + return "", true, fmt.Errorf("invalid type: expected uint64, got %T", val) +} + +func decodeBool(dic map[any]any, lbl any) (bool, bool, error) { + val, ok := dic[lbl] + if !ok { + return false, false, nil + } + if b, ok := val.(bool); ok { + return b, true, nil + } + if v := reflect.ValueOf(val); v.Kind() == reflect.Bool { + return v.Bool(), true, nil + } + return false, true, fmt.Errorf("invalid type: expected uint64, got %T", val) +} + +func decodeSlice(dic map[any]any, lbl any) ([]any, error) { + v, ok := dic[lbl] + if !ok { + return nil, nil + } + arr, ok := v.([]any) + if !ok { + return nil, fmt.Errorf("invalid type: expected []any, got %T", v) + } + return arr, nil +} diff --git a/key_test.go b/key_test.go new file mode 100644 index 0000000..6e5ee0c --- /dev/null +++ b/key_test.go @@ -0,0 +1,1894 @@ +package cose + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "encoding/hex" + "math/big" + "reflect" + "strconv" + "testing" +) + +func TestKey_ParamBytes(t *testing.T) { + key := &Key{ + Params: map[any]any{ + int64(-1): []byte{1}, + 2: []byte{2}, + uint16(3): []byte{3}, + "foo": ed25519.PublicKey([]byte{4}), + 5: 5, + }, + } + tests := []struct { + label any + want []byte + want1 bool + }{ + {int64(-1), []byte{1}, true}, + {2, []byte{2}, true}, + {uint16(3), []byte{3}, true}, + {3, nil, false}, + {5, nil, false}, + {"foo", []byte{4}, true}, + {"bar", nil, false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got, got1 := key.ParamBytes(tt.label) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.ParamBytes() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Key.ParamBytes() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestKey_ParamInt(t *testing.T) { + type i16 int16 + type u16 uint16 + key := &Key{ + Params: map[any]any{ + int64(-1): 1, + 2: int8(2), + uint16(3): i16(3), + uint16(6): u16(3), + "foo": -4, + 5: []byte{5}, + }, + } + tests := []struct { + label any + want int64 + want1 bool + }{ + {int64(-1), 1, true}, + {2, 2, true}, + {uint16(3), 3, true}, + {3, 0, false}, + {5, 0, false}, + {uint16(6), 0, false}, + {"foo", -4, true}, + {"bar", 0, false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got, got1 := key.ParamInt(tt.label) + if got != tt.want { + t.Errorf("Key.ParamInt() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Key.ParamInt() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestKey_ParamUint(t *testing.T) { + type i16 int16 + type u16 uint16 + key := &Key{ + Params: map[any]any{ + int64(-1): 1, + 2: int8(2), + uint16(3): i16(3), + 4: i16(-3), + uint16(6): u16(3), + "foo": -4, + 5: []byte{5}, + }, + } + tests := []struct { + label any + want uint64 + want1 bool + }{ + {int64(-1), 1, true}, + {2, 2, true}, + {uint16(3), 3, true}, + {uint16(6), 3, true}, + {4, 0, false}, + {3, 0, false}, + {5, 0, false}, + {"foo", 0, false}, + {"bar", 0, false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got, got1 := key.ParamUint(tt.label) + if got != tt.want { + t.Errorf("Key.ParamUint() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Key.ParamUint() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestKey_ParamString(t *testing.T) { + type str string + key := &Key{ + Params: map[any]any{ + 1: "foo", + "2": str("bar"), + 3: []byte("baz"), + 4: 5, + }, + } + tests := []struct { + label any + want string + want1 bool + }{ + {1, "foo", true}, + {"2", "bar", true}, + {3, "", false}, + {4, "", false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got, got1 := key.ParamString(tt.label) + if got != tt.want { + t.Errorf("Key.ParamString() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Key.ParamString() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestKey_ParamBool(t *testing.T) { + type boo bool + key := &Key{ + Params: map[any]any{ + 1: true, + "2": boo(false), + 3: []byte("baz"), + 4: 5, + }, + } + tests := []struct { + label any + want bool + want1 bool + }{ + {1, true, true}, + {"2", false, true}, + {3, false, false}, + {4, false, false}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got, got1 := key.ParamBool(tt.label) + if got != tt.want { + t.Errorf("Key.ParamBool() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Key.ParamBool() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestKeyOp_String(t *testing.T) { + tests := []struct { + op KeyOp + want string + }{ + {KeyOpReserved, "Reserved"}, + {KeyOpSign, "sign"}, + {KeyOpVerify, "verify"}, + {KeyOpEncrypt, "encrypt"}, + {KeyOpDecrypt, "decrypt"}, + {KeyOpWrapKey, "wrapKey"}, + {KeyOpUnwrapKey, "unwrapKey"}, + {KeyOpDeriveKey, "deriveKey"}, + {KeyOpDeriveBits, "deriveBits"}, + {KeyOpMACCreate, "MAC create"}, + {KeyOpMACVerify, "MAC verify"}, + {42, "unknown key_op value 42"}, + } + + for _, tt := range tests { + if got := tt.op.String(); got != tt.want { + t.Errorf("KeyOp.String() = %v, want %v", got, tt.want) + } + } +} + +func TestKey_UnmarshalCBOR(t *testing.T) { + tests := []struct { + name string + data []byte + want *Key + wantErr string + }{ + { + name: "invalid COSE_Key CBOR type", + data: []byte{ + 0x82, // array(2) + 0x01, 0x01, // kty: OKP + }, + want: nil, + wantErr: "cbor: cannot unmarshal array into Go value of type map[interface {}]interface {}", + }, { + name: "invalid kty value", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x41, 0x01, // kty: bytes(1) + 0x02, 0x43, 0x01, 0x02, 0x03, // kdi: bytes(3) + }, + want: nil, + wantErr: "kty: invalid type: expected int64, got []uint8", + }, { + name: "missing kty", + data: []byte{ + 0xa1, // map(1) + 0x02, 0x41, 0x01, // kdi: bytes(1) + }, + want: nil, + wantErr: "kty: missing", + }, { + name: "invalid key type", + data: []byte{ + 0xa1, // map (2) + 0x01, 0x00, // kty: invalid + }, + want: nil, + wantErr: "kty: invalid value 0", + }, { + name: "invalid kdi type", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x02, 0x01, // kdi: int(1) + }, + want: nil, + wantErr: "kid: invalid type: expected []uint8, got int64", + }, { + name: "invalid alg type", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x03, 0x41, 0x01, // alg: bstr(1) + }, + want: nil, + wantErr: "alg: invalid type: expected int64, got []uint8", + }, { + name: "invalid key_ops type", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x04, 0x41, 0x01, // key_ops: bstr(1) + }, + want: nil, + wantErr: "key_ops: invalid type: expected []any, got []uint8", + }, { + name: "unknown key_ops entry value", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x04, 0x82, // key_ops: array (2) + 0x02, // verify + 0x63, 0x66, 0x6f, 0x6f, // tstr: foo + }, + want: nil, + wantErr: `key_ops: unknown entry value "foo"`, + }, { + name: "invalid key_ops entry type", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x04, 0x82, // key_ops: array (2) + 0x02, // verify + 0xf6, // nil + }, + want: nil, + wantErr: `key_ops: invalid entry type `, + }, { + name: "invalid base_iv type", + data: []byte{ + 0xa2, // map(2) + 0x01, 0x01, // kty: OKP + 0x05, 0x01, // base_iv: int(1) + }, + want: nil, + wantErr: "base_iv: invalid type: expected []uint8, got int64", + }, { + name: "custom key invalid param type", + data: []byte{ + 0xa3, // map (3) + 0x01, 0x3a, 0x00, 0x01, 0x11, 0x6f, // kty: -70000 + 0x20, 0x06, // 0x20: 0x06 + 0xf6, 0xf6, // nil: nil + }, + want: nil, + wantErr: "invalid label type ", + }, { + name: "duplicated param", + data: []byte{ + 0xa3, // map(3) + 0x01, 0x01, // kty: OKP + 0x18, 0x66, 0x18, 0x67, // 66: 67 + 0x18, 0x66, 0x18, 0x47, // 66: 47 + }, + want: nil, + wantErr: `cbor: found duplicate map key "102" at map element index 2`, + }, { + name: "duplicated kty", + data: []byte{ + 0xa3, // map(3) + 0x01, 0x01, // kty: OKP + 0x02, 0x41, 0x01, // kdi: bytes(1) + 0x01, 0x01, // kty: OKP (duplicated) + }, + want: nil, + wantErr: `cbor: found duplicate map key "1" at map element index 2`, + }, { + name: "OKP missing curve", + data: []byte{ + 0xa1, // map (2) + 0x01, 0x01, // kty: OKP + }, + want: nil, + wantErr: "invalid key: required parameters missing", + }, { + name: "EC2 missing curve", + data: []byte{ + 0xa1, // map (2) + 0x01, 0x02, // kty: EC2 + }, + want: nil, + wantErr: "invalid key: required parameters missing", + }, { + name: "OKP invalid curve", + data: []byte{ + 0xa3, // map (3) + 0x01, 0x01, // kty: OKP + 0x20, 0x01, // curve: CurveP256 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + want: nil, + wantErr: "invalid key: curve not supported for the given key type", + }, { + name: "EC2 invalid curve", + data: []byte{ + 0xa4, // map (4) + 0x01, 0x02, // kty: EC2 + 0x20, 0x06, // curve: CurveEd25519 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + 0x22, 0x58, 0x20, // y-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + want: nil, + wantErr: "invalid key: curve not supported for the given key type", + }, { + name: "Symmetric missing K", + data: []byte{ + 0xa1, // map (1) + 0x01, 0x04, // kty: Symmetric + }, + want: nil, + wantErr: "invalid key: required parameters missing", + }, { + name: "EC2 invalid algorithm", + data: []byte{ + 0xa4, // map (3) + 0x01, 0x01, // kty: OKP + 0x03, 0x26, // alg: ECDSA w/ SHA-256 + 0x20, 0x06, // curve: Ed25519 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + want: nil, + wantErr: `found algorithm "ES256" (expected "EdDSA")`, + }, { + name: "custom key", + data: []byte{ + 0xa3, // map (3) + 0x01, 0x3a, 0x00, 0x01, 0x11, 0x6f, // kty: -70000 + 0x20, 0x06, // 0x20: 0x06 + 0x61, 0x66, 0x63, 0x66, 0x6f, 0x6f, // 0x21: foo + }, + want: &Key{ + Type: -70000, + Params: map[any]any{ + int64(-1): int64(6), + "f": "foo", + }, + }, + }, { + name: "OKP", + data: []byte{ + 0xa6, // map (6) + 0x01, 0x01, // kty: OKP + 0x03, 0x27, // alg: EdDSA w/ Ed25519 + 0x04, // key ops + 0x82, // array (2) + 0x02, // verify + 0x64, 0x73, 0x69, 0x67, 0x6e, // tstr: sign + 0x05, 0x43, 0x03, 0x02, 0x01, // base_iv: bytes(5) + 0x20, 0x06, // curve: Ed25519 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + want: &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + Ops: []KeyOp{KeyOpVerify, KeyOpSign}, + BaseIV: []byte{0x03, 0x02, 0x01}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: []byte{ + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + }, + }, + wantErr: "", + }, { + name: "Symmetric", + data: []byte{ + 0xa2, // map (2) + 0x01, 0x04, // kty: Symmetric + 0x20, 0x58, 0x20, // k: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + want: &Key{ + Type: KeyTypeSymmetric, + Params: map[any]any{ + KeyLabelSymmetricK: []byte{ + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + }, + }, + wantErr: "", + }, + // The following samples are taken from RFC8152 C.7.1. + { + name: "EC2 P-256 public", + data: mustHexToBytes("a5" + + "0102" + + "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65" + + "2001" + + "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d" + + "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c"), + want: &Key{ + Type: KeyTypeEC2, + ID: []byte("meriadoc.brandybuck@buckland.example"), + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: mustHexToBytes("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d"), + KeyLabelEC2Y: mustHexToBytes("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c"), + }, + }, + }, + { + name: "EC2 P-521 public", + data: mustHexToBytes("a5" + + "0102" + + "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65" + + "2003" + + "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad" + + "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475"), + want: &Key{ + Type: KeyTypeEC2, + ID: []byte("bilbo.baggins@hobbiton.example"), + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: mustHexToBytes("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad"), + KeyLabelEC2Y: mustHexToBytes("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475"), + }, + }, + }, + // The following samples are taken from RFC8152 C.7.2. + { + name: "EC2 P-256 private", + data: mustHexToBytes("a6" + + "0102" + + "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65" + + "2001" + + "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d" + + "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c" + + "235820aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf"), + want: &Key{ + Type: KeyTypeEC2, + ID: []byte("meriadoc.brandybuck@buckland.example"), + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: mustHexToBytes("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d"), + KeyLabelEC2Y: mustHexToBytes("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c"), + KeyLabelEC2D: mustHexToBytes("aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf"), + }, + }, + }, { + name: "EC2 P-521 private", + data: mustHexToBytes("a6" + + "0102" + + "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65" + + "2003" + + "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad" + + "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475" + + "23584200085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d"), + want: &Key{ + Type: KeyTypeEC2, + ID: []byte("bilbo.baggins@hobbiton.example"), + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: mustHexToBytes("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad"), + KeyLabelEC2Y: mustHexToBytes("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475"), + KeyLabelEC2D: mustHexToBytes("00085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d"), + }, + }, + }, { + name: "Symmetric", + data: mustHexToBytes("a3" + + "0104" + + "024a6f75722d736563726574" + + "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188"), + want: &Key{ + Type: KeyTypeSymmetric, + ID: []byte("our-secret"), + Params: map[any]any{ + KeyLabelSymmetricK: mustHexToBytes("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(Key) + err := got.UnmarshalCBOR(tt.data) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil && !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.UnmarshalCBOR() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKey_MarshalCBOR(t *testing.T) { + tests := []struct { + name string + key *Key + want []byte + wantErr string + }{ + { + name: "OKP with kty and kid", + key: &Key{ + Type: KeyTypeOKP, + ID: []byte{1, 2, 3}, + }, + want: []byte{ + 0xa2, // map (2) + 0x01, 0x01, // kty: OKP + 0x02, 0x43, 0x01, 0x02, 0x03, // kid: bytes(3) + }, + }, { + name: "OKP with only kty", + key: &Key{ + Type: KeyTypeOKP, + }, + want: []byte{ + 0xa1, // map (1) + 0x01, 0x01, // kty: OKP + }, + }, { + name: "OKP with kty and base_iv", + key: &Key{ + Type: KeyTypeOKP, + BaseIV: []byte{3, 2, 1}, + }, + want: []byte{ + 0xa2, // map (2) + 0x01, 0x01, // kty: OKP + 0x05, 0x43, 0x03, 0x02, 0x01, // base_iv: bytes(3) + }, + }, { + name: "OKP with kty and alg", + key: &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + }, + want: []byte{ + 0xa2, // map (2) + 0x01, 0x01, // kty: OKP + 0x03, 0x27, // alg: EdDSA + }, + }, { + name: "OKP with kty and private alg", + key: &Key{ + Type: KeyTypeOKP, + Algorithm: -70_000, + }, + want: []byte{ + 0xa2, // map (2) + 0x01, 0x01, // kty: OKP + 0x03, 0x3a, 0x00, 0x01, 0x11, 0x6f, // alg: -70000 + }, + }, { + name: "OKP with kty and key_ops", + key: &Key{ + Type: KeyTypeOKP, + ID: []byte{1, 2, 3}, + Ops: []KeyOp{KeyOpEncrypt, KeyOpDecrypt, -70_000}, + }, + want: []byte{ + 0xa3, // map (3) + 0x01, 0x01, // kty: OKP + 0x02, 0x43, 0x01, 0x02, 0x03, // kid: bytes(3) + 0x04, 0x83, // key_ops: array(3) + 0x03, 0x04, 0x3a, 0x00, 0x01, 0x11, 0x6f, // -70000 + }, + }, { + name: "OKP with kty and private int params", + key: &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + 0x46: 0x47, + 0x66: 0x67, + }, + }, + want: []byte{ + 0xa3, // map (3) + 0x01, 0x01, // kty: OKP + 0x18, 0x46, 0x18, 0x47, // 0x46: 0x47 (note canonical ordering) + 0x18, 0x66, 0x18, 0x67, // 0x66: 0x67 + }, + }, { + name: "OKP with kty and private mixed params", + key: &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + 0x1234: 0x47, + "a": 0x67, + }, + }, + want: []byte{ + 0xa3, // map (3) + 0x01, 0x01, // kty: OKP + 0x19, 0x12, 0x34, 0x18, 0x47, // 0x1234: 0x47 (note canonical lexicographic ordering) + 0x61, 0x61, 0x18, 0x67, // "a": 0x67 + }, + }, { + name: "OKP duplicated params", + key: &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + int8(10): 0, + int32(10): 1, + }, + }, + wantErr: "duplicate label 10", + }, { + name: "OKP with invalid param label", + key: &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + int8(10): 0, + -3.5: 1, + }, + }, + wantErr: "invalid label type float64", + }, { + name: "OKP", + key: &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + Ops: []KeyOp{KeyOpVerify, KeyOpEncrypt}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: []byte{ + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + }, + }, + want: []byte{ + 0xa5, // map (5) + 0x01, 0x01, // kty: OKP + 0x03, 0x27, // alg: EdDSA w/ Ed25519 + 0x04, // key ops + 0x82, // array (2) + 0x02, 0x03, // verify, encrypt + 0x20, 0x06, // curve: Ed25519 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + wantErr: "", + }, { + name: "EC2 with short x and y", + key: &Key{ + Type: KeyTypeEC2, + Algorithm: AlgorithmES256, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: []byte{0x01}, + KeyLabelEC2Y: []byte{0x02, 0x03}, + }, + }, + want: []byte{ + 0xa5, // map (4) + 0x01, 0x02, // kty: EC2 + 0x03, 0x26, // alg: ES256 + 0x20, 0x01, // curve: P256 + 0x21, 0x58, 0x20, // x-coordinate: bytes(32) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 32-byte value + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x22, 0x58, 0x20, // y-coordinate: bytes(32) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 32-byte value + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, + }, + wantErr: "", + }, { + name: "Symmetric", + key: &Key{ + Type: KeyTypeSymmetric, + Params: map[any]any{ + KeyLabelSymmetricK: []byte{ + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + }, + }, + want: []byte{ + 0xa2, // map (2) + 0x01, 0x04, // kty: Symmetric + 0x20, 0x58, 0x20, // K: bytes(32) + 0x15, 0x52, 0x2e, 0xf1, 0x57, 0x29, 0xcc, 0xf3, // 32-byte value + 0x95, 0x09, 0xea, 0x5c, 0x15, 0xa2, 0x6b, 0xe9, + 0x49, 0xe3, 0x88, 0x07, 0xa5, 0xc2, 0x6e, 0xf9, + 0x28, 0x14, 0x87, 0xef, 0x4a, 0xe6, 0x7b, 0x46, + }, + wantErr: "", + }, { + name: "unknown key type", + key: &Key{Type: 42}, + want: []byte{ + 0xa1, // map (1) + 0x01, 0x18, 0x2a, // kty: 42 + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.key.MarshalCBOR() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil && !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.MarshalCBOR() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewKeyOKP(t *testing.T) { + x, d := newEd25519(t) + type args struct { + alg Algorithm + x []byte + d []byte + } + tests := []struct { + name string + args args + want *Key + wantErr string + }{ + { + name: "valid", args: args{AlgorithmEdDSA, x, d}, + want: &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + KeyLabelOKPD: d, + }, + }, + wantErr: "", + }, { + name: "invalid alg", args: args{Algorithm(-100), x, d}, + want: nil, + wantErr: `unsupported algorithm "Algorithm(-100)"`, + }, { + name: "x and d missing", args: args{AlgorithmEdDSA, nil, nil}, + want: nil, + wantErr: "invalid key: required parameters missing", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewKeyOKP(tt.args.alg, tt.args.x, tt.args.d) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("NewKeyOKP() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewKeyOKP() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewNewKeyEC2(t *testing.T) { + ec256x, ec256y, ec256d := newEC2(t, elliptic.P256()) + ec384x, ec384y, ec384d := newEC2(t, elliptic.P384()) + ec521x, ec521y, ec521d := newEC2(t, elliptic.P521()) + type args struct { + alg Algorithm + x []byte + y []byte + d []byte + } + tests := []struct { + name string + args args + want *Key + wantErr string + }{ + { + name: "valid ES256", args: args{AlgorithmES256, ec256x, ec256y, ec256d}, + want: &Key{ + Type: KeyTypeEC2, + Algorithm: AlgorithmES256, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + KeyLabelEC2D: ec256d, + }, + }, + wantErr: "", + }, { + name: "valid ES384", args: args{AlgorithmES384, ec384x, ec384y, ec384d}, + want: &Key{ + Type: KeyTypeEC2, + Algorithm: AlgorithmES384, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + KeyLabelEC2X: ec384x, + KeyLabelEC2Y: ec384y, + KeyLabelEC2D: ec384d, + }, + }, + wantErr: "", + }, { + name: "valid ES521", args: args{AlgorithmES512, ec521x, ec521y, ec521d}, + want: &Key{ + Type: KeyTypeEC2, + Algorithm: AlgorithmES512, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: ec521x, + KeyLabelEC2Y: ec521y, + KeyLabelEC2D: ec521d, + }, + }, + wantErr: "", + }, { + name: "invalid alg", args: args{Algorithm(-100), ec256x, ec256y, ec256d}, + want: nil, + wantErr: `unsupported algorithm "Algorithm(-100)"`, + }, { + name: "x, y and d missing", args: args{AlgorithmES512, nil, nil, nil}, + want: nil, + wantErr: "invalid key: required parameters missing", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewKeyEC2(tt.args.alg, tt.args.x, tt.args.y, tt.args.d) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("NewKeyEC2() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewKeyEC2() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewKeySymmetric(t *testing.T) { + type args struct { + k []byte + } + tests := []struct { + name string + args args + want *Key + }{ + {"valid", args{[]byte{1, 2, 3}}, &Key{ + Type: KeyTypeSymmetric, + Params: map[any]any{ + KeyLabelSymmetricK: []byte{1, 2, 3}, + }, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewKeySymmetric(tt.args.k); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewKeySymmetric() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKey_SignRoundtrip(t *testing.T) { + tests := []struct { + name string + newKey func() (crypto.PrivateKey, error) + }{ + { + "P-256", func() (crypto.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + }, + }, { + "P-384", func() (crypto.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + }, + }, { + "P-521", func() (crypto.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + }, + }, { + "Ed25519", func() (crypto.PrivateKey, error) { + _, priv, err := ed25519.GenerateKey(rand.Reader) + return priv, err + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + priv, err := tt.newKey() + if err != nil { + t.Fatal(err) + } + key, err := NewKeyFromPrivate(priv) + if err != nil { + t.Fatal(err) + } + signer, err := key.Signer() + if err != nil { + t.Fatal(err) + } + message := []byte("foo bar") + sig, err := signer.Sign(rand.Reader, message) + if err != nil { + t.Fatal(err) + } + verifier, err := key.Verifier() + if err != nil { + t.Fatal(err) + } + err = verifier.Verify(message, sig) + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestKey_AlgorithmOrDefault(t *testing.T) { + tests := []struct { + name string + k *Key + want Algorithm + wantErr string + }{ + { + "custom", + &Key{Algorithm: -1000}, + -1000, + "", + }, + { + "OKP-Ed25519", + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + }, + }, + AlgorithmEdDSA, + "", + }, + { + "OKP-P256", + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveP256, + }, + }, + AlgorithmReserved, + `unsupported curve "P-256" for key type OKP`, + }, + { + "EC2-P256", + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + }, + }, + AlgorithmES256, + "", + }, + { + "EC2-P384", + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + }, + }, + AlgorithmES384, + "", + }, + { + "EC2-P521", + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + }, + }, + AlgorithmES512, + "", + }, + { + "EC2-Ed25519", + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveEd25519, + }, + }, + AlgorithmReserved, + `unsupported curve "Ed25519" for key type EC2`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.AlgorithmOrDefault() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.AlgorithmOrDefault() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.AlgorithmOrDefault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewKeyFromPrivate(t *testing.T) { + x, y, d := newEC2(t, elliptic.P256()) + okpx, okpd := newEd25519(t) + tests := []struct { + name string + k crypto.PrivateKey + want *Key + wantErr string + }{ + { + "ecdsa", &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(x), Y: new(big.Int).SetBytes(y)}, + D: new(big.Int).SetBytes(d), + }, &Key{ + Algorithm: AlgorithmES256, + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: x, + KeyLabelEC2Y: y, + KeyLabelEC2D: d, + }, + }, + "", + }, + { + "ecdsa invalid", &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{Curve: *new(elliptic.Curve), X: big.NewInt(1), Y: big.NewInt(2)}, + D: big.NewInt(3), + }, + nil, + "unsupported curve: ", + }, + { + "ed25519", ed25519.PrivateKey(append(okpd, okpx...)), + &Key{ + Algorithm: AlgorithmEdDSA, Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + KeyLabelOKPD: okpd, + }}, + "", + }, + { + "invalid key", ed25519.PublicKey{1, 2, 3}, + nil, + ErrInvalidPrivKey.Error(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewKeyFromPrivate(tt.k) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("NewKeyFromPrivate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewKeyFromPrivate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewKeyFromPublic(t *testing.T) { + ecx, ecy, _ := newEC2(t, elliptic.P256()) + okpx, _ := newEd25519(t) + tests := []struct { + name string + k crypto.PublicKey + want *Key + wantErr string + }{ + { + "ecdsa", &ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(ecx), Y: new(big.Int).SetBytes(ecy)}, + &Key{ + Algorithm: AlgorithmES256, + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ecx, + KeyLabelEC2Y: ecy, + }, + }, + "", + }, + { + "ecdsa invalid", &ecdsa.PublicKey{Curve: *new(elliptic.Curve), X: big.NewInt(1), Y: big.NewInt(2)}, + nil, + "unsupported curve: ", + }, + { + "ed25519", ed25519.PublicKey(okpx), + &Key{ + Algorithm: AlgorithmEdDSA, + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + }, + }, + "", + }, + { + "invalid key", ed25519.PrivateKey{1, 2, 3, 1, 2, 3}, + nil, + ErrInvalidPubKey.Error(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewKeyFromPublic(tt.k) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("NewKeyFromPublic() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewKeyFromPublic() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKey_Signer(t *testing.T) { + x, d := newEd25519(t) + tests := []struct { + name string + k *Key + wantAlg Algorithm + wantErr string + }{ + { + "without algorithm", &Key{ + Type: KeyTypeOKP, + Ops: []KeyOp{KeyOpSign}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + KeyLabelOKPD: d, + }, + }, + AlgorithmEdDSA, + "", + }, + { + "without key_ops", &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + KeyLabelOKPD: d, + }, + }, + AlgorithmEdDSA, + "", + }, + { + "invalid algorithm", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveP256, + KeyLabelOKPX: x, + KeyLabelOKPD: d, + }, + }, + AlgorithmReserved, + "invalid key: curve not supported for the given key type", + }, + { + "can't sign", &Key{ + Type: KeyTypeOKP, + Ops: []KeyOp{KeyOpVerify}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + KeyLabelOKPD: d, + }, + }, + AlgorithmReserved, + ErrOpNotSupported.Error(), + }, + { + "unsupported key", &Key{ + Type: KeyTypeSymmetric, + Ops: []KeyOp{KeyOpSign}, + Params: map[any]any{ + KeyLabelSymmetricK: d, + }, + }, + AlgorithmReserved, + `unexpected key type "Symmetric"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v, err := tt.k.Signer() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.Signer() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + if got := v.Algorithm(); got != tt.wantAlg { + t.Errorf("Key.Signer().Algorithm() = %v, want %v", got, tt.wantAlg) + } + } + }) + } +} + +func TestKey_Verifier(t *testing.T) { + x, _ := newEd25519(t) + tests := []struct { + name string + k *Key + wantAlg Algorithm + wantErr string + }{ + { + "without algorithm", &Key{ + Type: KeyTypeOKP, + Ops: []KeyOp{KeyOpVerify}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + }, + }, + AlgorithmEdDSA, + "", + }, + { + "without key_ops", &Key{ + Type: KeyTypeOKP, + Algorithm: AlgorithmEdDSA, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + }, + }, + AlgorithmEdDSA, + "", + }, + { + "invalid algorithm", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveP256, + KeyLabelOKPX: x, + }, + }, + AlgorithmReserved, + "invalid key: curve not supported for the given key type", + }, + { + "can't verify", &Key{ + Type: KeyTypeOKP, + Ops: []KeyOp{KeyOpSign}, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: x, + }, + }, + AlgorithmReserved, + ErrOpNotSupported.Error(), + }, + { + "unsupported key", &Key{ + Type: KeyTypeSymmetric, + Ops: []KeyOp{KeyOpVerify}, + Params: map[any]any{ + KeyLabelSymmetricK: x, + }, + }, + AlgorithmReserved, + `unexpected key type "Symmetric"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v, err := tt.k.Verifier() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.Verifier() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + if got := v.Algorithm(); got != tt.wantAlg { + t.Errorf("Key.Verifier().Algorithm() = %v, want %v", got, tt.wantAlg) + } + } + }) + } +} + +func TestKey_PrivateKey(t *testing.T) { + ec256x, ec256y, ec256d := newEC2(t, elliptic.P256()) + ec384x, ec384y, ec384d := newEC2(t, elliptic.P384()) + ec521x, ec521y, ec521d := newEC2(t, elliptic.P521()) + okpx, okpd := newEd25519(t) + tests := []struct { + name string + k *Key + want crypto.PrivateKey + wantErr string + }{ + { + "CurveEd25519", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + KeyLabelOKPD: okpd, + }, + }, + ed25519.PrivateKey(append(okpd, okpx...)), + "", + }, { + "CurveEd25519 missing x", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPD: okpd, + }, + }, + ed25519.PrivateKey(append(okpd, okpx...)), + "", + }, { + "CurveP256", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + KeyLabelEC2D: ec256d, + }, + }, + &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: new(big.Int).SetBytes(ec256x), + Y: new(big.Int).SetBytes(ec256y), + }, + D: new(big.Int).SetBytes(ec256d), + }, + "", + }, { + "CurveP256 missing x and y", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2D: ec256d, + }, + }, + nil, + "invalid private key: compressed point not supported", + }, { + "CurveP384", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + KeyLabelEC2X: ec384x, + KeyLabelEC2Y: ec384y, + KeyLabelEC2D: ec384d, + }, + }, + &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P384(), + X: new(big.Int).SetBytes(ec384x), + Y: new(big.Int).SetBytes(ec384y), + }, + D: new(big.Int).SetBytes(ec384d), + }, + "", + }, { + "CurveP521", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: ec521x, + KeyLabelEC2Y: ec521y, + KeyLabelEC2D: ec521d, + }, + }, + &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P521(), + X: new(big.Int).SetBytes(ec521x), + Y: new(big.Int).SetBytes(ec521y), + }, + D: new(big.Int).SetBytes(ec521d), + }, + "", + }, { + "unknown key type", &Key{ + Type: KeyType(7), + }, + nil, + `unexpected key type "unknown key type value 7"`, + }, { + "OKP unknown curve", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: 70, + KeyLabelOKPX: okpx, + KeyLabelOKPD: okpd, + }, + }, + nil, + `unsupported curve "unknown curve value 70" for key type OKP`, + }, { + "OKP missing d", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + }, + }, + nil, + ErrNotPrivKey.Error(), + }, { + "OKP incorrect x size", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: make([]byte, 10), + KeyLabelOKPD: okpd, + }, + }, + nil, + "invalid key: overflowing coordinate", + }, { + "OKP incorrect d size", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + KeyLabelOKPD: make([]byte, 5), + }, + }, + nil, + "invalid key: overflowing coordinate", + }, { + "EC2 missing D", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrNotPrivKey.Error(), + }, { + "EC2 unknown curve", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: 70, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + KeyLabelEC2D: ec256d, + }, + }, + nil, + `unsupported curve "unknown curve value 70" for key type EC2`, + }, { + "EC2 incorrect x size", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec384x, + KeyLabelEC2Y: ec256y, + KeyLabelEC2D: ec256d, + }, + }, + nil, + "invalid key: overflowing coordinate", + }, { + "EC2 incorrect y size", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec384y, + KeyLabelEC2D: ec256d, + }, + }, + nil, + "invalid key: overflowing coordinate", + }, { + "EC2 incorrect d size", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + KeyLabelEC2D: ec384d, + }, + }, + nil, + "invalid key: overflowing coordinate", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.PrivateKey() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.PrivateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.PrivateKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKey_PublicKey(t *testing.T) { + ec256x, ec256y, _ := newEC2(t, elliptic.P256()) + ec384x, ec384y, _ := newEC2(t, elliptic.P384()) + ec521x, ec521y, _ := newEC2(t, elliptic.P521()) + okpx, _ := newEd25519(t) + tests := []struct { + name string + k *Key + want crypto.PublicKey + wantErr string + }{ + { + "CurveEd25519", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + }, + }, + ed25519.PublicKey(okpx), + "", + }, { + "CurveP256", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + &ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: new(big.Int).SetBytes(ec256x), + Y: new(big.Int).SetBytes(ec256y), + }, + "", + }, { + "CurveP384", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + KeyLabelEC2X: ec384x, + KeyLabelEC2Y: ec384y, + }, + }, + &ecdsa.PublicKey{ + Curve: elliptic.P384(), + X: new(big.Int).SetBytes(ec384x), + Y: new(big.Int).SetBytes(ec384y), + }, + "", + }, { + "CurveP521", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: ec521x, + KeyLabelEC2Y: ec521y, + }, + }, + &ecdsa.PublicKey{ + Curve: elliptic.P521(), + X: new(big.Int).SetBytes(ec521x), + Y: new(big.Int).SetBytes(ec521y), + }, + "", + }, { + "unknown key type", &Key{ + Type: KeyType(7), + }, + nil, + `unexpected key type "unknown key type value 7"`, + }, { + "invalid key type", &Key{ + Type: KeyTypeReserved, + }, + nil, + `invalid key: kty value 0`, + }, { + "OKP missing X", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + }, + }, + nil, + ErrOKPNoPub.Error(), + }, { + "OKP unknown curve", &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: 70, + KeyLabelOKPX: okpx, + }, + }, + nil, + `unsupported curve "unknown curve value 70" for key type OKP`, + }, { + "EC2 missing X", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrEC2NoPub.Error(), + }, { + "EC2 missing Y", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + }, + }, + nil, + ErrEC2NoPub.Error(), + }, { + "EC2 unknown curve", &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: 70, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + `unsupported curve "unknown curve value 70" for key type EC2`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.PublicKey() + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.PublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.PublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyType_String(t *testing.T) { + tests := []struct { + kt KeyType + want string + }{ + {KeyTypeReserved, "Reserved"}, + {KeyTypeOKP, "OKP"}, + {KeyTypeEC2, "EC2"}, + {KeyTypeSymmetric, "Symmetric"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.kt.String(); got != tt.want { + t.Errorf("KeyType.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCurve_String(t *testing.T) { + tests := []struct { + kt Curve + want string + }{ + {CurveP256, "P-256"}, + {CurveP384, "P-384"}, + {CurveP521, "P-521"}, + {CurveX25519, "X25519"}, + {CurveX448, "X448"}, + {CurveEd25519, "Ed25519"}, + {CurveEd448, "Ed448"}, + {CurveReserved, "Reserved"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.kt.String(); got != tt.want { + t.Errorf("Curve.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyOpFromString(t *testing.T) { + tests := []struct { + val string + want KeyOp + want1 bool + }{ + {"sign", KeyOpSign, true}, + {"verify", KeyOpVerify, true}, + {"encrypt", KeyOpEncrypt, true}, + {"decrypt", KeyOpDecrypt, true}, + {"wrapKey", KeyOpWrapKey, true}, + {"unwrapKey", KeyOpUnwrapKey, true}, + {"deriveKey", KeyOpDeriveKey, true}, + {"deriveBits", KeyOpDeriveBits, true}, + {"", KeyOp(0), false}, + {"foo", KeyOp(0), false}, + } + for _, tt := range tests { + t.Run(tt.val, func(t *testing.T) { + got, got1 := KeyOpFromString(tt.val) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KeyOpFromString() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("KeyOpFromString() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func mustHexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +func newEd25519(t *testing.T) (x, d []byte) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + return pub, priv[:32] +} + +func newEC2(t *testing.T, crv elliptic.Curve) (x, y, d []byte) { + t.Helper() + priv, err := ecdsa.GenerateKey(crv, rand.Reader) + if err != nil { + t.Fatal(err) + } + return priv.X.Bytes(), priv.Y.Bytes(), priv.D.Bytes() +} diff --git a/release-checklist.md b/release-checklist.md new file mode 100644 index 0000000..fb09d26 --- /dev/null +++ b/release-checklist.md @@ -0,0 +1,23 @@ +# Release Checklist + +## Overview + +This document describes the checklist to publish a release via GitHub workflow. + +The maintainers may periodically update this checklist based on feedback. + +> [!NOTE] +> Make sure the dependencies in `go.mod` file are expected by the release. +> After updating `go.mod` file, run `go mod tidy` to ensure the `go.sum` file is also updated with any potential changes. + +## Release Process + +1. Determine a [SemVer2](https://semver.org/)-valid version prefixed with the letter `v` for release. For example, `v1.0.0-alpha.1`. +1. Determine the commit to be tagged and released. +1. Create an issue for voting with title similar to `vote: tag v1.0.0-alpha.1` with the proposed commit. +1. Wait for the vote pass. +1. Cut a release branch `release-X.Y` (e.g. `release-1.0`) if it does not exist. The voted commit MUST be the head of the release branch. + - To cut a release branch directly on GitHub, navigate to `https://github.com/veraison/go-cose/tree/{commit}` and then follow the [creating a branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository#creating-a-branch-using-the-branch-dropdown) doc. +1. Draft a new release from the release branch by following the [creating a release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) doc. Set release title to the voted version and create a tag in the **Choose a tag** dropdown menu with the voted version as the tag name. +1. Proofread the draft release, and publish the release. +1. Announce the release in the community. diff --git a/release-management.md b/release-management.md new file mode 100644 index 0000000..6c123bf --- /dev/null +++ b/release-management.md @@ -0,0 +1,153 @@ +# go-cose Release Management + +## Overview + +This document describes [go-cose][go-cose] project release management, which includes release criteria, versioning, supported releases, and supported upgrades. + +The go-cose project maintainers strive to provide a stable go-lang implementation for interacting with [COSE][ietf-cose] constructs. +Stable implies appropriate and measured changes to the library assuring consumers have the necessary functionality to interact with COSE objects. +If you or your project require added functionality, or bug fixes, please open an issue or create a pull request. +The project welcomes all contributions from adding functionality, implementing testing, security reviews to the release management. +Please see [here](https://github.com/veraison#contributing) for how to contribute. + +The maintainers may periodically update this policy based on feedback. + +## Release Versioning + +Consumers of the go-cose module may reference `main` directly, or reference released tags. + +All go-cose [releases][releases] follow a go-lang flavored derivation (`v*`) of the [semver][sem-ver] format, with optional pre-release labels. + +Logical Progression of a release: `v1.0.0-alpha.1` --> `v1.0.0-alpha.2` --> `v1.0.0-rc.1` --> `v1.0.0` + +A new major or minor release will not have an automated release posted until the branch reaches alpha quality. + +- All versions use a preface of `v` +- Given a version `vX.Y.Z`, + - `X` is the [Major](#major-releases) version + - `Y` is the [Minor](#minor-releases) version + - `Z` is the [Patch](#patch-releases) version +- _Optional_ `-alpha.n` | `-rc.n` [pre-release](#pre-release) version + - Each incremental alpha or rc build will bump the suffix (`n`) number. + - It's not expected to have more than 9 alphas or rcs. +The suffix will be a single digit. + - If > 9 builds do occur, the format will simply use two digit indicators (`v1.0.0-alpha.10`) + +> [!IMPORTANT] +> Pre-releases will NOT use the github pre-release flag. + +## Branch Management + +To meet the projects stability goals, go-cose does not currently operate with multiple feature branches. +All active development happens in `main`. +For each release, a branch is created for servicing, following the versioning name. +`v1.0.0-alpha-1` would have an associated [v1.0.0-alpha.1](https://github.com/veraison/go-cose/tree/v1.0.0-alpha.1) branch. +All version and branch names are lower case. + +### Major Releases + +As a best practice, consumers should opt-into new capabilities through major releases. +The go-cose project will not add new functionality to patches or minor releases as this could create a new surface area that may be exploited. +Consumers should make explicit opt-in decisions to upgrade, or possibly downgrade if necessary due to unexpected breaking changes. + +The go-cose project will issue major releases when: + +- Functionality has changed +- Breaking changes are required + +Each major release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. + +### Minor Releases + +The go-cose project will issue minor releases when incremental improvements, or bug fixes are added to existing functionality. +Minor releases will increment the minor field within the [semver][sem-ver] format. + +Each minor release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. + +### Patch Releases + +Patch Releases include bug and security fixes. +Patches will branch from the released branch being patched. +Fixes completed in main may be ported to a patch release if the maintainers believe the effort is justified by requests from the go-cose community. +If a bug fix requires new incremental, non-breaking change functionality, a new minor release may be issued. + +Principals of a patch release: + +- Should be a "safe bet" to upgrade to. +- No breaking changes. +- No feature or surface area changes. +- A "bug fix" that may be a breaking change may require a major release. +- Applicable fixes, including security fixes, may be cherry-picked from main into the latest supported minor `release-X.Y` branches. +- Patch releases are cut from a `release-X.Y` branch. + +Each patch release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. + +### Pre-Release + +As builds of `main` become stable, and a pending release is planned, a pre-release build will be made. +Pre-releases go through one or more `-alpha.n` releases, followed by one or more incremental `-rc.n` releases. + +- **alpha.n:** `X.Y.Z-alpha.n` + - alpha release, cut from the branch where development occurs. +To minimize branch management, no additional branches are maintained for each incremental release. + - Considered an unstable release which should only be used for early development purposes. + - Released incrementally until no additional issues and prs are made against the release. + - Once no triaged issues or pull requests (prs) are scoped to the release, a release candidate (`rc`) is cut. + - To minimize confusion, and the risk of an alpha being widely deployed, alpha branches and released binaries may be removed at the discretion, and a [two-thirds supermajority][super-majority] vote, of the maintainers. +Maintainers will create an Issue, and vote upon it for transparency to the decision to remove a release and/or branch. + - Not [supported](#supported-releases) +- **rc.n:** `X.Y.Z-rc.n` + - Released as needed before a final version is released + - Bugfixes on new features only as reported through usage + - An `rc` is not expected to revert to an alpha release. + - Once no triaged issues or PRs are scoped to the release, an final version is cut. + - A release candidate will typically have at least two weeks of bake time, providing the community time to provide feedback. + - Release candidates are cut from the branch where the work is done. + - To minimize confusion, and the risk of an rc being widely deployed, rc branches and released binaries may be removed at the discretion, and a [two-thirds supermajority][super-majority] vote, of the maintainers. +Maintainers will create an Issue, and vote upon it for transparency to the decision to remove a release and/or branch. + - Not [supported](#supported-releases) + +## Supported Releases + +The go-cose maintainers expect to "support" n (current) and `n-1` major.minor releases. +"Support" means we expect users to be running that version in production. +For example, when `v1.3.0` comes out, `v1.1.x` will no longer be supported for patches, and the maintainers encourage users to upgrade to a supported version as soon as possible. +Support will be provided best effort by the maintainers via GitHub issues and pull requests from the community. + +The go-cose maintainers expect users to stay up-to-date with the versions of go-cose release they use in production, but understand that it may take time to upgrade. +We expect users to be running approximately the latest patch release of a given minor release and encourage users to upgrade as soon as possible. + +While pre-releases may be deleted at the discretion of the maintainers, all Major, Minor and Patch releases should be maintained. +Only in extreme circumstances, as agreed upon by a [two-thirds supermajority][super-majority] of the maintainers, shall a release be removed. + +Applicable fixes, including security fixes, may be cherry-picked into the release branch, depending on severity and feasibility. +Patch releases are cut from that branch as needed. + +## Security Reviews + +The go-cose library is an sdk around underlying crypto libraries, tailored to COSE scenarios. +The go-cose library does not implement cryptographic functionality, reducing the potential risk. +To assure go-cose had the proper baseline, two [security reviews](./reports) were conducted prior to the [v1.0.0](https://github.com/veraison/go-cose/releases/tag/v1.0.0) release + +For each release, new security reviews are evaluated by the maintainers as required or optional. +The go-cose project welcomes additional security reviews. +See [SECURITY.md](./SECURITY.md) for more information. + +## Glossary of Terms + +- **X.Y.Z** refers to the version (based on git tag) of go-cose that is released. +This is the version of the go-cose binary. +- **Breaking changes** refer to schema changes, flag changes, and behavior changes of go-cose that may require existing content to be upgraded and may also introduce changes that could break backward compatibility. +- **Milestone** GitHub milestones are used by maintainers to manage each release. +PRs and Issues for each release should be created as part of a corresponding milestone. +- **Patch releases** refer to applicable fixes, including security fixes, may be backported to support releases, depending on severity and feasibility. + +## Attribution + +This document builds on the ideas and implementations of release processes from the [notation](https://github.com/notaryproject/notation) project. + +[go-cose]: https://github.com/veraison/go-cose +[ietf-cose]: https://datatracker.ietf.org/group/cose/about/ +[sem-ver]: https://semver.org +[releases]: https://github.com/veraison/go-cose/releases +[super-majority]: https://en.wikipedia.org/wiki/Supermajority#Two-thirds_vote diff --git a/reports/README.md b/reports/README.md index d624698..7dc67f2 100644 --- a/reports/README.md +++ b/reports/README.md @@ -1,10 +1,10 @@ # Security Reports -This folder contains all the security review reports for the go-cose library. +This folder contains all the security review reports for the go-cose library. ## List of Security Reports | Review Date | Name of Security Review | Report Location | -|:------------|:--------------------------------------| ------------------------------- +|:------------|:--------------------------------------| -------------------------------| | May 16, 2022 | NCC Group go-cose Security Assessment | [NCC Report](./NCC_Microsoft-go-cose-Report_2022-05-26_v1.0.pdf) | -| July 26, 2022 | Trail of Bits go-cose Security Assessment | [Trail of Bits Report](./Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf) | \ No newline at end of file +| July 26, 2022 | Trail of Bits go-cose Security Assessment | [Trail of Bits Report](./Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf) | diff --git a/rsa.go b/rsa.go index b63098c..bb920d6 100644 --- a/rsa.go +++ b/rsa.go @@ -24,14 +24,20 @@ func (rs *rsaSigner) Algorithm() Algorithm { // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 func (rs *rsaSigner) Sign(rand io.Reader, content []byte) ([]byte, error) { - hash := rs.alg.hashFunc() - digest, err := computeHash(hash, content) + digest, err := rs.alg.computeHash(content) if err != nil { return nil, err } + return rs.SignDigest(rand, digest) +} + +// SignDigest signs message digest with the private key, possibly using +// entropy from rand. +// The resulting signature should follow RFC 8152 section 8. +func (rs *rsaSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { return rs.key.Sign(rand, digest, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 - Hash: hash, + Hash: rs.alg.hashFunc(), }) } @@ -54,12 +60,20 @@ func (rv *rsaVerifier) Algorithm() Algorithm { // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 func (rv *rsaVerifier) Verify(content []byte, signature []byte) error { - hash := rv.alg.hashFunc() - digest, err := computeHash(hash, content) + digest, err := rv.alg.computeHash(content) if err != nil { return err } - if err := rsa.VerifyPSS(rv.key, hash, digest, signature, &rsa.PSSOptions{ + return rv.VerifyDigest(digest, signature) +} + +// VerifyDigest verifies message digest with the public key, returning nil +// for success. +// Otherwise, it returns ErrVerification. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 +func (rv *rsaVerifier) VerifyDigest(digest []byte, signature []byte) error { + if err := rsa.VerifyPSS(rv.key, rv.alg.hashFunc(), digest, signature, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 }); err != nil { return ErrVerification diff --git a/rsa_test.go b/rsa_test.go index e859b78..803406b 100644 --- a/rsa_test.go +++ b/rsa_test.go @@ -36,7 +36,7 @@ func Test_rsaSigner(t *testing.T) { // sign / verify round trip // see also conformance_test.go for strict tests. - content := []byte("hello world") + content := []byte("hello world, مرحبا بالعالم") sig, err := signer.Sign(rand.Reader, content) if err != nil { t.Fatalf("Sign() error = %v", err) @@ -49,6 +49,24 @@ func Test_rsaSigner(t *testing.T) { if err := verifier.Verify(content, sig); err != nil { t.Fatalf("Verifier.Verify() error = %v", err) } + + // digested sign/verify round trip + dsigner, ok := signer.(DigestSigner) + if !ok { + t.Fatalf("signer is not a DigestSigner") + } + digest := sha256.Sum256(content) + dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) + if err != nil { + t.Fatalf("SignDigest() error = %v", err) + } + dverifier, ok := verifier.(DigestVerifier) + if !ok { + t.Fatalf("verifier is not a DigestVerifier") + } + if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { + t.Fatalf("VerifyDigest() error = %v", err) + } } func Test_rsaSigner_SignHashFailure(t *testing.T) { diff --git a/sign.go b/sign.go index cd86d11..a2bb6c0 100644 --- a/sign.go +++ b/sign.go @@ -236,7 +236,7 @@ func (s *Signature) toBeSigned(bodyProtected cbor.RawMessage, payload, external if external == nil { external = []byte{} } - sigStructure := []interface{}{ + sigStructure := []any{ "Signature", // context bodyProtected, // body_protected signProtected, // sign_protected diff --git a/sign1.go b/sign1.go index fa229e9..e1bd4d0 100644 --- a/sign1.go +++ b/sign1.go @@ -174,7 +174,7 @@ func (m *Sign1Message) toBeSigned(external []byte) ([]byte, error) { if external == nil { external = []byte{} } - sigStructure := []interface{}{ + sigStructure := []any{ "Signature1", // context protected, // body_protected external, // external_aad @@ -262,7 +262,7 @@ func (m *UntaggedSign1Message) MarshalCBOR() ([]byte, error) { return encMode.Marshal(content) } -// UnmarshalCBOR decodes a COSE_Sign1 object into an UnataggedSign1Message. +// UnmarshalCBOR decodes a COSE_Sign1 object into an UntaggedSign1Message. func (m *UntaggedSign1Message) UnmarshalCBOR(data []byte) error { if m == nil { return errors.New("cbor: UnmarshalCBOR on nil UntaggedSign1Message pointer") diff --git a/sign_test.go b/sign_test.go index 793d967..4b1f701 100644 --- a/sign_test.go +++ b/sign_test.go @@ -654,13 +654,13 @@ func TestSignature_Sign_Internal(t *testing.T) { }, }, }, - protected: []byte{0x40, 0xa1, 0x00, 0x00}, + protected: []byte{0x43, 0xa1, 0x00, 0x00}, payload: []byte("hello world"), external: []byte{}, toBeSigned: []byte{ 0x85, // array type 0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context - 0x40, 0xa1, 0x00, 0x00, // body_protected + 0x43, 0xa1, 0x00, 0x00, // body_protected 0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // sign_protected 0x40, // external 0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload @@ -2222,7 +2222,7 @@ func TestSignature_toBeSigned(t *testing.T) { payload []byte external []byte want []byte - wantErr bool + wantErr string }{ { name: "valid signature", @@ -2233,12 +2233,12 @@ func TestSignature_toBeSigned(t *testing.T) { }, }, }, - protected: []byte{0x40, 0xa1, 0x00, 0x00}, + protected: []byte{0x43, 0xa1, 0x00, 0x00}, payload: []byte("hello world"), want: []byte{ 0x85, // array type 0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context - 0x40, 0xa1, 0x00, 0x00, // body_protected + 0x43, 0xa1, 0x00, 0x00, // body_protected 0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // sign_protected 0x40, // external 0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload @@ -2255,7 +2255,20 @@ func TestSignature_toBeSigned(t *testing.T) { }, protected: []byte{0x00}, payload: []byte{}, - wantErr: true, + wantErr: "cbor: require bstr type", + }, + { + name: "extraneous protected data", + s: &Signature{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: algorithmMock, + }, + }, + }, + protected: []byte{0x40, 0xa1, 0x00, 0x00}, + payload: []byte("hello world"), + wantErr: "cbor: 3 bytes of extraneous data starting at index 1", }, { name: "invalid sign protected header", @@ -2268,7 +2281,7 @@ func TestSignature_toBeSigned(t *testing.T) { }, protected: []byte{0x40}, payload: []byte{}, - wantErr: true, + wantErr: "protected header: header label: require int / tstr type", }, { name: "invalid raw sign protected header", @@ -2279,15 +2292,17 @@ func TestSignature_toBeSigned(t *testing.T) { }, protected: []byte{0x40}, payload: []byte{}, - wantErr: true, + wantErr: "cbor: require bstr type", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.s.toBeSigned(tt.protected, tt.payload, tt.external) - if (err != nil) != tt.wantErr { - t.Errorf("Signature.toBeSigned() error = %v, wantErr %v", err, tt.wantErr) - return + if err != nil && (err.Error() != tt.wantErr) { + t.Fatalf("Signature.toBeSigned() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil && (tt.wantErr != "") { + t.Fatalf("Signature.toBeSigned() error = %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Signature.toBeSigned() = %v, want %v", got, tt.want) diff --git a/signer.go b/signer.go index 6747546..9992b93 100644 --- a/signer.go +++ b/signer.go @@ -23,6 +23,17 @@ type Signer interface { Sign(rand io.Reader, content []byte) ([]byte, error) } +// DigestSigner is an interface for private keys to sign digested COSE signatures. +type DigestSigner interface { + // Algorithm returns the signing algorithm associated with the private key. + Algorithm() Algorithm + + // SignDigest signs message digest with the private key, possibly using + // entropy from rand. + // The resulting signature should follow RFC 8152 section 8. + SignDigest(rand io.Reader, digest []byte) ([]byte, error) +} + // NewSigner returns a signer with a given signing key. // The signing key can be a golang built-in crypto private key, a key in HSM, or // a remote KMS. @@ -34,9 +45,12 @@ type Signer interface { // public key of type `*rsa.PublicKey`, `*ecdsa.PublicKey`, or // `ed25519.PublicKey` are accepted. // +// The returned signer for rsa and ecdsa keys also implements `cose.DigestSigner`. +// // Note: `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, and `ed25519.PrivateKey` // implement `crypto.Signer`. func NewSigner(alg Algorithm, key crypto.Signer) (Signer, error) { + var errReason string switch alg { case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512: vk, ok := key.Public().(*rsa.PublicKey) @@ -68,14 +82,19 @@ func NewSigner(alg Algorithm, key crypto.Signer) (Signer, error) { key: vk, signer: key, }, nil - case AlgorithmEd25519: + case AlgorithmEdDSA: if _, ok := key.Public().(ed25519.PublicKey); !ok { return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) } return &ed25519Signer{ key: key, }, nil + case AlgorithmReserved: + errReason = "can't be implemented" + case AlgorithmRS256, AlgorithmRS384, AlgorithmRS512: + errReason = "no built-in implementation available" default: - return nil, ErrAlgorithmNotSupported + errReason = "unknown algorithm" } + return nil, fmt.Errorf("can't create new Signer for %s: %s: %w", alg, errReason, ErrAlgorithmNotSupported) } diff --git a/signer_test.go b/signer_test.go index e9475b9..eb04f8a 100644 --- a/signer_test.go +++ b/signer_test.go @@ -77,7 +77,7 @@ func TestNewSigner(t *testing.T) { }, { name: "ed25519 signer", - alg: AlgorithmEd25519, + alg: AlgorithmEdDSA, key: ed25519Key, want: &ed25519Signer{ key: ed25519Key, @@ -85,7 +85,7 @@ func TestNewSigner(t *testing.T) { }, { name: "ed25519 key mismatch", - alg: AlgorithmEd25519, + alg: AlgorithmEdDSA, key: rsaKey, wantErr: "EdDSA: invalid public key", }, @@ -111,9 +111,19 @@ func TestNewSigner(t *testing.T) { wantErr: "RSA key must be at least 2048 bits long", }, { - name: "unknown algorithm", - alg: 0, - wantErr: "algorithm not supported", + name: "unsupported rsa signing algorithm", + alg: AlgorithmRS256, + wantErr: "can't create new Signer for RS256: no built-in implementation available: algorithm not supported", + }, + { + name: "reserved algorithm", + alg: AlgorithmReserved, + wantErr: "can't create new Signer for Reserved: can't be implemented: algorithm not supported", + }, + { + name: "unassigned algorithm", + alg: -1, + wantErr: "can't create new Signer for Algorithm(-1): unknown algorithm: algorithm not supported", }, } for _, tt := range tests { diff --git a/verifier.go b/verifier.go index 1c6e83b..72d2039 100644 --- a/verifier.go +++ b/verifier.go @@ -22,10 +22,27 @@ type Verifier interface { Verify(content, signature []byte) error } +// DigestVerifier is an interface for public keys to verify digested COSE signatures. +type DigestVerifier interface { + // Algorithm returns the signing algorithm associated with the public key. + Algorithm() Algorithm + + // VerifyDigest verifies message digest with the public key, returning nil + // for success. + // Otherwise, it returns ErrVerification. + VerifyDigest(digest, signature []byte) error +} + // NewVerifier returns a verifier with a given public key. // Only golang built-in crypto public keys of type `*rsa.PublicKey`, // `*ecdsa.PublicKey`, and `ed25519.PublicKey` are accepted. +// When `*ecdsa.PublicKey` is specified, its curve must be supported by +// crypto/ecdh. +// +// The returned signer for rsa and ecdsa keys also implements +// `cose.DigestSigner`. func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) { + var errReason string switch alg { case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512: vk, ok := key.(*rsa.PublicKey) @@ -46,14 +63,17 @@ func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) { if !ok { return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) } - if !vk.Curve.IsOnCurve(vk.X, vk.Y) { - return nil, errors.New("public key point is not on curve") + if _, err := vk.ECDH(); err != nil { + if err.Error() == "ecdsa: invalid public key" { + return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) + } + return nil, fmt.Errorf("%v: %w: %v", alg, ErrInvalidPubKey, err) } return &ecdsaVerifier{ alg: alg, key: vk, }, nil - case AlgorithmEd25519: + case AlgorithmEdDSA: vk, ok := key.(ed25519.PublicKey) if !ok { return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) @@ -61,7 +81,12 @@ func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) { return &ed25519Verifier{ key: vk, }, nil + case AlgorithmReserved: + errReason = "can't be implemented" + case AlgorithmRS256, AlgorithmRS384, AlgorithmRS512: + errReason = "no built-in implementation available" default: - return nil, ErrAlgorithmNotSupported + errReason = "unknown algorithm" } + return nil, fmt.Errorf("can't create new Verifier for %s: %s: %w", alg, errReason, ErrAlgorithmNotSupported) } diff --git a/verifier_test.go b/verifier_test.go index b662cd0..82d84ca 100644 --- a/verifier_test.go +++ b/verifier_test.go @@ -48,6 +48,13 @@ func TestNewVerifier(t *testing.T) { // craft an EC public key with the x-coord not on curve ecdsaKeyPointNotOnCurve := generateBogusECKey() + // craft an EC public key with a curve not supported by crypto/ecdh + ecdsaKeyUnsupportedCurve := &ecdsa.PublicKey{ + Curve: ecdsaKey.Curve.Params(), + X: ecdsaKey.X, + Y: ecdsaKey.Y, + } + // run tests tests := []struct { name string @@ -73,7 +80,7 @@ func TestNewVerifier(t *testing.T) { }, { name: "ed25519 verifier", - alg: AlgorithmEd25519, + alg: AlgorithmEdDSA, key: ed25519Key, want: &ed25519Verifier{ key: ed25519Key, @@ -81,7 +88,7 @@ func TestNewVerifier(t *testing.T) { }, { name: "ed25519 invalid public key", - alg: AlgorithmEd25519, + alg: AlgorithmEdDSA, key: rsaKey, wantErr: "EdDSA: invalid public key", }, @@ -107,15 +114,31 @@ func TestNewVerifier(t *testing.T) { wantErr: "RSA key must be at least 2048 bits long", }, { - name: "unknown algorithm", - alg: 0, - wantErr: "algorithm not supported", + name: "unsupported rsa signing algorithm", + alg: AlgorithmRS256, + wantErr: "can't create new Verifier for RS256: no built-in implementation available: algorithm not supported", + }, + { + name: "reserved algorithm", + alg: AlgorithmReserved, + wantErr: "can't create new Verifier for Reserved: can't be implemented: algorithm not supported", + }, + { + name: "unassigned algorithm", + alg: -1, + wantErr: "can't create new Verifier for Algorithm(-1): unknown algorithm: algorithm not supported", }, { name: "bogus ecdsa public key (point not on curve)", alg: AlgorithmES256, key: ecdsaKeyPointNotOnCurve, - wantErr: "public key point is not on curve", + wantErr: "ES256: invalid public key", + }, + { + name: "ecdsa public key with unsupported curve", + alg: AlgorithmES256, + key: ecdsaKeyUnsupportedCurve, + wantErr: "ES256: invalid public key: ecdsa: unsupported curve by crypto/ecdh", }, } for _, tt := range tests {