diff --git a/.gitignore b/.gitignore index b04f6b0..9a26cee 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ vendor/ # Editor files .vscode/ +.idea/ diff --git a/errors.go b/errors.go index 770dc9e..88bd0b7 100644 --- a/errors.go +++ b/errors.go @@ -10,6 +10,7 @@ var ( ErrEmptySignature = errors.New("empty signature") ErrInvalidAlgorithm = errors.New("invalid algorithm") ErrMissingPayload = errors.New("missing payload") + ErrMultiplePayloads = errors.New("multiple payloads") ErrNoSignatures = errors.New("no signatures attached") ErrUnavailableHashFunc = errors.New("hash function is not available") ErrVerification = errors.New("verification error") diff --git a/sign.go b/sign.go index a2bb6c0..1159903 100644 --- a/sign.go +++ b/sign.go @@ -398,12 +398,37 @@ func (m *SignMessage) UnmarshalCBOR(data []byte) error { // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a // later release. func (m *SignMessage) Sign(rand io.Reader, external []byte, signers ...Signer) error { + return m.sign(rand, nil, external, signers) +} + +// SignDetached signs a SignMessage using the provided signers corresponding to the +// signatures. +// +// See `Signature.Sign()` for advanced signing scenarios. +// +// 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 (m *SignMessage) SignDetached(rand io.Reader, detached, external []byte, signers ...Signer) error { + if detached == nil { + return ErrMissingPayload + } + return m.sign(rand, detached, external, signers) +} + +func (m *SignMessage) sign(rand io.Reader, detached, external []byte, signers []Signer) error { if m == nil { return errors.New("signing nil SignMessage") } - if m.Payload == nil { - return ErrMissingPayload + + payload, err := checkPayload(m.Payload, detached) + if err != nil { + return err } + switch len(m.Signatures) { case 0: return ErrNoSignatures @@ -415,14 +440,14 @@ func (m *SignMessage) Sign(rand io.Reader, external []byte, signers ...Signer) e // populate common parameters var protected cbor.RawMessage - protected, err := m.Headers.MarshalProtected() + protected, err = m.Headers.MarshalProtected() if err != nil { return err } // sign message accordingly for i, signature := range m.Signatures { - if err := signature.Sign(rand, signers[i], protected, m.Payload, external); err != nil { + if err := signature.Sign(rand, signers[i], protected, payload, external); err != nil { return err } } @@ -443,12 +468,43 @@ func (m *SignMessage) Sign(rand io.Reader, external []byte, signers ...Signer) e // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a // later release. func (m *SignMessage) Verify(external []byte, verifiers ...Verifier) error { + return m.verify(nil, external, verifiers...) +} + +// VerifyDetached verifies the signatures on the SignMessage against the corresponding +// verifier, returning nil on success or a suitable error if verification fails. +// +// Returns ErrMissingPayload if detached is nil, and ErrMultiplePayloads if the SignMessage +// also has an attached payload. +// +// See `Signature.Verify()` for advanced verification scenarios like threshold +// policies. +// +// 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 (m *SignMessage) VerifyDetached(detached, external []byte, verifiers ...Verifier) error { + if detached == nil { + return ErrMissingPayload + } + msg := *m + msg.Payload = detached + return msg.Verify(external, verifiers...) +} + +func (m *SignMessage) verify(detached, external []byte, verifiers ...Verifier) error { if m == nil { return errors.New("verifying nil SignMessage") } - if m.Payload == nil { - return ErrMissingPayload + + payload, err := checkPayload(m.Payload, detached) + if err != nil { + return err } + switch len(m.Signatures) { case 0: return ErrNoSignatures @@ -460,16 +516,31 @@ func (m *SignMessage) Verify(external []byte, verifiers ...Verifier) error { // populate common parameters var protected cbor.RawMessage - protected, err := m.Headers.MarshalProtected() + protected, err = m.Headers.MarshalProtected() if err != nil { return err } // verify message accordingly for i, signature := range m.Signatures { - if err := signature.Verify(verifiers[i], protected, m.Payload, external); err != nil { + if err := signature.Verify(verifiers[i], protected, payload, external); err != nil { return err } } return nil } + +// checkPayload checks that exactly one of payload and detached is non-nil. +func checkPayload(payload, detached []byte) ([]byte, error) { + if detached != nil { + if payload != nil { + return nil, ErrMultiplePayloads + } + return detached, nil + } + + if payload == nil { + return nil, ErrMissingPayload + } + return payload, nil +} diff --git a/sign1.go b/sign1.go index e1bd4d0..0d395dd 100644 --- a/sign1.go +++ b/sign1.go @@ -87,12 +87,35 @@ func (m *Sign1Message) UnmarshalCBOR(data []byte) error { // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 func (m *Sign1Message) Sign(rand io.Reader, external []byte, signer Signer) error { + return m.sign(rand, nil, external, signer) +} + +// SignDetached signs a Sign1Message using the provided Signer. +// The signature is stored in m.Signature. +// +// Note that m.Signature is only valid as long as m.Headers.Protected +// remains unchanged after calling this method. +// It is possible to modify m.Headers.Unprotected after signing, +// i.e., add counter signatures or timestamps. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func (m *Sign1Message) SignDetached(rand io.Reader, detached, external []byte, signer Signer) error { + if detached == nil { + return ErrMissingPayload + } + return m.sign(rand, detached, external, signer) +} + +func (m *Sign1Message) sign(rand io.Reader, detached, external []byte, signer Signer) error { if m == nil { return errors.New("signing nil Sign1Message") } - if m.Payload == nil { - return ErrMissingPayload + + payload, err := checkPayload(m.Payload, detached) + if err != nil { + return err } + if len(m.Signature) > 0 { return errors.New("Sign1Message signature already has signature bytes") } @@ -100,13 +123,13 @@ func (m *Sign1Message) Sign(rand io.Reader, external []byte, signer Signer) erro // check algorithm if present. // `alg` header MUST be present if there is no externally supplied data. alg := signer.Algorithm() - err := m.Headers.ensureSigningAlgorithm(alg, external) + err = m.Headers.ensureSigningAlgorithm(alg, external) if err != nil { return err } // sign the message - toBeSigned, err := m.toBeSigned(external) + toBeSigned, err := m.toBeSigned(external, payload) if err != nil { return err } @@ -124,12 +147,30 @@ func (m *Sign1Message) Sign(rand io.Reader, external []byte, signer Signer) erro // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 func (m *Sign1Message) Verify(external []byte, verifier Verifier) error { + return m.verify(nil, external, verifier) +} + +// VerifyDetached verifies the signature on the Sign1Message returning nil on +// success or a suitable error if verification fails. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func (m *Sign1Message) VerifyDetached(detached, external []byte, verifier Verifier) error { + if detached == nil { + return ErrMissingPayload + } + return m.verify(detached, external, verifier) +} + +func (m *Sign1Message) verify(detached, external []byte, verifier Verifier) error { if m == nil { return errors.New("verifying nil Sign1Message") } - if m.Payload == nil { - return ErrMissingPayload + + payload, err := checkPayload(m.Payload, detached) + if err != nil { + return err } + if len(m.Signature) == 0 { return ErrEmptySignature } @@ -137,13 +178,13 @@ func (m *Sign1Message) Verify(external []byte, verifier Verifier) error { // check algorithm if present. // `alg` header MUST present if there is no externally supplied data. alg := verifier.Algorithm() - err := m.Headers.ensureVerificationAlgorithm(alg, external) + err = m.Headers.ensureVerificationAlgorithm(alg, external) if err != nil { return err } // verify the message - toBeSigned, err := m.toBeSigned(external) + toBeSigned, err := m.toBeSigned(external, payload) if err != nil { return err } @@ -153,7 +194,7 @@ func (m *Sign1Message) Verify(external []byte, verifier Verifier) error { // toBeSigned constructs Sig_structure, computes and returns ToBeSigned. // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 -func (m *Sign1Message) toBeSigned(external []byte) ([]byte, error) { +func (m *Sign1Message) toBeSigned(external []byte, payload []byte) ([]byte, error) { // create a Sig_structure and populate it with the appropriate fields. // // Sig_structure = [ @@ -178,7 +219,7 @@ func (m *Sign1Message) toBeSigned(external []byte) ([]byte, error) { "Signature1", // context protected, // body_protected external, // external_aad - m.Payload, // payload + payload, // payload } // create the value ToBeSigned by encoding the Sig_structure to a byte @@ -238,7 +279,7 @@ func (m *Sign1Message) doUnmarshal(data []byte) error { // This method is a wrapper of `Sign1Message.Sign()`. // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 -func Sign1(rand io.Reader, signer Signer, headers Headers, payload []byte, external []byte) ([]byte, error) { +func Sign1(rand io.Reader, signer Signer, headers Headers, payload, external []byte) ([]byte, error) { msg := Sign1Message{ Headers: headers, Payload: payload, @@ -250,6 +291,22 @@ func Sign1(rand io.Reader, signer Signer, headers Headers, payload []byte, exter return msg.MarshalCBOR() } +// Sign1Detached signs a Sign1Message using the provided Signer. +// +// This method is a wrapper of `Sign1Message.SignDetached()`. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func Sign1Detached(rand io.Reader, signer Signer, headers Headers, detached, external []byte) ([]byte, error) { + msg := Sign1Message{ + Headers: headers, + } + err := msg.SignDetached(rand, detached, external, signer) + if err != nil { + return nil, err + } + return msg.MarshalCBOR() +} + type UntaggedSign1Message Sign1Message // MarshalCBOR encodes UntaggedSign1Message into a COSE_Sign1 object. @@ -293,6 +350,19 @@ func (m *UntaggedSign1Message) Sign(rand io.Reader, external []byte, signer Sign return (*Sign1Message)(m).Sign(rand, external, signer) } +// SignDetached signs an UntaggedSign1Message using the provided Signer. +// The signature is stored in m.Signature. +// +// Note that m.Signature is only valid as long as m.Headers.Protected +// remains unchanged after calling this method. +// It is possible to modify m.Headers.Unprotected after signing, +// i.e., add counter signatures or timestamps. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func (m *UntaggedSign1Message) SignDetached(rand io.Reader, detached, external []byte, signer Signer) error { + return (*Sign1Message)(m).SignDetached(rand, detached, external, signer) +} + // Verify verifies the signature on the UntaggedSign1Message returning nil on success or // a suitable error if verification fails. // @@ -301,12 +371,20 @@ func (m *UntaggedSign1Message) Verify(external []byte, verifier Verifier) error return (*Sign1Message)(m).Verify(external, verifier) } +// VerifyDetached verifies the signature on the UntaggedSign1Message returning +// nil on success or a suitable error if verification fails. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func (m *UntaggedSign1Message) VerifyDetached(detached, external []byte, verifier Verifier) error { + return (*Sign1Message)(m).VerifyDetached(detached, external, verifier) +} + // Sign1Untagged signs an UntaggedSign1Message using the provided Signer. // // This method is a wrapper of `UntaggedSign1Message.Sign()`. // // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 -func Sign1Untagged(rand io.Reader, signer Signer, headers Headers, payload []byte, external []byte) ([]byte, error) { +func Sign1Untagged(rand io.Reader, signer Signer, headers Headers, payload, external []byte) ([]byte, error) { msg := UntaggedSign1Message{ Headers: headers, Payload: payload, @@ -317,3 +395,19 @@ func Sign1Untagged(rand io.Reader, signer Signer, headers Headers, payload []byt } return msg.MarshalCBOR() } + +// Sign1UntaggedDetached signs an UntaggedSign1Message using the provided Signer. +// +// This method is a wrapper of `UntaggedSign1Message.SignDetached()`. +// +// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 +func Sign1UntaggedDetached(rand io.Reader, signer Signer, headers Headers, detached, external []byte) ([]byte, error) { + msg := UntaggedSign1Message{ + Headers: headers, + } + err := msg.SignDetached(rand, detached, external, signer) + if err != nil { + return nil, err + } + return msg.MarshalCBOR() +} diff --git a/sign1_test.go b/sign1_test.go index 8bf2054..9260cfa 100644 --- a/sign1_test.go +++ b/sign1_test.go @@ -967,7 +967,7 @@ func TestSign1Message_toBeSigned(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.m.toBeSigned(tt.external) + got, err := tt.m.toBeSigned(tt.external, tt.m.Payload) if (err != nil) != tt.wantErr { t.Errorf("Sign1Message.toBeSigned() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/sign_test.go b/sign_test.go index 4b1f701..08f181e 100644 --- a/sign_test.go +++ b/sign_test.go @@ -1968,6 +1968,167 @@ func TestSignMessage_Sign(t *testing.T) { }) } + // detached payloads + detachedTests := []struct { + name string + msg *SignMessage + detached []byte + wantErr string + }{ + { + name: "valid message", + msg: &SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: "text/plain", + }, + Unprotected: UnprotectedHeader{ + "extra": "test", + }, + }, + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + }, + }, + detached: []byte("lorem ipsum"), + }, + { + name: "multiple payloads", + msg: &SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: "text/plain", + }, + Unprotected: UnprotectedHeader{ + "extra": "test", + }, + }, + Payload: []byte("lorem ipsum"), + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + }, + }, + detached: []byte("lorem ipsum"), + wantErr: "multiple payloads", + }, + { + name: "missing payload", + msg: &SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: "text/plain", + }, + Unprotected: UnprotectedHeader{ + "extra": "test", + }, + }, + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + }, + }, + wantErr: "missing payload", + }, + { + name: "message payload only", + msg: &SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: "text/plain", + }, + Unprotected: UnprotectedHeader{ + "extra": "test", + }, + }, + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + }, + Payload: []byte("lorem ipsum"), + }, + wantErr: "missing payload", + }, + } + for _, tt := range detachedTests { + t.Run(tt.name, func(t *testing.T) { + err := tt.msg.SignDetached(rand.Reader, tt.detached, nil, signers...) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("SignMessage.Sign() error = %v, wantErr %v", err, tt.wantErr) + } + return + } else if tt.wantErr != "" { + t.Errorf("SignMessage.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err := tt.msg.VerifyDetached(tt.detached, nil, verifiers...); err != nil { + t.Errorf("SignMessage.Verify() error = %v", err) + } + }) + } + // special cases t.Run("no signer", func(t *testing.T) { msg := &SignMessage{ @@ -2167,6 +2328,77 @@ func TestSignMessage_Verify(t *testing.T) { }) } + // detached payloads + detachedTests := []struct { + name string + detachedOnSign []byte + detachedOnVerify []byte + wantErr string + }{ + { + name: "round trip on valid detached message", + detachedOnSign: []byte("lorem ipsum"), + detachedOnVerify: []byte("lorem ipsum"), + }, + { + name: "missing payload", + detachedOnSign: []byte("lorem ipsum"), + wantErr: "missing payload", + }, + { + name: "changes payload", + detachedOnSign: []byte("lorem ipsum"), + detachedOnVerify: []byte("lorem ipsum dolor sit amet"), + wantErr: "verification error", + }, + } + for _, tt := range detachedTests { + t.Run(tt.name, func(t *testing.T) { + // generate message and sign + msg := &SignMessage{ + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelContentType: "text/plain", + }, + Unprotected: UnprotectedHeader{ + "extra": "test", + }, + }, + Signatures: []*Signature{ + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES256, + }, + Unprotected: UnprotectedHeader{ + HeaderLabelKeyID: []byte("42"), + }, + }, + }, + { + Headers: Headers{ + Protected: ProtectedHeader{ + HeaderLabelAlgorithm: AlgorithmES512, + }, + }, + }, + }, + } + if err := msg.SignDetached(rand.Reader, tt.detachedOnSign, nil, signers...); err != nil { + t.Errorf("SignMessage.SignDetached() error = %v", err) + return + } + + // verify message + err := msg.VerifyDetached(tt.detachedOnVerify, nil, verifiers...) + if err != nil && (err.Error() != tt.wantErr) { + t.Errorf("SignMessage.VerifyDetached() error = %v, wantErr %v", err, tt.wantErr) + } else if err == nil && (tt.wantErr != "") { + t.Errorf("SignMessage.VerifyDetached() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } + // special cases t.Run("nil payload", func(t *testing.T) { // payload is detached msg := &SignMessage{ @@ -2310,3 +2542,50 @@ func TestSignature_toBeSigned(t *testing.T) { }) } } + +func TestSign_checkPayload(t *testing.T) { + tests := []struct { + name string + payload []byte + detached []byte + want []byte + wantErr string + }{ + { + name: "payload, nil detached", + payload: []byte("payload"), + want: []byte("payload"), + }, + { + name: "nil payload, detached", + detached: []byte("detached"), + want: []byte("detached"), + }, + { + name: "nil payload, nil detached", + wantErr: ErrMissingPayload.Error(), + }, + { + name: "payload and detached", + payload: []byte("payload"), + detached: []byte("detached"), + wantErr: ErrMultiplePayloads.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkPayload(tt.payload, tt.detached) + if err != nil && (err.Error() != tt.wantErr) { + t.Errorf("checkPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } else if err == nil && (tt.wantErr != "") { + t.Errorf("checkPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Equal(tt.want, got) { + t.Fatalf("checkPayload(): got = %v, want = %v", got, tt.want) + } + }) + } +}