First mirror commit.
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..4c9e5bc
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,78 @@
+# Description:
+#   Auto-imported from github.com/veraison/go-cose
+
+load("//tools/build_defs/license:license.bzl", "license")
+
+package(
+    default_applicable_licenses = [":license"],
+    default_compatible_with = ["//buildenv/target:non_prod"],
+    default_visibility = ["//third_party/golang:__subpackages__"],
+)
+
+license(
+    name = "license",
+    package_name = "github_com/veraison/go_cose/v/v0",
+)
+
+licenses(["reciprocal"])
+
+exports_files(["LICENSE"])
+
+go_library(
+    name = "cose",
+    srcs = [
+        "algorithm.go",
+        "cbor.go",
+        "ecdsa.go",
+        "ed25519.go",
+        "errors.go",
+        "headers.go",
+        "rsa.go",
+        "sign.go",
+        "sign1.go",
+        "signer.go",
+        "verifier.go",
+    ],
+    deps = [
+        "//go/tools/nogo/allowlist/crypto:elliptic",
+        "//third_party/golang/github_com/fxamacker/cbor/v/v2:cbor",
+    ],
+)
+
+go_test(
+    name = "cose_test",
+    srcs = [
+        "algorithm_test.go",
+        "cbor_test.go",
+        "ecdsa_test.go",
+        "ed25519_test.go",
+        "headers_test.go",
+        "rsa_test.go",
+        "sign1_test.go",
+        "sign_test.go",
+        "signer_test.go",
+        "verifier_test.go",
+    ],
+    library = ":cose",
+    deps = [
+        "//go/tools/nogo/allowlist/crypto:elliptic",
+        "//third_party/golang/github_com/fxamacker/cbor/v/v2:cbor",
+    ],
+)
+
+go_test(
+    name = "cose_x_test",
+    srcs = [
+        "bench_test.go",
+        "conformance_test.go",
+        "example_test.go",
+        "fuzz_test.go",
+    ],
+    data = glob(["testdata/*"]),
+    deps = [
+        ":cose",
+        "//base/go:runfiles",
+        "//go/tools/nogo/allowlist/crypto:elliptic",
+        "//third_party/golang/github_com/fxamacker/cbor/v/v2:cbor",
+    ],
+)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..498baa3
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,15 @@
+# Community Participation Guidelines
+
+This repository is governed by Mozilla's code of conduct and etiquette guidelines. 
+For more details, please read the
+[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 
+
+## How to Report
+For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
+
+<!--
+## Project Specific Etiquette
+
+In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
+Please update for your project.
+-->
diff --git a/GOIMPORT/AUTOPATCHES/conformance_test.go.patch b/GOIMPORT/AUTOPATCHES/conformance_test.go.patch
new file mode 100644
index 0000000..18da0bb
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/conformance_test.go.patch
@@ -0,0 +1,29 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ 	"encoding/json"
+ 	"errors"
+ 	"math/big"
+-	"os"
+ 	"path/filepath"
+ 	"strings"
+ 	"testing"
+ 
++	"google3/base/go/runfiles"
++	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
++
+ 	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+ 	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+ )
+@@ func TestConformance(t *testing.T) {
+ 			if tt.skip {
+ 				t.SkipNow()
+ 			}
+-			data, err := os.ReadFile(filepath.Join("testdata", tt.name+".json"))
++			data, err := runfiles.ReadFile(filepath.Join("google3/third_party/golang/github_com/veraison/go_cose/v/v0/testdata", tt.name+".json"))
+ 			if err != nil {
+ 				t.Fatal(err)
+ 			}
diff --git a/GOIMPORT/AUTOPATCHES/ecdsa.go.patch b/GOIMPORT/AUTOPATCHES/ecdsa.go.patch
new file mode 100644
index 0000000..2968c4b
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/ecdsa.go.patch
@@ -0,0 +1,14 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ 	"fmt"
+ 	"io"
+ 	"math/big"
++
++	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+ )
+ 
+ // I2OSP - Integer-to-Octet-String primitive converts a nonnegative integer to
diff --git a/GOIMPORT/AUTOPATCHES/ecdsa_test.go.patch b/GOIMPORT/AUTOPATCHES/ecdsa_test.go.patch
new file mode 100644
index 0000000..f0d79d1
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/ecdsa_test.go.patch
@@ -0,0 +1,14 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ 	"math/big"
+ 	"reflect"
+ 	"testing"
++
++	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+ )
+ 
+ func TestI2OSP(t *testing.T) {
diff --git a/GOIMPORT/AUTOPATCHES/example_test.go.patch b/GOIMPORT/AUTOPATCHES/example_test.go.patch
new file mode 100644
index 0000000..63e396c
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/example_test.go.patch
@@ -0,0 +1,14 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ 	_ "crypto/sha512"
+ 	"fmt"
+ 
++	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
++
+ 	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+ )
+ 
diff --git a/GOIMPORT/AUTOPATCHES/fuzz_test.go.patch b/GOIMPORT/AUTOPATCHES/fuzz_test.go.patch
new file mode 100644
index 0000000..bc40524
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/fuzz_test.go.patch
@@ -0,0 +1,14 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ 	"strings"
+ 	"testing"
+ 
++	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
++
+ 	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+ 	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+ )
diff --git a/GOIMPORT/CONFIGURATION b/GOIMPORT/CONFIGURATION
new file mode 100644
index 0000000..690bc45
--- /dev/null
+++ b/GOIMPORT/CONFIGURATION
@@ -0,0 +1,40 @@
+# This file configures imports managed by //third_party/golang/import.go.
+# See go/thirdpartygo for more information.
+
+# ImportFiles specifies which files to import from the upstream source.
+[ImportFiles]
+  exclude: /[.]        # exclude hidden files and folders (e.g., ".gitignore")
+  exclude: /METADATA$  # see go/metadata
+  exclude: /OWNERS$    # see go/owners
+  exclude: /BUILD$     # see go/build
+  include: .
+
+# ImportRenames specifies whether to rename any source files or directories.
+[ImportRenames]
+  # Rename common names for the LICENSE file; adjust if necessary.
+  sed: s:^/LICEN[CS]E([.](gpl|md|txt))?$:/LICENSE:I  # see go/thirdpartylicenses
+
+  # Mangle special characters to comply with Piper and Blaze limitations.
+  sed: `:loop; s:/(.*)[.-](.*)/:/\1_\2/:; t loop;`  # dots/dashes in directories to underscores
+  sed: "s:[ ]:_:g"                                  # spaces to underscores
+  sed: "s:[()]::g"                                  # remove parentheses
+
+# RewriteFiles specifies which Go source files to rewrite import paths for.
+[RewriteFiles]
+  exclude: /testdata/
+  exclude: /[._][^/]*$
+  include: .
+
+# GoogleFiles specifies files added to the import to support use within google3.
+[GoogleFiles]
+  include: ^/GOIMPORT/  # configuration and metadata for import.go tool
+  include: /METADATA    # see go/metadata
+  include: /OWNERS$     # see go/owners
+  include: /BUILD$      # see go/build
+  include: /g3doc/      # see go/g3doc
+
+  # Treat files with google_ prefix as Google-internal. This may conflict with
+  # actual files upstream with a google_ prefix; delete if necessary.
+  include: /google_[^/]+$
+
+  exclude: .
diff --git a/GOIMPORT/MANIFEST b/GOIMPORT/MANIFEST
new file mode 100644
index 0000000..cca3fa5
--- /dev/null
+++ b/GOIMPORT/MANIFEST
@@ -0,0 +1,64 @@
+# This file contains a manifest of all files imported by import.go.
+# This is for both human review and machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
++ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx BUILD
+= c744d0086694af5eefd7bed10096691b CODE_OF_CONDUCT.md
++ 51c389601e1179870e5c2954ead27eed GOIMPORT/AUTOPATCHES/conformance_test.go.patch
++ 7b26d4ebb4cd54be45883dab6162159c GOIMPORT/AUTOPATCHES/ecdsa.go.patch
++ 18da151af4be13f0198c47ebf8e743f7 GOIMPORT/AUTOPATCHES/ecdsa_test.go.patch
++ dec2eb289a5253c543d9b5139c08b180 GOIMPORT/AUTOPATCHES/example_test.go.patch
++ 45c2db07477b30bce27c212460ae940f GOIMPORT/AUTOPATCHES/fuzz_test.go.patch
++ 14fb5725605f7a85b7641bf4c6c3b98c GOIMPORT/CONFIGURATION
++ 5ad43f4b5ce9414a0a1d58777e675102 GOIMPORT/MANIFEST
+= 03e8fb6c51acc2814555abaf89fc517f LICENSE
++ 536deb581e31a8b80c57cd70f36bd174 METADATA
++ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OWNERS
+= 80729f6e8496b06ae2e06098629cea16 README.md
+= 81d42c63000da542f7a6a4652e2a8342 algorithm.go
+= 0d6728651ba250f6a971e7386a56c286 algorithm_test.go
+= abadd2173fcde2dfb99a228593c59994 bench_test.go
+= 7bb4d504085e9451671fd79d2e05d83e cbor.go
+= bfaaf485086f597446f6299250931a21 cbor_test.go
+~ 6ad74e5dd4f279e2a0155516a9f0eaaa conformance_test.go
+~ 0b9f429f023b8406c54d9362f052596e ecdsa.go
+~ d7ee5bcebeb629fb84e85c0c0ac48fd1 ecdsa_test.go
+= 003a4f3c6bd05015f1372494366d0c4e ed25519.go
+= 3a1a80c0f8165eeef05b6ea48b2b7bfc ed25519_test.go
+= 150eb976c3e62a8d7630aca068055b58 errors.go
+~ a696eb9c793e81c4fbb00cd56e025e3b example_test.go
+~ 2bd0525eac9faf5a0cdacd508b4c9109 fuzz_test.go
+= a107332113a4ffca69428e675b914e9d go.mod
+= 60f82780f4f960b3c66e2f0d076532c8 go.sum
+= 5cb4e7a3c7844b37bbe3b15cf497ca44 headers.go
+= df573fc958e56cc16bf697793b8662f8 headers_test.go
+= bc3197312ea79ba591dd434b39f7f2d0 rsa.go
+= 1f2ced7d294874e02cacf27c7c87d325 rsa_test.go
+= c85fe06c19fdb92678108aea1fc77a09 scripts/licenses.sh
+= b6603d755246e1c8305260b0253483e2 sign.go
+= 35734fc1b88f16ca50e9b0e6089f5c4e sign1.go
+= c94e904dbc5dd9869c0c0d328c4c7737 sign1_test.go
+= 3250befd0eecaee28dd378406fb495a4 sign_test.go
+= 488e3a537b3cd9a1d62644db24d594a0 signer.go
+= c9269e7d2f8057809e2689852541719a signer_test.go
+= 1c8f4527e75c6eeb821b033c0455f104 testdata/sign1-sign-0000.json
+= 098731b0d879068ad726740d578a7676 testdata/sign1-sign-0001.json
+= 0983759aa2eee81d3c6bc6b3bb53ef4e testdata/sign1-sign-0002.json
+= 69b8bd65c15e7f4f2be3040ce1ea7c94 testdata/sign1-sign-0003.json
+= 9f98ee1521f2915cc8aa10c394103cab testdata/sign1-sign-0004.json
+= 174ef6327374e81ed998b9c8db45dbdb testdata/sign1-sign-0005.json
+= 56a8962e1b689797729a2a10cd469fd7 testdata/sign1-sign-0006.json
+= d8e58c285857e3653ff04f22587c4162 testdata/sign1-verify-0000.json
+= dade58d029bf6d5d209fc060e2392c6e testdata/sign1-verify-0001.json
+= 6318f2ba7995294f05b28b8013e87006 testdata/sign1-verify-0002.json
+= 16266e566cce28ec5fd4619ba083ece7 testdata/sign1-verify-0003.json
+= c78cda512ab263ca47c36502d90427d1 testdata/sign1-verify-0004.json
+= 8a11f36e7d8cfb4043d1c572eea354d3 testdata/sign1-verify-0005.json
+= c0a8cbdd877b84fd6a7bdcc249066ad0 testdata/sign1-verify-0006.json
+= 0c1af132c1a3cc04d7a3c88d99a21072 testdata/sign1-verify-negative-0000.json
+= 024652794b3ebe410ff71bd21c1871d9 testdata/sign1-verify-negative-0001.json
+= d9b6872a2947c326b5e3fe421af6c516 testdata/sign1-verify-negative-0002.json
+= 1574b9e608e16104c2c55f68cb903ffa testdata/sign1-verify-negative-0003.json
+= 8d540d7e8d9b9df73304ed9185de7b5b verifier.go
+= 15e0cbebd7a6511f96d44c2d87ae335f verifier_test.go
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7bdf415
--- /dev/null
+++ b/README.md
@@ -0,0 +1,106 @@
+# go-cose
+
+[![go.dev](https://pkg.go.dev/badge/github.com/veraison/go-cose.svg)](https://pkg.go.dev/github.com/veraison/go-cose)
+[![tests](https://github.com/veraison/go-cose/workflows/ci/badge.svg)](https://github.com/veraison/go-cose/actions?query=workflow%3Aci)
+[![coverage](https://github.com/veraison/go-cose/workflows/cover%20%E2%89%A589%25/badge.svg)](https://github.com/veraison/go-cose/actions?query=workflow%3A%22cover%20%E2%89%A589%25%22)
+
+A [COSE](https://tools.ietf.org/html/rfc8152) library for go.
+
+## Installation
+
+go-cose is compatible with modern Go releases in module mode, with Go installed:
+
+```bash
+go get github.com/veraison/go-cose
+```
+
+will resolve and add the package to the current development module, along with its dependencies.
+
+Alternatively the same can be achieved if you use import in a package:
+
+```go
+import "github.com/veraison/go-cose"
+```
+
+and run `go get` without parameters.
+
+Finally, to use the top-of-trunk version of this repo, use the following command:
+
+```bash
+go get github.com/veraison/go-cose@main
+```
+
+## Usage
+
+```go
+import "github.com/veraison/go-cose"
+```
+
+Construct a new COSE_Sign1 message, then sign it using ECDSA w/ SHA-512 and finally marshal it. For example:
+
+```go
+// create a signer
+privateKey, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+signer, _ := cose.NewSigner(cose.AlgorithmES512, privateKey)
+
+// create message to be signed
+msg := cose.NewSign1Message()
+msgToSign.Payload = []byte("hello world")
+msg.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
+
+// sign message
+_ = msg.Sign(rand.Reader, nil, signer)
+
+// marshal message
+data, _ := msg.MarshalCBOR()
+```
+
+Verify a raw COSE_Sign1 message. For example:
+
+```go
+// create a verifier from a trusted private key
+publicKey := privateKey.Public()
+verifier, _ := cose.NewVerifier(cose.AlgorithmES512, publicKey)
+
+// create a sign message from a raw COSE_Sign1 payload
+var msg cose.Sign1Message
+_ = msg.UnmarshalCBOR(raw)
+_ = msg.Verify(nil, verifier)
+```
+
+## Features
+
+### Signing and Verifying Objects
+
+go-cose supports two different signature structures:
+- [cose.Sign1Message](https://pkg.go.dev/github.com/veraison/go-cose#Sign1Message) implements [COSE_Sign1](https://datatracker.ietf.org/doc/html/rfc8152#section-4.2).
+- [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.
+
+### Built-in Algorithms
+
+go-cose has built-in supports the following algorithms:
+- PS{256,384,512}: RSASSA-PSS w/ SHA as defined in RFC 8230.
+- ES{256,384,512}: ECDSA w/ SHA as defined in RFC 8152.
+- Ed25519: PureEdDSA as defined in RFC 8152.
+
+### Custom Algorithms
+
+The supported algorithms can be extended at runtime by using [cose.RegisterAlgorithm](https://pkg.go.dev/github.com/veraison/go-cose#RegisterAlgorithm).
+
+[API docs](https://pkg.go.dev/github.com/veraison/go-cose)
+
+### Conformance Tests
+
+go-cose runs the [GlueCOSE](https://github.com/gluecose/test-vectors) test suite on every local `go test` execution.
+These are also executed on every CI job.
+
+### Fuzz Tests
+
+go-cose implements several fuzz tests using [Go's native fuzzing](https://go.dev/doc/fuzz).
+
+Fuzzing requires Go 1.18 or higher, and can be executed as follows:
+
+```bash
+go test -fuzz=FuzzSign1
+```
diff --git a/algorithm.go b/algorithm.go
new file mode 100644
index 0000000..9532c20
--- /dev/null
+++ b/algorithm.go
@@ -0,0 +1,180 @@
+package cose
+
+import (
+	"crypto"
+	"hash"
+	"strconv"
+)
+
+// Algorithms supported by this library.
+//
+// When using an algorithm which requires hashing,
+// make sure the associated hash function is linked to the binary.
+const (
+	// RSASSA-PSS w/ SHA-256 by RFC 8230.
+	// Requires an available crypto.SHA256.
+	AlgorithmPS256 Algorithm = -37
+
+	// RSASSA-PSS w/ SHA-384 by RFC 8230.
+	// Requires an available crypto.SHA384.
+	AlgorithmPS384 Algorithm = -38
+
+	// RSASSA-PSS w/ SHA-512 by RFC 8230.
+	// Requires an available crypto.SHA512.
+	AlgorithmPS512 Algorithm = -39
+
+	// ECDSA w/ SHA-256 by RFC 8152.
+	// Requires an available crypto.SHA256.
+	AlgorithmES256 Algorithm = -7
+
+	// ECDSA w/ SHA-384 by RFC 8152.
+	// Requires an available crypto.SHA384.
+	AlgorithmES384 Algorithm = -35
+
+	// ECDSA w/ SHA-512 by RFC 8152.
+	// Requires an available crypto.SHA512.
+	AlgorithmES512 Algorithm = -36
+
+	// PureEdDSA by RFC 8152.
+	AlgorithmEd25519 Algorithm = -8
+)
+
+// Algorithm represents an IANA algorithm entry in the COSE Algorithms registry.
+// Algorithms with string values are not supported.
+//
+// # See Also
+//
+// COSE Algorithms: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
+//
+// RFC 8152 16.4: https://datatracker.ietf.org/doc/html/rfc8152#section-16.4
+type Algorithm int64
+
+// extAlgorithm describes an extended algorithm, which is not implemented this
+// library.
+type extAlgorithm struct {
+	// Name of the algorithm.
+	Name string
+
+	// Hash is the hash algorithm associated with the algorithm.
+	// If HashFunc presents, Hash is ignored.
+	// If HashFunc does not present and Hash is set to 0, no hash is used.
+	Hash crypto.Hash
+
+	// HashFunc is the hash algorithm associated with the algorithm.
+	// HashFunc is preferred in the case that the hash algorithm is not
+	// supported by the golang build-in crypto hashes.
+	// For regular scenarios, use Hash instead.
+	HashFunc func() hash.Hash
+}
+
+// extAlgorithms contains extended algorithms.
+var extAlgorithms map[Algorithm]extAlgorithm
+
+// String returns the name of the algorithm
+func (a Algorithm) String() string {
+	switch a {
+	case AlgorithmPS256:
+		return "PS256"
+	case AlgorithmPS384:
+		return "PS384"
+	case AlgorithmPS512:
+		return "PS512"
+	case AlgorithmES256:
+		return "ES256"
+	case AlgorithmES384:
+		return "ES384"
+	case AlgorithmES512:
+		return "ES512"
+	case AlgorithmEd25519:
+		// As stated in RFC 8152 8.2, only the pure EdDSA version is used for
+		// COSE.
+		return "EdDSA"
+	}
+	if alg, ok := extAlgorithms[a]; ok {
+		return alg.Name
+	}
+	return "unknown algorithm value " + strconv.Itoa(int(a))
+}
+
+// hashFunc returns the hash associated with the algorithm supported by this
+// library.
+func (a Algorithm) hashFunc() (crypto.Hash, bool) {
+	switch a {
+	case AlgorithmPS256, AlgorithmES256:
+		return crypto.SHA256, true
+	case AlgorithmPS384, AlgorithmES384:
+		return crypto.SHA384, true
+	case AlgorithmPS512, AlgorithmES512:
+		return crypto.SHA512, true
+	case AlgorithmEd25519:
+		return 0, true
+	}
+	return 0, false
+}
+
+// newHash returns a new hash instance for computing the digest specified in the
+// algorithm.
+// Returns nil if no hash is required for the message.
+func (a Algorithm) newHash() (hash.Hash, error) {
+	h, ok := a.hashFunc()
+	if !ok {
+		alg, ok := extAlgorithms[a]
+		if !ok {
+			return nil, ErrUnknownAlgorithm
+		}
+		if alg.HashFunc != nil {
+			return alg.HashFunc(), nil
+		}
+		h = alg.Hash
+	}
+	if h == 0 {
+		// no hash required
+		return nil, nil
+	}
+	if h.Available() {
+		return h.New(), nil
+	}
+	return nil, ErrUnavailableHashFunc
+}
+
+// computeHash computing the digest using the hash specified in the algorithm.
+// Returns the input data if no hash is required for the message.
+func (a Algorithm) computeHash(data []byte) ([]byte, error) {
+	h, err := a.newHash()
+	if err != nil {
+		return nil, err
+	}
+	if h == nil {
+		return data, nil
+	}
+	if _, err := h.Write(data); err != nil {
+		return nil, err
+	}
+	return h.Sum(nil), nil
+}
+
+// RegisterAlgorithm provides extensibility for the cose library to support
+// private algorithms or algorithms not yet registered in IANA.
+// The existing algorithms cannot be re-registered.
+// The parameter `hash` is the hash algorithm associated with the algorithm. If
+// hashFunc presents, hash is ignored. If hashFunc does not present and hash is
+// set to 0, no hash is used for this algorithm.
+// The parameter `hashFunc` is preferred in the case that the hash algorithm is not
+// supported by the golang build-in crypto hashes.
+func RegisterAlgorithm(alg Algorithm, name string, hash crypto.Hash, hashFunc func() hash.Hash) error {
+	if _, ok := alg.hashFunc(); ok {
+		return ErrAlgorithmRegistered
+	}
+	if _, ok := extAlgorithms[alg]; ok {
+		return ErrAlgorithmRegistered
+	}
+	if extAlgorithms == nil {
+		extAlgorithms = make(map[Algorithm]extAlgorithm)
+	}
+	extAlgorithms[alg] = extAlgorithm{
+		Name:     name,
+		Hash:     hash,
+		HashFunc: hashFunc,
+	}
+	return nil
+}
diff --git a/algorithm_test.go b/algorithm_test.go
new file mode 100644
index 0000000..75eca2a
--- /dev/null
+++ b/algorithm_test.go
@@ -0,0 +1,246 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/sha256"
+	"reflect"
+	"testing"
+)
+
+// resetExtendedAlgorithm cleans up extended algorithms
+func resetExtendedAlgorithm() {
+	extAlgorithms = nil
+}
+
+func TestAlgorithm_String(t *testing.T) {
+	defer resetExtendedAlgorithm()
+
+	// register extended algorithms
+	algFoo := Algorithm(-102)
+	if err := RegisterAlgorithm(algFoo, "foo", 0, nil); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+	algBar := Algorithm(-98)
+	if err := RegisterAlgorithm(algBar, "bar", 0, nil); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+
+	// 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: "extended algorithm: foo",
+			alg:  algFoo,
+			want: "foo",
+		},
+		{
+			name: "extended algorithm: bar",
+			alg:  algBar,
+			want: "bar",
+		},
+		{
+			name: "unknown algorithm",
+			alg:  0,
+			want: "unknown algorithm value 0",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.alg.String(); got != tt.want {
+				t.Errorf("Algorithm.String() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestAlgorithm_computeHash(t *testing.T) {
+	defer resetExtendedAlgorithm()
+
+	// register extended algorithms
+	algFoo := Algorithm(-102)
+	if err := RegisterAlgorithm(algFoo, "foo", crypto.SHA256, nil); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+	algBar := Algorithm(-98)
+	if err := RegisterAlgorithm(algBar, "bar", 0, sha256.New); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+	algPlain := Algorithm(-112)
+	if err := RegisterAlgorithm(algPlain, "plain", 0, nil); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+	algUnavailableHash := Algorithm(-117)
+	if err := RegisterAlgorithm(algUnavailableHash, "unknown hash", 42, nil); err != nil {
+		t.Fatalf("RegisterAlgorithm() = %v", err)
+	}
+
+	// run tests
+	data := []byte("hello world")
+	tests := []struct {
+		name    string
+		alg     Algorithm
+		want    []byte
+		wantErr error
+	}{
+		{
+			name: "PS256",
+			alg:  AlgorithmPS256,
+			want: []byte{
+				0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa,
+				0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9,
+			},
+		},
+		{
+			name: "PS384",
+			alg:  AlgorithmPS384,
+			want: []byte{
+				0xfd, 0xbd, 0x8e, 0x75, 0xa6, 0x7f, 0x29, 0xf7, 0x01, 0xa4, 0xe0, 0x40, 0x38, 0x5e, 0x2e, 0x23,
+				0x98, 0x63, 0x03, 0xea, 0x10, 0x23, 0x92, 0x11, 0xaf, 0x90, 0x7f, 0xcb, 0xb8, 0x35, 0x78, 0xb3,
+				0xe4, 0x17, 0xcb, 0x71, 0xce, 0x64, 0x6e, 0xfd, 0x08, 0x19, 0xdd, 0x8c, 0x08, 0x8d, 0xe1, 0xbd,
+			},
+		},
+		{
+			name: "PS512",
+			alg:  AlgorithmPS512,
+			want: []byte{
+				0x30, 0x9e, 0xcc, 0x48, 0x9c, 0x12, 0xd6, 0xeb, 0x4c, 0xc4, 0x0f, 0x50, 0xc9, 0x02, 0xf2, 0xb4,
+				0xd0, 0xed, 0x77, 0xee, 0x51, 0x1a, 0x7c, 0x7a, 0x9b, 0xcd, 0x3c, 0xa8, 0x6d, 0x4c, 0xd8, 0x6f,
+				0x98, 0x9d, 0xd3, 0x5b, 0xc5, 0xff, 0x49, 0x96, 0x70, 0xda, 0x34, 0x25, 0x5b, 0x45, 0xb0, 0xcf,
+				0xd8, 0x30, 0xe8, 0x1f, 0x60, 0x5d, 0xcf, 0x7d, 0xc5, 0x54, 0x2e, 0x93, 0xae, 0x9c, 0xd7, 0x6f,
+			},
+		},
+		{
+			name: "ES256",
+			alg:  AlgorithmES256,
+			want: []byte{
+				0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa,
+				0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9,
+			},
+		},
+		{
+			name: "ES384",
+			alg:  AlgorithmES384,
+			want: []byte{
+				0xfd, 0xbd, 0x8e, 0x75, 0xa6, 0x7f, 0x29, 0xf7, 0x01, 0xa4, 0xe0, 0x40, 0x38, 0x5e, 0x2e, 0x23,
+				0x98, 0x63, 0x03, 0xea, 0x10, 0x23, 0x92, 0x11, 0xaf, 0x90, 0x7f, 0xcb, 0xb8, 0x35, 0x78, 0xb3,
+				0xe4, 0x17, 0xcb, 0x71, 0xce, 0x64, 0x6e, 0xfd, 0x08, 0x19, 0xdd, 0x8c, 0x08, 0x8d, 0xe1, 0xbd,
+			},
+		},
+		{
+			name: "ES512",
+			alg:  AlgorithmES512,
+			want: []byte{
+				0x30, 0x9e, 0xcc, 0x48, 0x9c, 0x12, 0xd6, 0xeb, 0x4c, 0xc4, 0x0f, 0x50, 0xc9, 0x02, 0xf2, 0xb4,
+				0xd0, 0xed, 0x77, 0xee, 0x51, 0x1a, 0x7c, 0x7a, 0x9b, 0xcd, 0x3c, 0xa8, 0x6d, 0x4c, 0xd8, 0x6f,
+				0x98, 0x9d, 0xd3, 0x5b, 0xc5, 0xff, 0x49, 0x96, 0x70, 0xda, 0x34, 0x25, 0x5b, 0x45, 0xb0, 0xcf,
+				0xd8, 0x30, 0xe8, 0x1f, 0x60, 0x5d, 0xcf, 0x7d, 0xc5, 0x54, 0x2e, 0x93, 0xae, 0x9c, 0xd7, 0x6f,
+			},
+		},
+		{
+			name: "Ed25519",
+			alg:  AlgorithmEd25519,
+			want: data,
+		},
+		{
+			name: "extended algorithm with crypto.Hash",
+			alg:  algFoo,
+			want: []byte{
+				0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa,
+				0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9,
+			},
+		},
+		{
+			name: "extended algorithm with hashFunc",
+			alg:  algBar,
+			want: []byte{
+				0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa,
+				0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9,
+			},
+		},
+		{
+			name: "extended algorithm without hash",
+			alg:  algPlain,
+			want: data,
+		},
+		{
+			name:    "unknown algorithm",
+			alg:     0,
+			wantErr: ErrUnknownAlgorithm,
+		},
+		{
+			name:    "unknown hash",
+			alg:     algUnavailableHash,
+			wantErr: ErrUnavailableHashFunc,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.alg.computeHash(data)
+			if err != tt.wantErr {
+				t.Errorf("Algorithm.computeHash() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Algorithm.computeHash() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestRegisterAlgorithm(t *testing.T) {
+	defer resetExtendedAlgorithm()
+
+	// register existing algorithm (should fail)
+	if err := RegisterAlgorithm(AlgorithmES256, "ES256", crypto.SHA256, nil); err != ErrAlgorithmRegistered {
+		t.Errorf("RegisterAlgorithm() error = %v, wantErr %v", err, ErrAlgorithmRegistered)
+	}
+
+	// register external algorithm
+	algFoo := Algorithm(-102)
+	if err := RegisterAlgorithm(algFoo, "foo", 0, nil); err != nil {
+		t.Errorf("RegisterAlgorithm() error = %v, wantErr %v", err, false)
+	}
+
+	// double register external algorithm (should fail)
+	if err := RegisterAlgorithm(algFoo, "foo", 0, nil); err != ErrAlgorithmRegistered {
+		t.Errorf("RegisterAlgorithm() error = %v, wantErr %v", err, ErrAlgorithmRegistered)
+	}
+}
diff --git a/bench_test.go b/bench_test.go
new file mode 100644
index 0000000..e2fb3ac
--- /dev/null
+++ b/bench_test.go
@@ -0,0 +1,90 @@
+package cose_test
+
+import (
+	"io"
+	"testing"
+
+	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+)
+
+func newSign1Message() *cose.Sign1Message {
+	return &cose.Sign1Message{
+		Headers: cose.Headers{
+			Protected: cose.ProtectedHeader{
+				cose.HeaderLabelAlgorithm: cose.AlgorithmES256,
+			},
+			Unprotected: cose.UnprotectedHeader{
+				cose.HeaderLabelKeyID: 1,
+			},
+		},
+		Payload:   make([]byte, 100),
+		Signature: make([]byte, 32),
+	}
+}
+
+type noSigner struct{}
+
+func (noSigner) Algorithm() cose.Algorithm {
+	return cose.AlgorithmES256
+}
+
+func (noSigner) Sign(_ io.Reader, digest []byte) ([]byte, error) {
+	return digest, nil
+}
+
+func (noSigner) Verify(_, _ []byte) error {
+	return nil
+}
+
+func BenchmarkSign1Message_MarshalCBOR(b *testing.B) {
+	msg := newSign1Message()
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err := msg.MarshalCBOR()
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkSign1Message_UnmarshalCBOR(b *testing.B) {
+	data, err := newSign1Message().MarshalCBOR()
+	if err != nil {
+		b.Fatal(err)
+	}
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		var m cose.Sign1Message
+		err = m.UnmarshalCBOR(data)
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkSign1Message_Sign(b *testing.B) {
+	msg := newSign1Message()
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		msg.Signature = nil
+		err := msg.Sign(zeroSource{}, nil, noSigner{})
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkSign1Message_Verify(b *testing.B) {
+	msg := newSign1Message()
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		err := msg.Verify(nil, noSigner{})
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}
diff --git a/cbor.go b/cbor.go
new file mode 100644
index 0000000..bd29604
--- /dev/null
+++ b/cbor.go
@@ -0,0 +1,84 @@
+package cose
+
+import (
+	"bytes"
+	"errors"
+	"io"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+)
+
+// CBOR Tags for COSE signatures registered in the IANA "CBOR Tags" registry.
+//
+// Reference: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags
+const (
+	CBORTagSignMessage  = 98
+	CBORTagSign1Message = 18
+)
+
+// Pre-configured modes for CBOR encoding and decoding.
+var (
+	encMode                  cbor.EncMode
+	decMode                  cbor.DecMode
+	decModeWithTagsForbidden cbor.DecMode
+)
+
+func init() {
+	var err error
+
+	// init encode mode
+	encOpts := cbor.EncOptions{
+		Sort:        cbor.SortCanonical,        // sort map keys
+		IndefLength: cbor.IndefLengthForbidden, // no streaming
+	}
+	encMode, err = encOpts.EncMode()
+	if err != nil {
+		panic(err)
+	}
+
+	// init decode mode
+	decOpts := cbor.DecOptions{
+		DupMapKey:   cbor.DupMapKeyEnforcedAPF, // duplicated key not allowed
+		IndefLength: cbor.IndefLengthForbidden, // no streaming
+		IntDec:      cbor.IntDecConvertSigned,  // decode CBOR uint/int to Go int64
+	}
+	decMode, err = decOpts.DecMode()
+	if err != nil {
+		panic(err)
+	}
+	decOpts.TagsMd = cbor.TagsForbidden
+	decModeWithTagsForbidden, err = decOpts.DecMode()
+	if err != nil {
+		panic(err)
+	}
+}
+
+// byteString represents a "bstr / nil" type.
+type byteString []byte
+
+// UnmarshalCBOR decodes data into a "bstr / nil" type.
+// It also ensures the data is of major type 2 since []byte can be alternatively
+// interpreted as an array of bytes.
+//
+// Note: `github.com/fxamacker/cbor/v2` considers the primitive value
+// `undefined` (major type 7, value 23) as nil, which is not recognized by COSE.
+//
+// Related Code: https://github.com/fxamacker/cbor/blob/v2.4.0/decode.go#L709
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.3
+func (s *byteString) UnmarshalCBOR(data []byte) error {
+	if s == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil byteString pointer")
+	}
+	if len(data) == 0 {
+		return io.EOF // same error as returned by cbor.Unmarshal()
+	}
+	if bytes.Equal(data, []byte{0xf6}) {
+		*s = nil
+		return nil
+	}
+	if data[0]>>5 != 2 { // major type 2: bstr
+		return errors.New("cbor: require bstr type")
+	}
+	return decModeWithTagsForbidden.Unmarshal(data, (*[]byte)(s))
+}
diff --git a/cbor_test.go b/cbor_test.go
new file mode 100644
index 0000000..2fdd6b9
--- /dev/null
+++ b/cbor_test.go
@@ -0,0 +1,77 @@
+package cose
+
+import (
+	"bytes"
+	"testing"
+)
+
+func Test_byteString_UnmarshalCBOR(t *testing.T) {
+	// test nil pointer
+	t.Run("nil byteString pointer", func(t *testing.T) {
+		var s *byteString
+		data := []byte{0x40}
+		if err := s.UnmarshalCBOR(data); err == nil {
+			t.Errorf("want error on nil *byteString")
+		}
+	})
+
+	// test others
+	tests := []struct {
+		name    string
+		data    []byte
+		want    byteString
+		wantErr bool
+	}{
+		{
+			name: "valid string",
+			data: []byte{0x43, 0x66, 0x6f, 0x6f},
+			want: []byte{0x66, 0x6f, 0x6f},
+		},
+		{
+			name: "empty string",
+			data: []byte{0x40},
+			want: []byte{},
+		},
+		{
+			name: "nil string",
+			data: []byte{0xf6},
+			want: nil,
+		},
+		{
+			name:    "undefined string",
+			data:    []byte{0xf7},
+			wantErr: true,
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name:    "tagged string",
+			data:    []byte{0xc2, 0x40},
+			wantErr: true,
+		},
+		{
+			name:    "array of bytes", // issue #46
+			data:    []byte{0x82, 0x00, 0x1},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got byteString
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("byteString.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if !bytes.Equal(got, tt.want) {
+				t.Errorf("byteString.UnmarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/conformance_test.go b/conformance_test.go
new file mode 100644
index 0000000..4b5b158
--- /dev/null
+++ b/conformance_test.go
@@ -0,0 +1,321 @@
+package cose_test
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rsa"
+	_ "crypto/sha256"
+	_ "crypto/sha512"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"math/big"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"google3/base/go/runfiles"
+	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+)
+
+type TestCase struct {
+	UUID        string   `json:"uuid"`
+	Title       string   `json:"title"`
+	Description string   `json:"description"`
+	Key         Key      `json:"key"`
+	Alg         string   `json:"alg"`
+	Sign1       *Sign1   `json:"sign1::sign"`
+	Verify1     *Verify1 `json:"sign1::verify"`
+}
+
+type Key map[string]string
+
+type Sign1 struct {
+	Payload            string `json:"payload"`
+	ProtectedHeaders   *CBOR  `json:"protectedHeaders"`
+	UnprotectedHeaders *CBOR  `json:"unprotectedHeaders"`
+	External           string `json:"external"`
+	Detached           bool   `json:"detached"`
+	TBS                CBOR   `json:"tbsHex"`
+	Output             CBOR   `json:"expectedOutput"`
+	OutputLength       int    `json:"fixedOutputLength"`
+}
+
+type Verify1 struct {
+	TaggedCOSESign1 CBOR   `json:"taggedCOSESign1"`
+	External        string `json:"external"`
+	Verify          bool   `json:"shouldVerify"`
+}
+
+type CBOR struct {
+	CBORHex  string `json:"cborHex"`
+	CBORDiag string `json:"cborDiag"`
+}
+
+// Conformance samples are taken from
+// https://github.com/gluecose/test-vectors.
+var testCases = []struct {
+	name          string
+	deterministic bool
+	err           string
+	skip          bool
+}{
+	{name: "sign1-sign-0000"},
+	{name: "sign1-sign-0001"},
+	{name: "sign1-sign-0002"},
+	{name: "sign1-sign-0003"},
+	{name: "sign1-sign-0004", deterministic: true},
+	{name: "sign1-sign-0005", deterministic: true},
+	{name: "sign1-sign-0006", deterministic: true},
+	{name: "sign1-verify-0000"},
+	{name: "sign1-verify-0001"},
+	{name: "sign1-verify-0002"},
+	{name: "sign1-verify-0003"},
+	{name: "sign1-verify-0004"},
+	{name: "sign1-verify-0005"},
+	{name: "sign1-verify-0006"},
+	{name: "sign1-verify-negative-0000", err: "cbor: invalid protected header: cbor: require bstr type"},
+	{name: "sign1-verify-negative-0001", err: "cbor: invalid protected header: cbor: protected header: require map type"},
+	{name: "sign1-verify-negative-0002", err: "cbor: invalid protected header: cbor: found duplicate map key \"1\" at map element index 1"},
+	{name: "sign1-verify-negative-0003", err: "cbor: invalid unprotected header: cbor: found duplicate map key \"4\" at map element index 1"},
+}
+
+func TestConformance(t *testing.T) {
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.skip {
+				t.SkipNow()
+			}
+			data, err := runfiles.ReadFile(filepath.Join("google3/third_party/golang/github_com/veraison/go_cose/v/v0/testdata", tt.name+".json"))
+			if err != nil {
+				t.Fatal(err)
+			}
+			var tc TestCase
+			err = json.Unmarshal(data, &tc)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if tc.Sign1 != nil {
+				testSign1(t, &tc, tt.deterministic)
+			} else if tc.Verify1 != nil {
+				testVerify1(t, &tc, tt.err)
+			} else {
+				t.Fatal("test case not supported")
+			}
+		})
+	}
+}
+
+func testVerify1(t *testing.T, tc *TestCase, wantErr string) {
+	var err error
+	defer func() {
+		if tc.Verify1.Verify && err != nil {
+			t.Fatal(err)
+		} else if !tc.Verify1.Verify {
+			if err == nil {
+				t.Fatal("Verify1 should have failed")
+			}
+			if wantErr != "" {
+				if got := err.Error(); !strings.Contains(got, wantErr) {
+					t.Fatalf("error mismatch; want %q, got %q", wantErr, got)
+				}
+			}
+		}
+	}()
+	var verifier cose.Verifier
+	_, verifier, err = getSigner(tc, false)
+	if err != nil {
+		return
+	}
+	var sigMsg cose.Sign1Message
+	err = sigMsg.UnmarshalCBOR(mustHexToBytes(tc.Verify1.TaggedCOSESign1.CBORHex))
+	if err != nil {
+		return
+	}
+	var external []byte
+	if tc.Verify1.External != "" {
+		external = mustHexToBytes(tc.Verify1.External)
+	}
+	err = sigMsg.Verify(external, verifier)
+	if tc.Verify1.Verify && err != nil {
+		t.Fatal(err)
+	} else if !tc.Verify1.Verify && err == nil {
+		t.Fatal("Verify1 should have failed")
+	}
+}
+
+func testSign1(t *testing.T, tc *TestCase, deterministic bool) {
+	signer, verifier, err := getSigner(tc, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sig := tc.Sign1
+	sigMsg := cose.NewSign1Message()
+	sigMsg.Payload = mustHexToBytes(sig.Payload)
+	sigMsg.Headers, err = decodeHeaders(mustHexToBytes(sig.ProtectedHeaders.CBORHex), mustHexToBytes(sig.UnprotectedHeaders.CBORHex))
+	if err != nil {
+		t.Fatal(err)
+	}
+	var external []byte
+	if sig.External != "" {
+		external = mustHexToBytes(sig.External)
+	}
+	err = sigMsg.Sign(new(zeroSource), external, signer)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = sigMsg.Verify(external, verifier)
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err := sigMsg.MarshalCBOR()
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := mustHexToBytes(sig.Output.CBORHex)
+	if !deterministic {
+		got = got[:sig.OutputLength]
+		want = want[:sig.OutputLength]
+	}
+	if !bytes.Equal(want, got) {
+		t.Fatalf("unexpected output:\nwant: %x\n got: %x", want, got)
+	}
+}
+
+func getSigner(tc *TestCase, private bool) (cose.Signer, cose.Verifier, error) {
+	pkey, err := getKey(tc.Key, private)
+	if err != nil {
+		return nil, nil, err
+	}
+	alg := mustNameToAlg(tc.Alg)
+	signer, err := cose.NewSigner(alg, pkey)
+	if err != nil {
+		return nil, nil, err
+	}
+	verifier, err := cose.NewVerifier(alg, pkey.Public())
+	if err != nil {
+		return nil, nil, err
+	}
+	return signer, verifier, nil
+}
+
+func getKey(key Key, private bool) (crypto.Signer, error) {
+	switch key["kty"] {
+	case "RSA":
+		pkey := &rsa.PrivateKey{
+			PublicKey: rsa.PublicKey{
+				N: mustBase64ToBigInt(key["n"]),
+				E: mustBase64ToInt(key["e"]),
+			},
+		}
+		if private {
+			pkey.D = mustBase64ToBigInt(key["d"])
+			pkey.Primes = []*big.Int{mustBase64ToBigInt(key["p"]), mustBase64ToBigInt(key["q"])}
+			pkey.Precomputed = rsa.PrecomputedValues{
+				Dp:        mustBase64ToBigInt(key["dp"]),
+				Dq:        mustBase64ToBigInt(key["dq"]),
+				Qinv:      mustBase64ToBigInt(key["qi"]),
+				CRTValues: make([]rsa.CRTValue, 0),
+			}
+		}
+		return pkey, nil
+	case "EC":
+		var c elliptic.Curve
+		switch key["crv"] {
+		case "P-224":
+			c = elliptic.P224()
+		case "P-256":
+			c = elliptic.P256()
+		case "P-384":
+			c = elliptic.P384()
+		case "P-521":
+			c = elliptic.P521()
+		default:
+			return nil, errors.New("unsupported EC curve: " + key["crv"])
+		}
+		pkey := &ecdsa.PrivateKey{
+			PublicKey: ecdsa.PublicKey{
+				X:     mustBase64ToBigInt(key["x"]),
+				Y:     mustBase64ToBigInt(key["y"]),
+				Curve: c,
+			},
+		}
+		if private {
+			pkey.D = mustBase64ToBigInt(key["d"])
+		}
+		return pkey, nil
+	}
+	return nil, errors.New("unsupported key type: " + key["kty"])
+}
+
+// zeroSource is an io.Reader that returns an unlimited number of zero bytes.
+type zeroSource struct{}
+
+func (zeroSource) Read(b []byte) (n int, err error) {
+	for i := range b {
+		b[i] = 0
+	}
+
+	return len(b), nil
+}
+
+var encMode, _ = cbor.CanonicalEncOptions().EncMode()
+
+func decodeHeaders(protected, unprotected []byte) (hdr cose.Headers, err error) {
+	// test-vectors encodes the protected header as a map instead of a map wrapped in a bstr.
+	// UnmarshalFromRaw expects the former, so wrap the map here before passing it to UnmarshalFromRaw.
+	hdr.RawProtected, err = encMode.Marshal(protected)
+	if err != nil {
+		return
+	}
+	hdr.RawUnprotected = unprotected
+	err = hdr.UnmarshalFromRaw()
+	return hdr, err
+}
+
+func mustBase64ToInt(s string) int {
+	return int(mustBase64ToBigInt(s).Int64())
+}
+
+func mustHexToBytes(s string) []byte {
+	b, err := hex.DecodeString(s)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+func mustBase64ToBigInt(s string) *big.Int {
+	val, err := base64.RawURLEncoding.DecodeString(s)
+	if err != nil {
+		panic(err)
+	}
+	return new(big.Int).SetBytes(val)
+}
+
+// mustNameToAlg returns the algorithm associated to name.
+// The content of name is not defined in any RFC,
+// but it's what the test cases use to identify algorithms.
+func mustNameToAlg(name string) cose.Algorithm {
+	switch name {
+	case "PS256":
+		return cose.AlgorithmPS256
+	case "PS384":
+		return cose.AlgorithmPS384
+	case "PS512":
+		return cose.AlgorithmPS512
+	case "ES256":
+		return cose.AlgorithmES256
+	case "ES384":
+		return cose.AlgorithmES384
+	case "ES512":
+		return cose.AlgorithmES512
+	}
+	panic("algorithm name not found: " + name)
+}
diff --git a/ecdsa.go b/ecdsa.go
new file mode 100644
index 0000000..fdef90c
--- /dev/null
+++ b/ecdsa.go
@@ -0,0 +1,152 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"encoding/asn1"
+	"errors"
+	"fmt"
+	"io"
+	"math/big"
+
+	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+)
+
+// I2OSP - Integer-to-Octet-String primitive converts a nonnegative integer to
+// an octet string of a specified length `len(buf)`, and stores it in `buf`.
+// I2OSP is used for encoding ECDSA signature (r, s) into byte strings.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8017#section-4.1
+func I2OSP(x *big.Int, buf []byte) error {
+	if x.Sign() < 0 {
+		return errors.New("I2OSP: negative integer")
+	}
+	if x.BitLen() > len(buf)*8 {
+		return errors.New("I2OSP: integer too large")
+	}
+	x.FillBytes(buf)
+	return nil
+}
+
+// OS2IP - Octet-String-to-Integer primitive converts an octet string to a
+// nonnegative integer.
+// OS2IP is used for decoding ECDSA signature (r, s) from byte strings.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8017#section-4.2
+func OS2IP(x []byte) *big.Int {
+	return new(big.Int).SetBytes(x)
+}
+
+// ecdsaKeySigner is a ECDSA based signer with golang built-in keys.
+type ecdsaKeySigner struct {
+	alg Algorithm
+	key *ecdsa.PrivateKey
+}
+
+// Algorithm returns the signing algorithm associated with the private key.
+func (es *ecdsaKeySigner) Algorithm() Algorithm {
+	return es.alg
+}
+
+// Sign signs digest with the private key, possibly using entropy from rand.
+// The resulting signature should follow RFC 8152 section 8.1.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1
+func (es *ecdsaKeySigner) Sign(rand io.Reader, digest []byte) ([]byte, error) {
+	r, s, err := ecdsa.Sign(rand, es.key, digest)
+	if err != nil {
+		return nil, err
+	}
+	return encodeECDSASignature(es.key.Curve, r, s)
+}
+
+// ecdsaKeySigner is a ECDSA based signer with a generic crypto.Signer.
+type ecdsaCryptoSigner struct {
+	alg    Algorithm
+	key    *ecdsa.PublicKey
+	signer crypto.Signer
+}
+
+// Algorithm returns the signing algorithm associated with the private key.
+func (es *ecdsaCryptoSigner) Algorithm() Algorithm {
+	return es.alg
+}
+
+// Sign signs digest with the private key, possibly using entropy from rand.
+// The resulting signature should follow RFC 8152 section 8.1.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1
+func (es *ecdsaCryptoSigner) Sign(rand io.Reader, digest []byte) ([]byte, error) {
+	sigASN1, err := es.signer.Sign(rand, digest, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// decode ASN.1 decoded signature
+	var sig struct {
+		R, S *big.Int
+	}
+	if _, err := asn1.Unmarshal(sigASN1, &sig); err != nil {
+		return nil, err
+	}
+
+	// encode signature in the COSE form
+	return encodeECDSASignature(es.key.Curve, sig.R, sig.S)
+}
+
+// encodeECDSASignature encodes (r, s) into a signature binary string using the
+// method specified by RFC 8152 section 8.1.
+func encodeECDSASignature(curve elliptic.Curve, r, s *big.Int) ([]byte, error) {
+	n := (curve.Params().BitSize + 7) / 8
+	sig := make([]byte, n*2)
+	if err := I2OSP(r, sig[:n]); err != nil {
+		return nil, err
+	}
+	if err := I2OSP(s, sig[n:]); err != nil {
+		return nil, err
+	}
+	return sig, nil
+}
+
+// decodeECDSASignature decodes (r, s) from a signature binary string using the
+// method specified by RFC 8152 section 8.1.
+func decodeECDSASignature(curve elliptic.Curve, sig []byte) (r, s *big.Int, err error) {
+	n := (curve.Params().BitSize + 7) / 8
+	if len(sig) != n*2 {
+		return nil, nil, fmt.Errorf("invalid signature length: %d", len(sig))
+	}
+	return OS2IP(sig[:n]), OS2IP(sig[n:]), nil
+}
+
+// ecdsaVerifier is a ECDSA based verifier with golang built-in keys.
+type ecdsaVerifier struct {
+	alg Algorithm
+	key *ecdsa.PublicKey
+}
+
+// Algorithm returns the signing algorithm associated with the public key.
+func (ev *ecdsaVerifier) Algorithm() Algorithm {
+	return ev.alg
+}
+
+// Verify verifies 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) Verify(digest []byte, signature []byte) error {
+	// verify digest size
+	if h, ok := ev.alg.hashFunc(); !ok || h.Size() != len(digest) {
+		return ErrVerification
+	}
+
+	// verify signature
+	r, s, err := decodeECDSASignature(ev.key.Curve, signature)
+	if err != nil {
+		return ErrVerification
+	}
+	if verified := ecdsa.Verify(ev.key, digest, r, s); !verified {
+		return ErrVerification
+	}
+	return nil
+}
diff --git a/ecdsa_test.go b/ecdsa_test.go
new file mode 100644
index 0000000..ca75db5
--- /dev/null
+++ b/ecdsa_test.go
@@ -0,0 +1,365 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"math/big"
+	"reflect"
+	"testing"
+
+	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+)
+
+func TestI2OSP(t *testing.T) {
+	tests := []struct {
+		name    string
+		x       *big.Int
+		buf     []byte
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name:    "negative int",
+			x:       big.NewInt(-1),
+			buf:     make([]byte, 2),
+			wantErr: true,
+		},
+		{
+			name:    "integer too large #1",
+			x:       big.NewInt(1),
+			buf:     make([]byte, 0),
+			wantErr: true,
+		},
+		{
+			name:    "integer too large #2",
+			x:       big.NewInt(256),
+			buf:     make([]byte, 0),
+			wantErr: true,
+		},
+		{
+			name:    "integer too large #3",
+			x:       big.NewInt(1 << 24),
+			buf:     make([]byte, 3),
+			wantErr: true,
+		},
+		{
+			name: "zero length string",
+			x:    big.NewInt(0),
+			buf:  make([]byte, 0),
+			want: []byte{},
+		},
+		{
+			name: "zero length string with nil buffer",
+			x:    big.NewInt(0),
+			buf:  nil,
+			want: nil,
+		},
+		{
+			name: "I2OSP(0, 2)",
+			x:    big.NewInt(0),
+			buf:  make([]byte, 2),
+			want: []byte{0x00, 0x00},
+		},
+		{
+			name: "I2OSP(1, 2)",
+			x:    big.NewInt(1),
+			buf:  make([]byte, 2),
+			want: []byte{0x00, 0x01},
+		},
+		{
+			name: "I2OSP(255, 2)",
+			x:    big.NewInt(255),
+			buf:  make([]byte, 2),
+			want: []byte{0x00, 0xff},
+		},
+		{
+			name: "I2OSP(256, 2)",
+			x:    big.NewInt(256),
+			buf:  make([]byte, 2),
+			want: []byte{0x01, 0x00},
+		},
+		{
+			name: "I2OSP(65535, 2)",
+			x:    big.NewInt(65535),
+			buf:  make([]byte, 2),
+			want: []byte{0xff, 0xff},
+		},
+		{
+			name: "I2OSP(1234, 5)",
+			x:    big.NewInt(1234),
+			buf:  make([]byte, 5),
+			want: []byte{0x00, 0x00, 0x00, 0x04, 0xd2},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := I2OSP(tt.x, tt.buf)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("I2OSP() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got := tt.buf; !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("I2OSP() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestOS2IP(t *testing.T) {
+	tests := []struct {
+		name string
+		x    []byte
+		want *big.Int
+	}{
+		{
+			name: "zero length string",
+			x:    []byte{},
+			want: big.NewInt(0),
+		},
+		{
+			name: "OS2IP(I2OSP(0, 2))",
+			x:    []byte{0x00, 0x00},
+			want: big.NewInt(0),
+		},
+		{
+			name: "OS2IP(I2OSP(1, 2))",
+			x:    []byte{0x00, 0x01},
+			want: big.NewInt(1),
+		},
+		{
+			name: "OS2IP(I2OSP(255, 2))",
+			x:    []byte{0x00, 0xff},
+			want: big.NewInt(255),
+		},
+		{
+			name: "OS2IP(I2OSP(256, 2))",
+			x:    []byte{0x01, 0x00},
+			want: big.NewInt(256),
+		},
+		{
+			name: "OS2IP(I2OSP(65535, 2))",
+			x:    []byte{0xff, 0xff},
+			want: big.NewInt(65535),
+		},
+		{
+			name: "OS2IP(I2OSP(1234, 5))",
+			x:    []byte{0x00, 0x00, 0x00, 0x04, 0xd2},
+			want: big.NewInt(1234),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := OS2IP(tt.x); tt.want.Cmp(got) != 0 {
+				t.Errorf("OS2IP() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func generateTestECDSAKey(t *testing.T) *ecdsa.PrivateKey {
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatalf("ecdsa.GenerateKey() error = %v", err)
+	}
+	return key
+}
+
+func Test_ecdsaKeySigner(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// set up signer
+	signer, err := NewSigner(alg, key)
+	if err != nil {
+		t.Fatalf("NewSigner() error = %v", err)
+	}
+	if _, ok := signer.(*ecdsaKeySigner); !ok {
+		t.Fatalf("NewSigner() type = %v, want *ecdsaKeySigner", reflect.TypeOf(signer))
+	}
+	if got := signer.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// sign / verify round trip
+	// see also conformance_test.go for strict tests.
+	digest, err := alg.computeHash([]byte("hello world"))
+	if err != nil {
+		t.Fatalf("Algorithm.computeHash() error = %v", err)
+	}
+	sig, err := signer.Sign(rand.Reader, digest)
+	if err != nil {
+		t.Fatalf("Sign() error = %v", err)
+	}
+
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("Verifier.Verify() error = %v", err)
+	}
+}
+
+func Test_ecdsaCryptoSigner(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// set up signer
+	wrappedKey := struct {
+		crypto.Signer
+	}{
+		Signer: key,
+	}
+	signer, err := NewSigner(alg, wrappedKey)
+	if err != nil {
+		t.Fatalf("NewSigner() error = %v", err)
+	}
+	if _, ok := signer.(*ecdsaCryptoSigner); !ok {
+		t.Fatalf("NewSigner() type = %v, want *ecdsaCryptoSigner", reflect.TypeOf(signer))
+	}
+	if got := signer.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// sign / verify round trip
+	// see also conformance_test.go for strict tests.
+	digest, err := alg.computeHash([]byte("hello world"))
+	if err != nil {
+		t.Fatalf("Algorithm.computeHash() error = %v", err)
+	}
+	sig, err := signer.Sign(rand.Reader, digest)
+	if err != nil {
+		t.Fatalf("Sign() error = %v", err)
+	}
+
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("Verifier.Verify() error = %v", err)
+	}
+}
+
+func Test_ecdsaVerifier_Verify_Success(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if _, ok := verifier.(*ecdsaVerifier); !ok {
+		t.Fatalf("NewVerifier() type = %v, want *ecdsaVerifier", reflect.TypeOf(verifier))
+	}
+	if got := verifier.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// verify round trip
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("ecdsaVerifier.Verify() error = %v", err)
+	}
+}
+
+func Test_ecdsaVerifier_Verify_AlgorithmMismatch(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier with a different algorithm
+	verifier := &ecdsaVerifier{
+		alg: AlgorithmES512,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on algorithm mismatch
+	if err := verifier.Verify(digest, sig); err != ErrVerification {
+		t.Fatalf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+	}
+}
+
+func Test_ecdsaVerifier_Verify_KeyMismatch(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier with a different key / new key
+	key = generateTestECDSAKey(t)
+	verifier := &ecdsaVerifier{
+		alg: alg,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on key mismatch
+	if err := verifier.Verify(digest, sig); err != ErrVerification {
+		t.Fatalf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+	}
+}
+
+func Test_ecdsaVerifier_Verify_InvalidSignature(t *testing.T) {
+	// generate key
+	alg := AlgorithmES256
+	key := generateTestECDSAKey(t)
+
+	// generate a valid signature with a tampered one
+	digest, sig := signTestData(t, alg, key)
+	tamperedSig := make([]byte, len(sig))
+	copy(tamperedSig, sig)
+	tamperedSig[0]++
+
+	// set up verifier with a different algorithm
+	verifier := &ecdsaVerifier{
+		alg: alg,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on invalid signature
+	tests := []struct {
+		name      string
+		signature []byte
+	}{
+		{
+			name:      "nil signature",
+			signature: nil,
+		},
+		{
+			name:      "empty signature",
+			signature: []byte{},
+		},
+		{
+			name:      "incomplete signature",
+			signature: sig[:len(sig)-2],
+		},
+		{
+			name:      "tampered signature",
+			signature: tamperedSig,
+		},
+		{
+			name:      "too many signature bytes",
+			signature: append(sig, 0),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := verifier.Verify(digest, tt.signature); err != ErrVerification {
+				t.Errorf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+			}
+		})
+	}
+}
diff --git a/ed25519.go b/ed25519.go
new file mode 100644
index 0000000..30929f7
--- /dev/null
+++ b/ed25519.go
@@ -0,0 +1,48 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ed25519"
+	"io"
+)
+
+// ed25519Signer is a Pure EdDsA based signer with a generic crypto.Signer.
+type ed25519Signer struct {
+	key crypto.Signer
+}
+
+// Algorithm returns the signing algorithm associated with the private key.
+func (es *ed25519Signer) Algorithm() Algorithm {
+	return AlgorithmEd25519
+}
+
+// Sign signs digest with the private key, possibly using entropy from rand.
+// The resulting signature should follow RFC 8152 section 8.2.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.2
+func (es *ed25519Signer) Sign(rand io.Reader, digest []byte) ([]byte, error) {
+	// crypto.Hash(0) must be passed as an option.
+	// Reference: https://pkg.go.dev/crypto/ed25519#PrivateKey.Sign
+	return es.key.Sign(rand, digest, crypto.Hash(0))
+}
+
+// ed25519Verifier is a Pure EdDsA based verifier with golang built-in keys.
+type ed25519Verifier struct {
+	key ed25519.PublicKey
+}
+
+// Algorithm returns the signing algorithm associated with the public key.
+func (ev *ed25519Verifier) Algorithm() Algorithm {
+	return AlgorithmEd25519
+}
+
+// Verify verifies digest with the public key, returning nil for success.
+// Otherwise, it returns ErrVerification.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.2
+func (ev *ed25519Verifier) Verify(digest []byte, signature []byte) error {
+	if verified := ed25519.Verify(ev.key, digest, signature); !verified {
+		return ErrVerification
+	}
+	return nil
+}
diff --git a/ed25519_test.go b/ed25519_test.go
new file mode 100644
index 0000000..3b61abe
--- /dev/null
+++ b/ed25519_test.go
@@ -0,0 +1,150 @@
+package cose
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"reflect"
+	"testing"
+)
+
+func generateTestEd25519Key(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
+	vk, sk, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		t.Fatalf("ed25519.GenerateKey() error = %v", err)
+	}
+	return vk, sk
+}
+
+func Test_ed25519Signer(t *testing.T) {
+	// generate key
+	alg := AlgorithmEd25519
+	_, key := generateTestEd25519Key(t)
+
+	// set up signer
+	signer, err := NewSigner(alg, key)
+	if err != nil {
+		t.Fatalf("NewSigner() error = %v", err)
+	}
+	if _, ok := signer.(*ed25519Signer); !ok {
+		t.Fatalf("NewSigner() type = %v, want *ed25519Signer", reflect.TypeOf(signer))
+	}
+	if got := signer.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// sign / verify round trip
+	// see also conformance_test.go for strict tests.
+	digest, err := alg.computeHash([]byte("hello world"))
+	if err != nil {
+		t.Fatalf("Algorithm.computeHash() error = %v", err)
+	}
+	sig, err := signer.Sign(rand.Reader, digest)
+	if err != nil {
+		t.Fatalf("Sign() error = %v", err)
+	}
+
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("Verifier.Verify() error = %v", err)
+	}
+}
+
+func Test_ed25519Verifier_Verify_Success(t *testing.T) {
+	// generate key
+	alg := AlgorithmEd25519
+	_, key := generateTestEd25519Key(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if _, ok := verifier.(*ed25519Verifier); !ok {
+		t.Fatalf("NewVerifier() type = %v, want *ed25519Verifier", reflect.TypeOf(verifier))
+	}
+	if got := verifier.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// verify round trip
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("ed25519Verifier.Verify() error = %v", err)
+	}
+}
+
+func Test_ed25519Verifier_Verify_KeyMismatch(t *testing.T) {
+	// generate key
+	alg := AlgorithmEd25519
+	_, key := generateTestEd25519Key(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier with a different key / new key
+	vk, _ := generateTestEd25519Key(t)
+	verifier := &ed25519Verifier{
+		key: vk,
+	}
+
+	// verification should fail on key mismatch
+	if err := verifier.Verify(digest, sig); err != ErrVerification {
+		t.Fatalf("ed25519Verifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+	}
+}
+
+func Test_ed25519Verifier_Verify_InvalidSignature(t *testing.T) {
+	// generate key
+	alg := AlgorithmEd25519
+	vk, sk := generateTestEd25519Key(t)
+
+	// generate a valid signature with a tampered one
+	digest, sig := signTestData(t, alg, sk)
+	tamperedSig := make([]byte, len(sig))
+	copy(tamperedSig, sig)
+	tamperedSig[0]++
+
+	// set up verifier with a different algorithm
+	verifier := &ed25519Verifier{
+		key: vk,
+	}
+
+	// verification should fail on invalid signature
+	tests := []struct {
+		name      string
+		signature []byte
+	}{
+		{
+			name:      "nil signature",
+			signature: nil,
+		},
+		{
+			name:      "empty signature",
+			signature: []byte{},
+		},
+		{
+			name:      "incomplete signature",
+			signature: sig[:len(sig)-2],
+		},
+		{
+			name:      "tampered signature",
+			signature: tamperedSig,
+		},
+		{
+			name:      "too many signature bytes",
+			signature: append(sig, 0),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := verifier.Verify(digest, tt.signature); err != ErrVerification {
+				t.Errorf("ed25519Verifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+			}
+		})
+	}
+}
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..ac9ba27
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,18 @@
+package cose
+
+import "errors"
+
+// Common errors
+var (
+	ErrAlgorithmMismatch     = errors.New("algorithm mismatch")
+	ErrAlgorithmNotFound     = errors.New("algorithm not found")
+	ErrAlgorithmNotSupported = errors.New("algorithm not supported")
+	ErrAlgorithmRegistered   = errors.New("algorithm registered")
+	ErrEmptySignature        = errors.New("empty signature")
+	ErrInvalidAlgorithm      = errors.New("invalid algorithm")
+	ErrMissingPayload        = errors.New("missing payload")
+	ErrNoSignatures          = errors.New("no signatures attached")
+	ErrUnavailableHashFunc   = errors.New("hash function is not available")
+	ErrUnknownAlgorithm      = errors.New("unknown algorithm")
+	ErrVerification          = errors.New("verification error")
+)
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..b63883c
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,233 @@
+package cose_test
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	_ "crypto/sha512"
+	"fmt"
+
+	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+
+	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+)
+
+// This example demonstrates signing and verifying COSE_Sign signatures.
+//
+// The COSE Sign API is EXPERIMENTAL and may be changed or removed in a later
+// release.
+func ExampleSignMessage() {
+	// create a signature holder
+	sigHolder := cose.NewSignature()
+	sigHolder.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
+	sigHolder.Headers.Unprotected[cose.HeaderLabelKeyID] = 1
+
+	// create message to be signed
+	msgToSign := cose.NewSignMessage()
+	msgToSign.Payload = []byte("hello world")
+	msgToSign.Signatures = append(msgToSign.Signatures, sigHolder)
+
+	// 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()
+	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.SignMessage
+	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
+}
+
+// This example demonstrates signing and verifying COSE_Sign1 signatures.
+func ExampleSign1Message() {
+	// create message to be signed
+	msgToSign := cose.NewSign1Message()
+	msgToSign.Payload = []byte("hello world")
+	msgToSign.Headers.Protected.SetAlgorithm(cose.AlgorithmES512)
+	msgToSign.Headers.Unprotected[cose.HeaderLabelKeyID] = 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()
+	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
+}
+
+// This example demonstrates signing COSE_Sign1 signatures using Sign1().
+func ExampleSign1() {
+	// 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
+	protected := cose.ProtectedHeader{}
+	protected.SetAlgorithm(cose.AlgorithmES512)
+	msg, err := cose.Sign1(rand.Reader, signer, protected, []byte("hello world"), nil)
+	if err != nil {
+		panic(err)
+	}
+
+	// update unprotected headers
+	msg.Headers.Unprotected[cose.HeaderLabelKeyID] = 1
+
+	// encode message
+	sig, err := msg.MarshalCBOR()
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("message signed")
+	_ = sig // futher process on sig
+	// Output:
+	// message signed
+}
+
+// This example demonstrates verifying COSE_Sign1 signatures using Verify1().
+func ExampleVerify1() {
+	// get a signed message and a trusted public key
+	sig, publicKey := getSignatureAndPublicKey()
+
+	// create a verifier from a trusted public key
+	verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey)
+	if err != nil {
+		panic(err)
+	}
+
+	// verify message
+	var msg cose.Sign1Message
+	err = msg.UnmarshalCBOR(sig)
+	if err != nil {
+		panic(err)
+	}
+	err = cose.Verify1(&msg, nil, verifier)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("message verified")
+
+	// tamper the message and verification should fail
+	msg.Payload = []byte("foobar")
+	err = cose.Verify1(&msg, nil, verifier)
+	if err != cose.ErrVerification {
+		panic(err)
+	}
+	fmt.Println("verification error as expected")
+	// Output:
+	// message verified
+	// verification error as expected
+}
+
+// getSignatureAndPublicKey is a helping function for ExampleVerify1().
+func getSignatureAndPublicKey() ([]byte, crypto.PublicKey) {
+	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)
+	}
+	msgToSign, err := cose.Sign1(rand.Reader, signer, nil, []byte("hello world"), nil)
+	if err != nil {
+		panic(err)
+	}
+	sig, err := msgToSign.MarshalCBOR()
+	if err != nil {
+		panic(err)
+	}
+	return sig, privateKey.Public()
+}
diff --git a/fuzz_test.go b/fuzz_test.go
new file mode 100644
index 0000000..71fd223
--- /dev/null
+++ b/fuzz_test.go
@@ -0,0 +1,174 @@
+//go:build go1.18
+// +build go1.18
+
+package cose_test
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	_ "google3/go/tools/nogo/allowlist/crypto/elliptic"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+	"google3/third_party/golang/github_com/veraison/go_cose/v/v0/cose"
+)
+
+func FuzzSign1Message_UnmarshalCBOR(f *testing.F) {
+	testdata, err := os.ReadDir("testdata")
+	if err != nil {
+		f.Fatalf("failed to read testdata directory: %s", err)
+	}
+	for _, de := range testdata {
+		if de.IsDir() || !strings.HasPrefix(de.Name(), "sign1-") || !strings.HasSuffix(de.Name(), ".json") {
+			continue
+		}
+		b, err := os.ReadFile(filepath.Join("testdata", de.Name()))
+		if err != nil {
+			f.Fatalf("failed to read testdata: %s", err)
+		}
+		type testCase struct {
+			Sign1   *Sign1   `json:"sign1::sign"`
+			Verify1 *Verify1 `json:"sign1::verify"`
+		}
+		var tc testCase
+		err = json.Unmarshal(b, &tc)
+		if err != nil {
+			f.Fatal(err)
+		}
+		if tc.Sign1 != nil {
+			f.Add(mustHexToBytes(tc.Sign1.Output.CBORHex))
+		} else if tc.Verify1 != nil {
+			f.Add(mustHexToBytes(tc.Verify1.TaggedCOSESign1.CBORHex))
+		}
+	}
+	enc, _ := cbor.CanonicalEncOptions().EncMode()
+	dec, _ := cbor.DecOptions{IntDec: cbor.IntDecConvertSigned}.DecMode()
+	isCanonical := func(b []byte) bool {
+		var tmp interface{}
+		err := dec.Unmarshal(b, &tmp)
+		if err != nil {
+			return false
+		}
+		b1, err := enc.Marshal(tmp)
+		return bytes.Equal(b, b1)
+	}
+	f.Fuzz(func(t *testing.T, b []byte) {
+		var msg cose.Sign1Message
+		if err := msg.UnmarshalCBOR(b); err != nil {
+			return
+		}
+		got, err := msg.MarshalCBOR()
+		if err != nil {
+			t.Fatalf("failed to marshal valid message: %s", err)
+		}
+		if !isCanonical(b) {
+			return
+		}
+		if len(b) > len(got) {
+			b = b[:len(got)]
+		}
+		if !bytes.Equal(b, got) {
+			t.Fatalf("roundtripped message has changed, got: %v, want: %v", got, b)
+		}
+	})
+}
+
+func FuzzSign1(f *testing.F) {
+	testdata, err := os.ReadDir("testdata")
+	if err != nil {
+		f.Fatalf("failed to read testdata directory: %s", err)
+	}
+	for _, de := range testdata {
+		if de.IsDir() || !strings.HasPrefix(de.Name(), "sign1-sign") || !strings.HasSuffix(de.Name(), ".json") {
+			continue
+		}
+		b, err := os.ReadFile(filepath.Join("testdata", de.Name()))
+		if err != nil {
+			f.Fatalf("failed to read testdata: %s", err)
+		}
+		type testCase struct {
+			Sign1 *Sign1 `json:"sign1::sign"`
+		}
+		var tc testCase
+		err = json.Unmarshal(b, &tc)
+		if err != nil {
+			f.Fatal(err)
+		}
+		if tc.Sign1 != nil {
+			hdr, _ := encMode.Marshal(mustHexToBytes(tc.Sign1.ProtectedHeaders.CBORHex))
+			f.Add(hdr, mustHexToBytes(tc.Sign1.Payload), mustHexToBytes(tc.Sign1.External))
+		}
+	}
+	f.Fuzz(func(t *testing.T, hdr_data, payload, external []byte) {
+		hdr := make(cose.ProtectedHeader)
+		err := hdr.UnmarshalCBOR(hdr_data)
+		if err != nil {
+			return
+		}
+		alg, err := hdr.Algorithm()
+		if err != nil {
+			return
+		}
+		pkey, err := newSignerWithEphemeralKey(alg)
+		if err != nil {
+			return
+		}
+		signer, err := cose.NewSigner(alg, pkey)
+		if err != nil {
+			t.Fatal(err)
+		}
+		verifier, err := cose.NewVerifier(alg, pkey.Public())
+		if err != nil {
+			t.Fatal(err)
+		}
+		msg, err := cose.Sign1(rand.Reader, signer, hdr, payload, external)
+		if err != nil {
+			t.Fatal(err)
+		}
+		err = cose.Verify1(msg, external, verifier)
+		if err != nil {
+			t.Fatal(err)
+		}
+		err = cose.Verify1(msg, append(external, []byte{0}...), verifier)
+		if err == nil {
+			t.Fatal("verification error expected")
+		}
+	})
+}
+
+func newSignerWithEphemeralKey(alg cose.Algorithm) (crypto.Signer, error) {
+	var key crypto.Signer
+	var err error
+	switch alg {
+	case cose.AlgorithmPS256:
+		key, err = rsa.GenerateKey(rand.Reader, 2048)
+	case cose.AlgorithmPS384:
+		key, err = rsa.GenerateKey(rand.Reader, 3072)
+	case cose.AlgorithmPS512:
+		key, err = rsa.GenerateKey(rand.Reader, 4096)
+	case cose.AlgorithmES256:
+		key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	case cose.AlgorithmES384:
+		key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	case cose.AlgorithmES512:
+		key, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	case cose.AlgorithmEd25519:
+		_, key, err = ed25519.GenerateKey(rand.Reader)
+	default:
+		return nil, cose.ErrAlgorithmNotSupported
+	}
+	if err != nil {
+		return nil, err
+	}
+	return key, nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..609875d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,7 @@
+module github.com/veraison/go-cose
+
+go 1.18
+
+require github.com/fxamacker/cbor/v2 v2.4.0
+
+require github.com/x448/float16 v0.8.4 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f424051
--- /dev/null
+++ b/go.sum
@@ -0,0 +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/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
new file mode 100644
index 0000000..065f72a
--- /dev/null
+++ b/headers.go
@@ -0,0 +1,425 @@
+package cose
+
+import (
+	"errors"
+	"fmt"
+	"math/big"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+)
+
+// COSE Header labels registered in the IANA "COSE Header Parameters" registry.
+//
+// Reference: https://www.iana.org/assignments/cose/cose.xhtml#header-parameters
+const (
+	HeaderLabelAlgorithm         int64 = 1
+	HeaderLabelCritical          int64 = 2
+	HeaderLabelContentType       int64 = 3
+	HeaderLabelKeyID             int64 = 4
+	HeaderLabelCounterSignature  int64 = 7
+	HeaderLabelCounterSignature0 int64 = 9
+	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{}
+
+// 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
+// zero-length map (encoded as h'a0').
+func (h ProtectedHeader) MarshalCBOR() ([]byte, error) {
+	var encoded []byte
+	if len(h) == 0 {
+		encoded = []byte{}
+	} else {
+		err := validateHeaderLabel(h)
+		if err != nil {
+			return nil, err
+		}
+		if err = h.ensureCritical(); err != nil {
+			return nil, err
+		}
+		encoded, err = encMode.Marshal(map[interface{}]interface{}(h))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return encMode.Marshal(encoded)
+}
+
+// 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
+func (h *ProtectedHeader) UnmarshalCBOR(data []byte) error {
+	if h == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil ProtectedHeader pointer")
+	}
+	var encoded byteString
+	if err := encoded.UnmarshalCBOR(data); err != nil {
+		return err
+	}
+	if encoded == nil {
+		return errors.New("cbor: nil protected header")
+	}
+	if len(encoded) == 0 {
+		*h = make(ProtectedHeader)
+	} else {
+		if encoded[0]>>5 != 5 { // major type 5: map
+			return errors.New("cbor: protected header: require map type")
+		}
+		if err := validateHeaderLabelCBOR(encoded); err != nil {
+			return err
+		}
+		var header map[interface{}]interface{}
+		if err := decMode.Unmarshal(encoded, &header); err != nil {
+			return err
+		}
+		candidate := ProtectedHeader(header)
+		if err := candidate.ensureCritical(); err != nil {
+			return err
+		}
+
+		// cast to type Algorithm if `alg` presents
+		if alg, err := candidate.Algorithm(); err == nil {
+			candidate.SetAlgorithm(alg)
+		}
+
+		*h = candidate
+	}
+	return nil
+}
+
+// SetAlgorithm sets the algorithm value to the algorithm header.
+func (h ProtectedHeader) SetAlgorithm(alg Algorithm) {
+	h[HeaderLabelAlgorithm] = alg
+}
+
+// Algorithm gets the algorithm value from the algorithm header.
+func (h ProtectedHeader) Algorithm() (Algorithm, error) {
+	value, ok := h[HeaderLabelAlgorithm]
+	if !ok {
+		return 0, ErrAlgorithmNotFound
+	}
+	switch alg := value.(type) {
+	case Algorithm:
+		return alg, nil
+	case int:
+		return Algorithm(alg), nil
+	case int8:
+		return Algorithm(alg), nil
+	case int16:
+		return Algorithm(alg), nil
+	case int32:
+		return Algorithm(alg), nil
+	case int64:
+		return Algorithm(alg), nil
+	default:
+		return 0, ErrInvalidAlgorithm
+	}
+}
+
+// Critical indicates which protected header labels an application that is
+// processing a message is required to understand.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-3.1
+func (h ProtectedHeader) Critical() ([]interface{}, error) {
+	value, ok := h[HeaderLabelCritical]
+	if !ok {
+		return nil, nil
+	}
+	criticalLabels, ok := value.([]interface{})
+	if !ok {
+		return nil, errors.New("invalid crit header")
+	}
+	// if present, the array MUST have at least one value in it.
+	if len(criticalLabels) == 0 {
+		return nil, errors.New("empty crit header")
+	}
+	return criticalLabels, nil
+}
+
+// ensureCritical ensures all critical headers present in the protected bucket.
+func (h ProtectedHeader) ensureCritical() error {
+	labels, err := h.Critical()
+	if err != nil {
+		return err
+	}
+	for _, label := range labels {
+		if _, ok := h[label]; !ok {
+			return fmt.Errorf("missing critical header: %v", label)
+		}
+	}
+	return nil
+}
+
+// UnprotectedHeader contains parameters that are not cryptographically
+// protected.
+type UnprotectedHeader map[interface{}]interface{}
+
+// 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').
+func (h UnprotectedHeader) MarshalCBOR() ([]byte, error) {
+	if len(h) == 0 {
+		return []byte{0xa0}, nil
+	}
+	if err := validateHeaderLabel(h); err != nil {
+		return nil, err
+	}
+	return encMode.Marshal(map[interface{}]interface{}(h))
+}
+
+// UnmarshalCBOR decodes a CBOR map object into UnprotectedHeader.
+//
+// UnprotectedHeader is a header_map.
+func (h *UnprotectedHeader) UnmarshalCBOR(data []byte) error {
+	if h == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil UnprotectedHeader pointer")
+	}
+	if data == nil {
+		return errors.New("cbor: nil unprotected header")
+	}
+	if len(data) == 0 {
+		return errors.New("cbor: unprotected header: missing type")
+	}
+	if data[0]>>5 != 5 { // major type 5: map
+		return errors.New("cbor: unprotected header: require map type")
+	}
+	if err := validateHeaderLabelCBOR(data); err != nil {
+		return err
+	}
+	var header map[interface{}]interface{}
+	if err := decMode.Unmarshal(data, &header); err != nil {
+		return err
+	}
+	*h = header
+	return 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
+// hints for the processing of the layer."
+//
+// It is represented by CDDL fragments:
+//
+//	Headers = (
+//	    protected : empty_or_serialized_map,
+//	    unprotected : header_map
+//	)
+//
+//	header_map = {
+//	    Generic_Headers,
+//	    * label => values
+//	}
+//
+//	label  = int / tstr
+//	values = any
+//
+//	empty_or_serialized_map = bstr .cbor header_map / bstr .size 0
+//
+// # See Also
+//
+// https://tools.ietf.org/html/rfc8152#section-3
+type Headers struct {
+	// RawProtected contains the raw CBOR encoded data for the protected header.
+	// It is populated when decoding.
+	// Applications can use this field for customized encoding / decoding of
+	// the protected header in case the default decoder provided by this library
+	// is not preferred.
+	RawProtected cbor.RawMessage
+
+	// Protected contains parameters that are to be cryptographically protected.
+	// When encoding or signing, the protected header is encoded using the
+	// default CBOR encoder if RawProtected is set to nil. Otherwise,
+	// RawProtected will be used with Protected ignored.
+	Protected ProtectedHeader
+
+	// RawUnprotected contains the raw CBOR encoded data for the unprotected
+	// header. It is populated when decoding.
+	// Applications can use this field for customized encoding / decoding of
+	// the unprotected header in case the default decoder provided by this
+	// library is not preferred.
+	RawUnprotected cbor.RawMessage
+
+	// Unprotected contains parameters that are not cryptographically protected.
+	// When encoding, the unprotected header is encoded using the default CBOR
+	// encoder if RawUnprotected is set to nil. Otherwise, RawUnprotected will
+	// be used with Unprotected ignored.
+	Unprotected UnprotectedHeader
+}
+
+// MarshalProtected encodes the protected header.
+// RawProtected is returned if it is not set to nil.
+func (h *Headers) MarshalProtected() ([]byte, error) {
+	if len(h.RawProtected) > 0 {
+		return h.RawProtected, nil
+	}
+	return encMode.Marshal(h.Protected)
+}
+
+// MarshalUnprotected encodes the unprotected header.
+// RawUnprotected is returned if it is not set to nil.
+func (h *Headers) MarshalUnprotected() ([]byte, error) {
+	if len(h.RawUnprotected) > 0 {
+		return h.RawUnprotected, nil
+	}
+	return encMode.Marshal(h.Unprotected)
+}
+
+// UnmarshalFromRaw decodes Protected from RawProtected and Unprotected from
+// RawUnprotected.
+func (h *Headers) UnmarshalFromRaw() error {
+	if err := decMode.Unmarshal(h.RawProtected, &h.Protected); err != nil {
+		return fmt.Errorf("cbor: invalid protected header: %w", err)
+	}
+	if err := decMode.Unmarshal(h.RawUnprotected, &h.Unprotected); err != nil {
+		return fmt.Errorf("cbor: invalid unprotected header: %w", err)
+	}
+	return nil
+}
+
+// ensureSigningAlgorithm ensures the presence of the `alg` header if there is
+// no externally supplied data for signing.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func (h *Headers) ensureSigningAlgorithm(alg Algorithm, external []byte) error {
+	candidate, err := h.Protected.Algorithm()
+	switch err {
+	case nil:
+		if candidate != alg {
+			return fmt.Errorf("%w: signer %v: header %v", ErrAlgorithmMismatch, alg, candidate)
+		}
+		return nil
+	case ErrAlgorithmNotFound:
+		if len(external) > 0 {
+			return nil
+		}
+		if h.RawProtected != nil {
+			return ErrAlgorithmNotFound
+		}
+		if h.Protected == nil {
+			h.Protected = make(ProtectedHeader)
+		}
+		h.Protected.SetAlgorithm(alg)
+		return nil
+	}
+	return err
+}
+
+// ensureVerificationAlgorithm ensures the presence of the `alg` header if there
+// is no externally supplied data for verification.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func (h *Headers) ensureVerificationAlgorithm(alg Algorithm, external []byte) error {
+	candidate, err := h.Protected.Algorithm()
+	switch err {
+	case nil:
+		if candidate != alg {
+			return fmt.Errorf("%w: verifier %v: header %v", ErrAlgorithmMismatch, alg, candidate)
+		}
+		return nil
+	case ErrAlgorithmNotFound:
+		if len(external) > 0 {
+			return nil
+		}
+	}
+	return err
+}
+
+// validateHeaderLabel validates if all header labels are integers or strings.
+//
+//	label = int / tstr
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
+func validateHeaderLabel(h map[interface{}]interface{}) error {
+	existing := make(map[interface{}]struct{})
+	for label := range h {
+		switch v := label.(type) {
+		case int:
+			label = int64(v)
+		case int8:
+			label = int64(v)
+		case int16:
+			label = int64(v)
+		case int32:
+			label = int64(v)
+		case int64:
+			label = int64(v)
+		case uint:
+			label = int64(v)
+		case uint8:
+			label = int64(v)
+		case uint16:
+			label = int64(v)
+		case uint32:
+			label = int64(v)
+		case uint64:
+			label = int64(v)
+		case string:
+			// no conversion
+		default:
+			return errors.New("cbor: header label: require int / tstr type")
+		}
+		if _, ok := existing[label]; ok {
+			return fmt.Errorf("cbor: header label: duplicated label: %v", label)
+		} else {
+			existing[label] = struct{}{}
+		}
+	}
+	return nil
+}
+
+// headerLabelValidator is used to validate the header label of a COSE header.
+type headerLabelValidator struct {
+	value interface{}
+}
+
+// String prints the value without brackets `{}`. Useful in error printing.
+func (hlv headerLabelValidator) String() string {
+	return fmt.Sprint(hlv.value)
+}
+
+// UnmarshalCBOR decodes the label value of a COSE header, and returns error if
+// label is not a int (major type 0, 1) or string (major type 3).
+func (hlv *headerLabelValidator) UnmarshalCBOR(data []byte) error {
+	if len(data) == 0 {
+		return errors.New("cbor: header label: missing type")
+	}
+	switch data[0] >> 5 {
+	case 0, 1, 3:
+		err := decMode.Unmarshal(data, &hlv.value)
+		if err != nil {
+			return err
+		}
+		if _, ok := hlv.value.(big.Int); ok {
+			return errors.New("cbor: header label: int key must not be higher than 1<<63 - 1")
+		}
+		return nil
+	}
+	return errors.New("cbor: header label: require int / tstr type")
+}
+
+// discardedCBORMessage is used to read CBOR message and discard it.
+type discardedCBORMessage struct{}
+
+// UnmarshalCBOR discards the read CBOR object.
+func (discardedCBORMessage) UnmarshalCBOR(data []byte) error {
+	return nil
+}
+
+// validateHeaderLabelCBOR validates if all header labels are integers or
+// strings of a CBOR map object.
+//
+//	label = int / tstr
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4
+func validateHeaderLabelCBOR(data []byte) error {
+	var header map[headerLabelValidator]discardedCBORMessage
+	return decMode.Unmarshal(data, &header)
+}
diff --git a/headers_test.go b/headers_test.go
new file mode 100644
index 0000000..4c4aadb
--- /dev/null
+++ b/headers_test.go
@@ -0,0 +1,846 @@
+package cose
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestProtectedHeader_MarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       ProtectedHeader
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "valid header",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: AlgorithmES256,
+				HeaderLabelCritical: []interface{}{
+					HeaderLabelContentType,
+					"foo",
+				},
+				HeaderLabelContentType: "text/plain",
+				"foo":                  "bar",
+			},
+			want: []byte{
+				0x58, 0x1e, // bstr
+				0xa4,       // map
+				0x01, 0x26, // alg
+				0x02, 0x82, 0x03, 0x63, 0x66, 0x6f, 0x6f, // crit
+				0x03, 0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e, // cty
+				0x63, 0x66, 0x6f, 0x6f, 0x63, 0x62, 0x61, 0x72, // foo: bar
+			},
+		},
+		{
+			name: "nil header",
+			h:    nil,
+			want: []byte{0x40},
+		},
+		{
+			name: "empty header",
+			h:    ProtectedHeader{},
+			want: []byte{0x40},
+		},
+		{
+			name: "various types of integer label",
+			h: ProtectedHeader{
+				uint(1):   0,
+				uint8(2):  0,
+				uint16(3): 0,
+				uint32(4): 0,
+				uint64(5): 0,
+				int(-1):   0,
+				int8(-2):  0,
+				int16(-3): 0,
+				int32(-4): 0,
+				int64(-5): 0,
+			},
+			want: []byte{
+				0x55, // bstr
+				0xaa, // map
+				0x01, 0x00,
+				0x02, 0x00,
+				0x03, 0x00,
+				0x04, 0x00,
+				0x05, 0x00,
+				0x20, 0x00,
+				0x21, 0x00,
+				0x22, 0x00,
+				0x23, 0x00,
+				0x24, 0x00,
+			},
+		},
+		{
+			name: "invalid header label: struct type",
+			h: ProtectedHeader{
+				struct {
+					value int
+				}{}: 42,
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty critical",
+			h: ProtectedHeader{
+				HeaderLabelCritical: []interface{}{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid critical",
+			h: ProtectedHeader{
+				HeaderLabelCritical: 42,
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing header marked as critical",
+			h: ProtectedHeader{
+				HeaderLabelCritical: []interface{}{
+					HeaderLabelContentType,
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicated key",
+			h: ProtectedHeader{
+				int8(42):  "foo",
+				int64(42): "bar",
+			},
+			wantErr: true,
+		},
+		{
+			name: "un-marshalable content",
+			h: ProtectedHeader{
+				"foo": make(chan bool),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.MarshalCBOR()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProtectedHeader.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProtectedHeader.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestProtectedHeader_UnmarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		data    []byte
+		want    ProtectedHeader
+		wantErr bool
+	}{
+		{
+			name: "valid header",
+			data: []byte{
+				0x58, 0x1e, // bstr
+				0xa4,       // map
+				0x01, 0x26, // alg
+				0x02, 0x82, 0x03, 0x63, 0x66, 0x6f, 0x6f, // crit
+				0x03, 0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e, // cty
+				0x63, 0x66, 0x6f, 0x6f, 0x63, 0x62, 0x61, 0x72, // foo: bar
+			},
+			want: ProtectedHeader{
+				HeaderLabelAlgorithm: AlgorithmES256,
+				HeaderLabelCritical: []interface{}{
+					HeaderLabelContentType,
+					"foo",
+				},
+				HeaderLabelContentType: "text/plain",
+				"foo":                  "bar",
+			},
+		},
+		{
+			name: "empty header",
+			data: []byte{0x40},
+			want: ProtectedHeader{},
+		},
+		{
+			name: "empty map",
+			data: []byte{0x41, 0xa0},
+			want: ProtectedHeader{},
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name:    "bad CBOR data",
+			data:    []byte{0x00, 0x01, 0x02, 0x04},
+			wantErr: true,
+		},
+		{
+			name:    "nil bstr",
+			data:    []byte{0xf6},
+			wantErr: true,
+		},
+		{
+			name:    "non-map header",
+			data:    []byte{0x41, 0x00},
+			wantErr: true,
+		},
+		{
+			name: "invalid header label type: bstr type",
+			data: []byte{
+				0x43, 0xa1, 0x40, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid header label type: major type 7: simple value", // issue #38
+			data: []byte{
+				0x43, 0xa1, 0xf3, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty critical",
+			data: []byte{
+				0x43, 0xa1, 0x02, 0x80,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid critical",
+			data: []byte{
+				0x43, 0xa1, 0x02, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing header marked as critical",
+			data: []byte{
+				0x44, 0xa1, 0x02, 0x81, 0x03,
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicated key",
+			data: []byte{
+				0x45, 0xa2, 0x01, 0x00, 0x01, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "incomplete CBOR data",
+			data: []byte{
+				0x45,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid map value",
+			data: []byte{
+				0x46, 0xa1, 0x00, 0xa1, 0x00, 0x4f, 0x01,
+			},
+			wantErr: true,
+		},
+		{
+			name: "int map key too large",
+			data: []byte{
+				0x4b, 0xa1, 0x3b, 0x83, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+			},
+			wantErr: true,
+		},
+		{
+			name: "header as a byte array",
+			data: []byte{
+				0x80,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got ProtectedHeader
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("ProtectedHeader.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProtectedHeader.UnmarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestProtectedHeader_Algorithm(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       ProtectedHeader
+		want    Algorithm
+		wantErr error
+	}{
+		{
+			name: "algorithm",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: AlgorithmES256,
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name: "int",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: int(AlgorithmES256),
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name: "int8",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: int8(AlgorithmES256),
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name: "int16",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: int16(AlgorithmES256),
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name: "int32",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: int32(AlgorithmES256),
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name: "int64",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: int64(AlgorithmES256),
+			},
+			want: AlgorithmES256,
+		},
+		{
+			name:    "nil header",
+			h:       nil,
+			wantErr: ErrAlgorithmNotFound,
+		},
+		{
+			name:    "empty header",
+			h:       ProtectedHeader{},
+			wantErr: ErrAlgorithmNotFound,
+		},
+		{
+			name: "missing algorithm header",
+			h: ProtectedHeader{
+				"foo": "bar",
+			},
+			wantErr: ErrAlgorithmNotFound,
+		},
+		{
+			name: "unknown algorithm",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: "foo",
+			},
+			wantErr: ErrInvalidAlgorithm,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.Algorithm()
+			if err != tt.wantErr {
+				t.Errorf("ProtectedHeader.Algorithm() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProtectedHeader.Algorithm() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestProtectedHeader_Critical(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       ProtectedHeader
+		want    []interface{}
+		wantErr bool
+	}{
+		{
+			name: "valid header",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: AlgorithmES256,
+				HeaderLabelCritical: []interface{}{
+					HeaderLabelContentType,
+					"foo",
+				},
+				HeaderLabelContentType: "text/plain",
+				"foo":                  "bar",
+			},
+			want: []interface{}{
+				HeaderLabelContentType,
+				"foo",
+			},
+		},
+		{
+			name: "nil header",
+			h:    nil,
+			want: nil,
+		},
+		{
+			name: "empty header",
+			h:    ProtectedHeader{},
+			want: nil,
+		},
+		{
+			name: "nothing critical",
+			h: ProtectedHeader{
+				HeaderLabelAlgorithm: AlgorithmES256,
+			},
+			want: nil,
+		},
+		{
+			name: "empty critical",
+			h: ProtectedHeader{
+				HeaderLabelCritical: []interface{}{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid critical",
+			h: ProtectedHeader{
+				HeaderLabelCritical: 42,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.Critical()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProtectedHeader.Critical() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProtectedHeader.Critical() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestUnprotectedHeader_MarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       UnprotectedHeader
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "valid header",
+			h: UnprotectedHeader{
+				HeaderLabelKeyID: "foobar",
+			},
+			want: []byte{
+				0xa1,                                     // map
+				0x04,                                     // kid
+				0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, // foobar
+			},
+		},
+		{
+			name: "nil header",
+			h:    nil,
+			want: []byte{0xa0},
+		},
+		{
+			name: "empty header",
+			h:    UnprotectedHeader{},
+			want: []byte{0xa0},
+		},
+		{
+			name: "various types of integer label",
+			h: UnprotectedHeader{
+				uint(1):   0,
+				uint8(2):  0,
+				uint16(3): 0,
+				uint32(4): 0,
+				uint64(5): 0,
+				int(-1):   0,
+				int8(-2):  0,
+				int16(-3): 0,
+				int32(-4): 0,
+				int64(-5): 0,
+			},
+			want: []byte{
+				0xaa, // map
+				0x01, 0x00,
+				0x02, 0x00,
+				0x03, 0x00,
+				0x04, 0x00,
+				0x05, 0x00,
+				0x20, 0x00,
+				0x21, 0x00,
+				0x22, 0x00,
+				0x23, 0x00,
+				0x24, 0x00,
+			},
+		},
+		{
+			name: "invalid header label: struct type",
+			h: UnprotectedHeader{
+				struct {
+					value int
+				}{}: 42,
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicated key",
+			h: UnprotectedHeader{
+				int8(42):  "foo",
+				int64(42): "bar",
+			},
+			wantErr: true,
+		},
+		{
+			name: "un-marshalable content",
+			h: UnprotectedHeader{
+				"foo": make(chan bool),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.MarshalCBOR()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("UnprotectedHeader.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("UnprotectedHeader.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestUnprotectedHeader_UnmarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		data    []byte
+		want    UnprotectedHeader
+		wantErr bool
+	}{
+		{
+			name: "valid header",
+			data: []byte{
+				0xa1,                                     // map
+				0x04,                                     // kid
+				0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, // foobar
+			},
+			want: UnprotectedHeader{
+				HeaderLabelKeyID: "foobar",
+			},
+		},
+		{
+			name: "empty map",
+			data: []byte{0xa0},
+			want: UnprotectedHeader{},
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name:    "bad CBOR data",
+			data:    []byte{0x00, 0x01, 0x02, 0x04},
+			wantErr: true,
+		},
+		{
+			name:    "non-map header",
+			data:    []byte{0x00},
+			wantErr: true,
+		},
+		{
+			name: "invalid header label type: bstr type",
+			data: []byte{
+				0xa1, 0x40, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid header label type: major type 7: simple value", // issue #38
+			data: []byte{
+				0xa1, 0xf3, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicated key",
+			data: []byte{
+				0xa2, 0x01, 0x00, 0x01, 0x00,
+			},
+			wantErr: true,
+		},
+		{
+			name: "incomplete CBOR data",
+			data: []byte{
+				0xa5,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid map value",
+			data: []byte{
+				0xa1, 0x00, 0xa1, 0x00, 0x4f, 0x01,
+			},
+			wantErr: true,
+		},
+		{
+			name: "int map key too large",
+			data: []byte{
+				0xa1, 0x3b, 0x83, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got UnprotectedHeader
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("UnprotectedHeader.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("UnprotectedHeader.UnmarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestHeaders_MarshalProtected(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       Headers
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "pre-marshaled protected header",
+			h: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			want: []byte{0x43, 0xa1, 0x01, 0x26},
+		},
+		{
+			name: "raw over protected",
+			h: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmPS512,
+				},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			want: []byte{0x43, 0xa1, 0x01, 0x26},
+		},
+		{
+			name: "no pre-marshaled protected header",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			want: []byte{0x43, 0xa1, 0x01, 0x26},
+		},
+		{
+			name: "invalid protected header",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: make(chan bool),
+				},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.MarshalProtected()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Headers.MarshalProtected() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Headers.MarshalProtected() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestHeaders_MarshalUnprotected(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       Headers
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "pre-marshaled protected header",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			want: []byte{0xa1, 0x04, 0x18, 0x2a},
+		},
+		{
+			name: "raw over protected",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 43,
+				},
+			},
+			want: []byte{0xa1, 0x04, 0x18, 0x2a},
+		},
+		{
+			name: "no pre-marshaled protected header",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+			want: []byte{0xa1, 0x04, 0x18, 0x2a},
+		},
+		{
+			name: "invalid protected header",
+			h: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: make(chan bool),
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.h.MarshalUnprotected()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Headers.MarshalUnprotected() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Headers.MarshalUnprotected() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestHeaders_UnmarshalFromRaw(t *testing.T) {
+	tests := []struct {
+		name    string
+		h       Headers
+		want    Headers
+		wantErr bool
+	}{
+		{
+			name: "nil raw protected header",
+			h: Headers{
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil raw unprotected header",
+			h: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid raw header",
+			h: Headers{
+				RawProtected:   []byte{0x43, 0xa1, 0x01, 0x26},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+			},
+			want: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+		},
+		{
+			name: "replaced with raw header",
+			h: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES512,
+				},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 43,
+				},
+			},
+			want: Headers{
+				RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+				RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+				Unprotected: UnprotectedHeader{
+					HeaderLabelKeyID: 42,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.h
+			if err := got.UnmarshalFromRaw(); (err != nil) != tt.wantErr {
+				t.Errorf("Headers.UnmarshalFromRaw() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/rsa.go b/rsa.go
new file mode 100644
index 0000000..0ff5aff
--- /dev/null
+++ b/rsa.go
@@ -0,0 +1,59 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/rsa"
+	"io"
+)
+
+// rsaSigner is a RSASSA-PSS based signer with a generic crypto.Signer.
+//
+// Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-2
+type rsaSigner struct {
+	alg Algorithm
+	key crypto.Signer
+}
+
+// Algorithm returns the signing algorithm associated with the private key.
+func (rs *rsaSigner) Algorithm() Algorithm {
+	return rs.alg
+}
+
+// Sign signs digest with the private key, possibly using entropy from rand.
+// The resulting signature should follow RFC 8152 section 8.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8
+func (rs *rsaSigner) Sign(rand io.Reader, digest []byte) ([]byte, error) {
+	hash, _ := rs.alg.hashFunc()
+	return rs.key.Sign(rand, digest, &rsa.PSSOptions{
+		SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2
+		Hash:       hash,
+	})
+}
+
+// rsaVerifier is a RSASSA-PSS based verifier with golang built-in keys.
+//
+// Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-2
+type rsaVerifier struct {
+	alg Algorithm
+	key *rsa.PublicKey
+}
+
+// Algorithm returns the signing algorithm associated with the public key.
+func (rv *rsaVerifier) Algorithm() Algorithm {
+	return rv.alg
+}
+
+// Verify verifies digest with the public key, returning nil for success.
+// Otherwise, it returns ErrVerification.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8
+func (rv *rsaVerifier) Verify(digest []byte, signature []byte) error {
+	hash, _ := rv.alg.hashFunc()
+	if err := rsa.VerifyPSS(rv.key, hash, digest, signature, &rsa.PSSOptions{
+		SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2
+	}); err != nil {
+		return ErrVerification
+	}
+	return nil
+}
diff --git a/rsa_test.go b/rsa_test.go
new file mode 100644
index 0000000..1abb958
--- /dev/null
+++ b/rsa_test.go
@@ -0,0 +1,172 @@
+package cose
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"reflect"
+	"testing"
+)
+
+func generateTestRSAKey(t *testing.T) *rsa.PrivateKey {
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey() error = %v", err)
+	}
+	return key
+}
+
+func Test_rsaSigner(t *testing.T) {
+	// generate key
+	alg := AlgorithmPS256
+	key := generateTestRSAKey(t)
+
+	// set up signer
+	signer, err := NewSigner(alg, key)
+	if err != nil {
+		t.Fatalf("NewSigner() error = %v", err)
+	}
+	if _, ok := signer.(*rsaSigner); !ok {
+		t.Fatalf("NewSigner() type = %v, want *rsaSigner", reflect.TypeOf(signer))
+	}
+	if got := signer.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// sign / verify round trip
+	// see also conformance_test.go for strict tests.
+	digest, err := alg.computeHash([]byte("hello world"))
+	if err != nil {
+		t.Fatalf("Algorithm.computeHash() error = %v", err)
+	}
+	sig, err := signer.Sign(rand.Reader, digest)
+	if err != nil {
+		t.Fatalf("Sign() error = %v", err)
+	}
+
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("Verifier.Verify() error = %v", err)
+	}
+}
+
+func Test_rsaVerifier_Verify_Success(t *testing.T) {
+	// generate key
+	alg := AlgorithmPS256
+	key := generateTestRSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier
+	verifier, err := NewVerifier(alg, key.Public())
+	if err != nil {
+		t.Fatalf("NewVerifier() error = %v", err)
+	}
+	if _, ok := verifier.(*rsaVerifier); !ok {
+		t.Fatalf("NewVerifier() type = %v, want *rsaVerifier", reflect.TypeOf(verifier))
+	}
+	if got := verifier.Algorithm(); got != alg {
+		t.Fatalf("Algorithm() = %v, want %v", got, alg)
+	}
+
+	// verify round trip
+	if err := verifier.Verify(digest, sig); err != nil {
+		t.Fatalf("rsaVerifier.Verify() error = %v", err)
+	}
+}
+
+func Test_rsaVerifier_Verify_AlgorithmMismatch(t *testing.T) {
+	// generate key
+	alg := AlgorithmPS256
+	key := generateTestRSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier with a different algorithm
+	verifier := &rsaVerifier{
+		alg: AlgorithmPS512,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on algorithm mismatch
+	if err := verifier.Verify(digest, sig); err != ErrVerification {
+		t.Fatalf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+	}
+}
+
+func Test_rsaVerifier_Verify_KeyMismatch(t *testing.T) {
+	// generate key
+	alg := AlgorithmPS256
+	key := generateTestRSAKey(t)
+
+	// generate a valid signature
+	digest, sig := signTestData(t, alg, key)
+
+	// set up verifier with a different key / new key
+	key = generateTestRSAKey(t)
+	verifier := &rsaVerifier{
+		alg: alg,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on key mismatch
+	if err := verifier.Verify(digest, sig); err != ErrVerification {
+		t.Fatalf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+	}
+}
+
+func Test_rsaVerifier_Verify_InvalidSignature(t *testing.T) {
+	// generate key
+	alg := AlgorithmPS256
+	key := generateTestRSAKey(t)
+
+	// generate a valid signature with a tampered one
+	digest, sig := signTestData(t, alg, key)
+	tamperedSig := make([]byte, len(sig))
+	copy(tamperedSig, sig)
+	tamperedSig[0]++
+
+	// set up verifier with a different algorithm
+	verifier := &rsaVerifier{
+		alg: alg,
+		key: &key.PublicKey,
+	}
+
+	// verification should fail on invalid signature
+	tests := []struct {
+		name      string
+		signature []byte
+	}{
+		{
+			name:      "nil signature",
+			signature: nil,
+		},
+		{
+			name:      "empty signature",
+			signature: []byte{},
+		},
+		{
+			name:      "incomplete signature",
+			signature: sig[:len(sig)-2],
+		},
+		{
+			name:      "tampered signature",
+			signature: tamperedSig,
+		},
+		{
+			name:      "too many signature bytes",
+			signature: append(sig, 0),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := verifier.Verify(digest, tt.signature); err != ErrVerification {
+				t.Errorf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification)
+			}
+		})
+	}
+}
diff --git a/scripts/licenses.sh b/scripts/licenses.sh
new file mode 100644
index 0000000..91151b0
--- /dev/null
+++ b/scripts/licenses.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -e
+
+type go-licenses &> /dev/null || go get github.com/google/go-licenses
+
+MODULES+=("github.com/veraison/go-cose")
+
+for module in ${MODULES[@]}
+do
+  echo ">> retrieving licenses [ ${module} ]"
+  go-licenses csv ${module}
+done
diff --git a/sign.go b/sign.go
new file mode 100644
index 0000000..5b7af1f
--- /dev/null
+++ b/sign.go
@@ -0,0 +1,485 @@
+package cose
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+)
+
+// signature represents a COSE_Signature CBOR object:
+//
+//	COSE_Signature =  [
+//	    Headers,
+//	    signature : bstr
+//	]
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.1
+type signature struct {
+	_           struct{} `cbor:",toarray"`
+	Protected   cbor.RawMessage
+	Unprotected cbor.RawMessage
+	Signature   byteString
+}
+
+// signaturePrefix represents the fixed prefix of COSE_Signature.
+var signaturePrefix = []byte{
+	0x83, // Array of length 3
+}
+
+// Signature represents a decoded COSE_Signature.
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.1
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+type Signature struct {
+	Headers   Headers
+	Signature []byte
+}
+
+// NewSignature returns a Signature with header initialized.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func NewSignature() *Signature {
+	return &Signature{
+		Headers: Headers{
+			Protected:   ProtectedHeader{},
+			Unprotected: UnprotectedHeader{},
+		},
+	}
+}
+
+// MarshalCBOR encodes Signature into a COSE_Signature object.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func (s *Signature) MarshalCBOR() ([]byte, error) {
+	if s == nil {
+		return nil, errors.New("cbor: MarshalCBOR on nil Signature pointer")
+	}
+	if len(s.Signature) == 0 {
+		return nil, ErrEmptySignature
+	}
+	protected, err := s.Headers.MarshalProtected()
+	if err != nil {
+		return nil, err
+	}
+	unprotected, err := s.Headers.MarshalUnprotected()
+	if err != nil {
+		return nil, err
+	}
+	sig := signature{
+		Protected:   protected,
+		Unprotected: unprotected,
+		Signature:   s.Signature,
+	}
+	return encMode.Marshal(sig)
+}
+
+// UnmarshalCBOR decodes a COSE_Signature object into Signature.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func (s *Signature) UnmarshalCBOR(data []byte) error {
+	if s == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil Signature pointer")
+	}
+
+	// fast signature check
+	if !bytes.HasPrefix(data, signaturePrefix) {
+		return errors.New("cbor: invalid Signature object")
+	}
+
+	// decode to signature and parse
+	var raw signature
+	if err := decModeWithTagsForbidden.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	if len(raw.Signature) == 0 {
+		return ErrEmptySignature
+	}
+	sig := Signature{
+		Headers: Headers{
+			RawProtected:   raw.Protected,
+			RawUnprotected: raw.Unprotected,
+		},
+		Signature: raw.Signature,
+	}
+	if err := sig.Headers.UnmarshalFromRaw(); err != nil {
+		return err
+	}
+
+	*s = sig
+	return nil
+}
+
+// Sign signs a Signature using the provided Signer.
+// Signing a COSE_Signature requires the encoded protected header and the
+// payload of its 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 *Signature) Sign(rand io.Reader, signer Signer, protected cbor.RawMessage, payload, external []byte) error {
+	if s == nil {
+		return errors.New("signing nil Signature")
+	}
+	if payload == nil {
+		return ErrMissingPayload
+	}
+	if len(s.Signature) > 0 {
+		return errors.New("Signature already has signature bytes")
+	}
+	if len(protected) == 0 || protected[0]>>5 != 2 { // protected is a bstr
+		return errors.New("invalid body protected headers")
+	}
+
+	// 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
+	digest, err := s.digestToBeSigned(alg, protected, payload, external)
+	if err != nil {
+		return err
+	}
+	sig, err := signer.Sign(rand, digest)
+	if err != nil {
+		return err
+	}
+
+	s.Signature = sig
+	return nil
+}
+
+// Verify verifies the signature, returning nil on success or a suitable error
+// if verification fails.
+// Verifying a COSE_Signature requires the encoded protected header and the
+// payload of its 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 *Signature) Verify(verifier Verifier, protected cbor.RawMessage, payload, external []byte) error {
+	if s == nil {
+		return errors.New("verifying nil Signature")
+	}
+	if payload == nil {
+		return ErrMissingPayload
+	}
+	if len(s.Signature) == 0 {
+		return ErrEmptySignature
+	}
+	if len(protected) == 0 || protected[0]>>5 != 2 { // protected is a bstr
+		return errors.New("invalid body protected headers")
+	}
+
+	// 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
+	digest, err := s.digestToBeSigned(alg, protected, payload, external)
+	if err != nil {
+		return err
+	}
+	return verifier.Verify(digest, s.Signature)
+}
+
+// digestToBeSigned constructs Sig_structure, computes ToBeSigned, and returns
+// the digest of ToBeSigned.
+// If the signing algorithm does not have a hash algorithm associated,
+// ToBeSigned is returned instead.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func (s *Signature) digestToBeSigned(alg Algorithm, bodyProtected cbor.RawMessage, payload, external []byte) ([]byte, error) {
+	// create a Sig_structure and populate it with the appropriate fields.
+	//
+	//   Sig_structure = [
+	//       context : "Signature",
+	//       body_protected : empty_or_serialized_map,
+	//       sign_protected : empty_or_serialized_map,
+	//       external_aad : bstr,
+	//       payload : bstr
+	//   ]
+	var signProtected cbor.RawMessage
+	signProtected, err := s.Headers.MarshalProtected()
+	if err != nil {
+		return nil, err
+	}
+	if external == nil {
+		external = []byte{}
+	}
+	sigStructure := []interface{}{
+		"Signature",   // context
+		bodyProtected, // body_protected
+		signProtected, // sign_protected
+		external,      // external_aad
+		payload,       // payload
+	}
+
+	// create the value ToBeSigned by encoding the Sig_structure to a byte
+	// string.
+	toBeSigned, err := encMode.Marshal(sigStructure)
+	if err != nil {
+		return nil, err
+	}
+
+	// hash toBeSigned if there is a hash algorithm associated with the signing
+	// algorithm.
+	return alg.computeHash(toBeSigned)
+}
+
+// signMessage represents a COSE_Sign CBOR object:
+//
+//	COSE_Sign = [
+//	    Headers,
+//	    payload : bstr / nil,
+//	    signatures : [+ COSE_Signature]
+//	]
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.1
+type signMessage struct {
+	_           struct{} `cbor:",toarray"`
+	Protected   cbor.RawMessage
+	Unprotected cbor.RawMessage
+	Payload     byteString
+	Signatures  []cbor.RawMessage
+}
+
+// signMessagePrefix represents the fixed prefix of COSE_Sign_Tagged.
+var signMessagePrefix = []byte{
+	0xd8, 0x62, // #6.98
+	0x84, // Array of length 4
+}
+
+// SignMessage represents a decoded COSE_Sign message.
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.1
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+type SignMessage struct {
+	Headers    Headers
+	Payload    []byte
+	Signatures []*Signature
+}
+
+// NewSignMessage returns a SignMessage with header initialized.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func NewSignMessage() *SignMessage {
+	return &SignMessage{
+		Headers: Headers{
+			Protected:   ProtectedHeader{},
+			Unprotected: UnprotectedHeader{},
+		},
+	}
+}
+
+// MarshalCBOR encodes SignMessage into a COSE_Sign_Tagged object.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func (m *SignMessage) MarshalCBOR() ([]byte, error) {
+	if m == nil {
+		return nil, errors.New("cbor: MarshalCBOR on nil SignMessage pointer")
+	}
+	if len(m.Signatures) == 0 {
+		return nil, ErrNoSignatures
+	}
+	protected, err := m.Headers.MarshalProtected()
+	if err != nil {
+		return nil, err
+	}
+	unprotected, err := m.Headers.MarshalUnprotected()
+	if err != nil {
+		return nil, err
+	}
+	signatures := make([]cbor.RawMessage, 0, len(m.Signatures))
+	for _, sig := range m.Signatures {
+		sigCBOR, err := sig.MarshalCBOR()
+		if err != nil {
+			return nil, err
+		}
+		signatures = append(signatures, sigCBOR)
+	}
+	content := signMessage{
+		Protected:   protected,
+		Unprotected: unprotected,
+		Payload:     m.Payload,
+		Signatures:  signatures,
+	}
+	return encMode.Marshal(cbor.Tag{
+		Number:  CBORTagSignMessage,
+		Content: content,
+	})
+}
+
+// UnmarshalCBOR decodes a COSE_Sign_Tagged object into SignMessage.
+//
+// # Experimental
+//
+// Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func (m *SignMessage) UnmarshalCBOR(data []byte) error {
+	if m == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil SignMessage pointer")
+	}
+
+	// fast message check
+	if !bytes.HasPrefix(data, signMessagePrefix) {
+		return errors.New("cbor: invalid COSE_Sign_Tagged object")
+	}
+
+	// decode to signMessage and parse
+	var raw signMessage
+	if err := decModeWithTagsForbidden.Unmarshal(data[2:], &raw); err != nil {
+		return err
+	}
+	if len(raw.Signatures) == 0 {
+		return ErrNoSignatures
+	}
+	signatures := make([]*Signature, 0, len(raw.Signatures))
+	for _, sigCBOR := range raw.Signatures {
+		sig := &Signature{}
+		if err := sig.UnmarshalCBOR(sigCBOR); err != nil {
+			return err
+		}
+		signatures = append(signatures, sig)
+	}
+	msg := SignMessage{
+		Headers: Headers{
+			RawProtected:   raw.Protected,
+			RawUnprotected: raw.Unprotected,
+		},
+		Payload:    raw.Payload,
+		Signatures: signatures,
+	}
+	if err := msg.Headers.UnmarshalFromRaw(); err != nil {
+		return err
+	}
+
+	*m = msg
+	return nil
+}
+
+// Sign 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) Sign(rand io.Reader, external []byte, signers ...Signer) error {
+	if m == nil {
+		return errors.New("signing nil SignMessage")
+	}
+	if m.Payload == nil {
+		return ErrMissingPayload
+	}
+	switch len(m.Signatures) {
+	case 0:
+		return ErrNoSignatures
+	case len(signers):
+		// no ops
+	default:
+		return fmt.Errorf("%d signers for %d signatures", len(signers), len(m.Signatures))
+	}
+
+	// populate common parameters
+	var protected cbor.RawMessage
+	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 {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Verify verifies the signatures on the SignMessage against the corresponding
+// verifier, returning nil on success or a suitable error if verification fails.
+//
+// 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) Verify(external []byte, verifiers ...Verifier) error {
+	if m == nil {
+		return errors.New("verifying nil SignMessage")
+	}
+	if m.Payload == nil {
+		return ErrMissingPayload
+	}
+	switch len(m.Signatures) {
+	case 0:
+		return ErrNoSignatures
+	case len(verifiers):
+		// no ops
+	default:
+		return fmt.Errorf("%d verifiers for %d signatures", len(verifiers), len(m.Signatures))
+	}
+
+	// populate common parameters
+	var protected cbor.RawMessage
+	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 {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/sign1.go b/sign1.go
new file mode 100644
index 0000000..8fb1e1c
--- /dev/null
+++ b/sign1.go
@@ -0,0 +1,256 @@
+package cose
+
+import (
+	"bytes"
+	"errors"
+	"io"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+)
+
+// sign1Message represents a COSE_Sign1 CBOR object:
+//
+//	COSE_Sign1 = [
+//	    Headers,
+//	    payload : bstr / nil,
+//	    signature : bstr
+//	]
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.2
+type sign1Message struct {
+	_           struct{} `cbor:",toarray"`
+	Protected   cbor.RawMessage
+	Unprotected cbor.RawMessage
+	Payload     byteString
+	Signature   byteString
+}
+
+// sign1MessagePrefix represents the fixed prefix of COSE_Sign1_Tagged.
+var sign1MessagePrefix = []byte{
+	0xd2, // #6.18
+	0x84, // Array of length 4
+}
+
+// Sign1Message represents a decoded COSE_Sign1 message.
+//
+// Reference: https://tools.ietf.org/html/rfc8152#section-4.2
+type Sign1Message struct {
+	Headers   Headers
+	Payload   []byte
+	Signature []byte
+}
+
+// NewSign1Message returns a Sign1Message with header initialized.
+func NewSign1Message() *Sign1Message {
+	return &Sign1Message{
+		Headers: Headers{
+			Protected:   ProtectedHeader{},
+			Unprotected: UnprotectedHeader{},
+		},
+	}
+}
+
+// MarshalCBOR encodes Sign1Message into a COSE_Sign1_Tagged object.
+func (m *Sign1Message) MarshalCBOR() ([]byte, error) {
+	if m == nil {
+		return nil, errors.New("cbor: MarshalCBOR on nil Sign1Message pointer")
+	}
+	if len(m.Signature) == 0 {
+		return nil, ErrEmptySignature
+	}
+	protected, err := m.Headers.MarshalProtected()
+	if err != nil {
+		return nil, err
+	}
+	unprotected, err := m.Headers.MarshalUnprotected()
+	if err != nil {
+		return nil, err
+	}
+	content := sign1Message{
+		Protected:   protected,
+		Unprotected: unprotected,
+		Payload:     m.Payload,
+		Signature:   m.Signature,
+	}
+	return encMode.Marshal(cbor.Tag{
+		Number:  CBORTagSign1Message,
+		Content: content,
+	})
+}
+
+// UnmarshalCBOR decodes a COSE_Sign1_Tagged object into Sign1Message.
+func (m *Sign1Message) UnmarshalCBOR(data []byte) error {
+	if m == nil {
+		return errors.New("cbor: UnmarshalCBOR on nil Sign1Message pointer")
+	}
+
+	// fast message check
+	if !bytes.HasPrefix(data, sign1MessagePrefix) {
+		return errors.New("cbor: invalid COSE_Sign1_Tagged object")
+	}
+
+	// decode to sign1Message and parse
+	var raw sign1Message
+	if err := decModeWithTagsForbidden.Unmarshal(data[1:], &raw); err != nil {
+		return err
+	}
+	if len(raw.Signature) == 0 {
+		return ErrEmptySignature
+	}
+	msg := Sign1Message{
+		Headers: Headers{
+			RawProtected:   raw.Protected,
+			RawUnprotected: raw.Unprotected,
+		},
+		Payload:   raw.Payload,
+		Signature: raw.Signature,
+	}
+	if err := msg.Headers.UnmarshalFromRaw(); err != nil {
+		return err
+	}
+
+	*m = msg
+	return nil
+}
+
+// Sign signs a Sign1Message using the provided Signer.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func (m *Sign1Message) Sign(rand io.Reader, external []byte, signer Signer) error {
+	if m == nil {
+		return errors.New("signing nil Sign1Message")
+	}
+	if m.Payload == nil {
+		return ErrMissingPayload
+	}
+	if len(m.Signature) > 0 {
+		return errors.New("Sign1Message signature already has signature bytes")
+	}
+
+	// check algorithm if present.
+	// `alg` header MUST present if there is no externally supplied data.
+	alg := signer.Algorithm()
+	err := m.Headers.ensureSigningAlgorithm(alg, external)
+	if err != nil {
+		return err
+	}
+
+	// sign the message
+	digest, err := m.digestToBeSigned(alg, external)
+	if err != nil {
+		return err
+	}
+	sig, err := signer.Sign(rand, digest)
+	if err != nil {
+		return err
+	}
+
+	m.Signature = sig
+	return nil
+}
+
+// Verify 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) Verify(external []byte, verifier Verifier) error {
+	if m == nil {
+		return errors.New("verifying nil Sign1Message")
+	}
+	if m.Payload == nil {
+		return ErrMissingPayload
+	}
+	if len(m.Signature) == 0 {
+		return ErrEmptySignature
+	}
+
+	// check algorithm if present.
+	// `alg` header MUST present if there is no externally supplied data.
+	alg := verifier.Algorithm()
+	err := m.Headers.ensureVerificationAlgorithm(alg, external)
+	if err != nil {
+		return err
+	}
+
+	// verify the message
+	digest, err := m.digestToBeSigned(alg, external)
+	if err != nil {
+		return err
+	}
+	return verifier.Verify(digest, m.Signature)
+}
+
+// digestToBeSigned constructs Sig_structure, computes ToBeSigned, and returns
+// the digest of ToBeSigned.
+// If the signing algorithm does not have a hash algorithm associated,
+// ToBeSigned is returned instead.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func (m *Sign1Message) digestToBeSigned(alg Algorithm, external []byte) ([]byte, error) {
+	// create a Sig_structure and populate it with the appropriate fields.
+	//
+	//   Sig_structure = [
+	//       context : "Signature1",
+	//       body_protected : empty_or_serialized_map,
+	//       external_aad : bstr,
+	//       payload : bstr
+	//   ]
+	var protected cbor.RawMessage
+	protected, err := m.Headers.MarshalProtected()
+	if err != nil {
+		return nil, err
+	}
+	if external == nil {
+		external = []byte{}
+	}
+	sigStructure := []interface{}{
+		"Signature1", // context
+		protected,    // body_protected
+		external,     // external_aad
+		m.Payload,    // payload
+	}
+
+	// create the value ToBeSigned by encoding the Sig_structure to a byte
+	// string.
+	toBeSigned, err := encMode.Marshal(sigStructure)
+	if err != nil {
+		return nil, err
+	}
+
+	// hash toBeSigned if there is a hash algorithm associated with the signing
+	// algorithm.
+	return alg.computeHash(toBeSigned)
+}
+
+// Sign1 signs a Sign1Message using the provided Signer.
+//
+// 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, protected ProtectedHeader, payload, external []byte) (*Sign1Message, error) {
+	if protected == nil {
+		protected = ProtectedHeader{}
+	}
+	msg := &Sign1Message{
+		Headers: Headers{
+			Protected:   protected,
+			Unprotected: UnprotectedHeader{},
+		},
+		Payload: payload,
+	}
+	err := msg.Sign(rand, external, signer)
+	if err != nil {
+		return nil, err
+	}
+	return msg, nil
+}
+
+// Verify1 verifies a Sign1Message returning nil on success or a suitable error
+// if verification fails.
+//
+// This method is a wrapper of `Sign1Message.Verify()`.
+//
+// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
+func Verify1(msg *Sign1Message, external []byte, verifier Verifier) error {
+	return msg.Verify(external, verifier)
+}
diff --git a/sign1_test.go b/sign1_test.go
new file mode 100644
index 0000000..890e7ba
--- /dev/null
+++ b/sign1_test.go
@@ -0,0 +1,789 @@
+package cose
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/rand"
+	"reflect"
+	"testing"
+)
+
+func TestSign1Message_MarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		m       *Sign1Message
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "valid message",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload:   []byte("foo"),
+				Signature: []byte("bar"),
+			},
+			want: []byte{
+				0xd2, // tag
+				0x84,
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // payload
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+		},
+		{
+			name:    "nil message",
+			m:       nil,
+			wantErr: true,
+		},
+		{
+			name: "nil payload",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload:   nil,
+				Signature: []byte("bar"),
+			},
+			want: []byte{
+				0xd2, // tag
+				0x84,
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0xf6,                   // payload
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+		},
+		{
+			name: "nil signature",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload:   []byte("foo"),
+				Signature: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload:   nil,
+				Signature: []byte{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid protected header",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: make(chan bool),
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload:   []byte("foo"),
+				Signature: []byte("bar"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid unprotected header",
+			m: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: make(chan bool),
+					},
+				},
+				Payload:   []byte("foo"),
+				Signature: []byte("bar"),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.m.MarshalCBOR()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Sign1Message.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Sign1Message.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSign1Message_UnmarshalCBOR(t *testing.T) {
+	// test nil pointer
+	t.Run("nil Sign1Message pointer", func(t *testing.T) {
+		var msg *Sign1Message
+		data := []byte{0xd2, 0x84, 0x40, 0xa0, 0xf6, 0x41, 0x00}
+		if err := msg.UnmarshalCBOR(data); err == nil {
+			t.Errorf("want error on nil *Sign1Message")
+		}
+	})
+
+	// test others
+	tests := []struct {
+		name    string
+		data    []byte
+		want    Sign1Message
+		wantErr bool
+	}{
+		{
+			name: "valid message",
+			data: []byte{
+				0xd2, // tag
+				0x84,
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // payload
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+			want: Sign1Message{
+				Headers: Headers{
+					RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: int64(42),
+					},
+				},
+				Payload:   []byte("foo"),
+				Signature: []byte("bar"),
+			},
+		},
+		{
+			name: "valid message with nil payload",
+			data: []byte{
+				0xd2, // tag
+				0x84,
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0xf6,                   // payload
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+			want: Sign1Message{
+				Headers: Headers{
+					RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: int64(42),
+					},
+				},
+				Payload:   nil,
+				Signature: []byte("bar"),
+			},
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name:    "invalid message with valid prefix", // issue #29
+			data:    []byte{0xd2, 0x84, 0xf7, 0xf7, 0xf7, 0xf7},
+			wantErr: true,
+		},
+		{
+			name: "tagged signature", // issue #30
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6,             // nil payload
+				0xcb, 0xa1, 0x00, // tagged signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil signature",
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6, // payload
+				0xf6, // nil signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6, // payload
+				0x40, // empty signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch tag",
+			data: []byte{
+				0xd3, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6,       // payload
+				0x41, 0x00, //  signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch type",
+			data: []byte{
+				0xd2, 0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "smaller array size",
+			data: []byte{
+				0xd2, 0x83, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6, // payload
+			},
+			wantErr: true,
+		},
+		{
+			name: "larger array size",
+			data: []byte{
+				0xd2, 0x85, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6,       // payload
+				0x41, 0x00, // signature
+				0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "undefined payload",
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf7,       // undefined payload
+				0x41, 0x00, // signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "payload as a byte array",
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0x80,       // payload
+				0x41, 0x00, // signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "signature as a byte array",
+			data: []byte{
+				0xd2, 0x84, // prefix
+				0x40, 0xa0, // empty headers
+				0xf6,       // nil payload
+				0x81, 0x00, // signature
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got Sign1Message
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("Sign1Message.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Sign1Message.UnmarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSign1Message_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
+	// see also conformance_test.go for strict tests.
+	tests := []struct {
+		name             string
+		msg              *Sign1Message
+		externalOnSign   []byte
+		externalOnVerify []byte
+		wantErr          bool
+		check            func(t *testing.T, m *Sign1Message)
+	}{
+		{
+			name: "valid message",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			externalOnSign:   []byte{},
+			externalOnVerify: []byte{},
+		},
+		{
+			name: "valid message with external",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			externalOnSign:   []byte("foo"),
+			externalOnVerify: []byte("foo"),
+		},
+		{
+			name: "nil external",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			externalOnSign:   nil,
+			externalOnVerify: nil,
+		},
+		{
+			name: "mixed nil / empty external",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			externalOnSign:   []byte{},
+			externalOnVerify: nil,
+		},
+		{
+			name: "nil payload", // payload is detached
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+				},
+				Payload: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch algorithm",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES512,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing algorithm",
+			msg: &Sign1Message{
+				Payload: []byte("hello world"),
+			},
+			check: func(t *testing.T, m *Sign1Message) {
+				got, err := m.Headers.Protected.Algorithm()
+				if err != nil {
+					t.Errorf("Sign1Message.Headers.Protected.Algorithm() error = %v", err)
+				}
+				if got != alg {
+					t.Errorf("Sign1Message.Headers.Protected.Algorithm() = %v, want %v", got, alg)
+				}
+			},
+		},
+		{
+			name: "missing algorithm with raw protected",
+			msg: &Sign1Message{
+				Headers: Headers{
+					RawProtected: []byte{0x40},
+				},
+				Payload: []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing algorithm with externally supplied data",
+			msg: &Sign1Message{
+				Payload: []byte("hello world"),
+			},
+			externalOnSign:   []byte("foo"),
+			externalOnVerify: []byte("foo"),
+			check: func(t *testing.T, m *Sign1Message) {
+				_, err := m.Headers.Protected.Algorithm()
+				if want := ErrAlgorithmNotFound; err != want {
+					t.Errorf("Sign1Message.Headers.Protected.Algorithm() error = %v, wantErr %v", err, want)
+				}
+			},
+		},
+		{
+			name: "double signing",
+			msg: &Sign1Message{
+				Payload:   []byte("hello world"),
+				Signature: []byte("foobar"),
+			},
+			wantErr: true,
+		},
+		{
+			name:    "nil message",
+			msg:     nil,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.msg.Sign(rand.Reader, tt.externalOnSign, signer)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Sign1Message.Sign() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err != nil {
+				return
+			}
+			if tt.check != nil {
+				tt.check(t, tt.msg)
+			}
+			if err := tt.msg.Verify(tt.externalOnVerify, verifier); err != nil {
+				t.Errorf("Sign1Message.Verify() error = %v", err)
+			}
+		})
+	}
+}
+
+func TestSign1Message_Sign_Internal(t *testing.T) {
+	tests := []struct {
+		name       string
+		msg        *Sign1Message
+		external   []byte
+		toBeSigned []byte
+	}{
+		{
+			name: "valid message",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			external: []byte{},
+			toBeSigned: []byte{
+				0x84,                                                             // array type
+				0x6a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x31, // context
+				0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // protected
+				0x40,                                                                   // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+		{
+			name: "valid message with external",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			external: []byte("foo"),
+			toBeSigned: []byte{
+				0x84,                                                             // array type
+				0x6a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x31, // context
+				0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // protected
+				0x43, 0x66, 0x6f, 0x6f, // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+		{
+			name: "nil external",
+			msg: &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			},
+			external: nil,
+			toBeSigned: []byte{
+				0x84,                                                             // array type
+				0x6a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x31, // context
+				0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // protected
+				0x40,                                                                   // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+		{
+			name: "nil protected header",
+			msg: &Sign1Message{
+				Payload: []byte("hello world"),
+			},
+			external: []byte("foo"),
+			toBeSigned: []byte{
+				0x84,                                                             // array type
+				0x6a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x31, // context
+				0x40,                   // protected
+				0x43, 0x66, 0x6f, 0x6f, // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			hash := crypto.SHA256
+			RegisterAlgorithm(algorithmMock, "Mock", hash, nil)
+			defer resetExtendedAlgorithm()
+
+			sig := make([]byte, 64)
+			_, err := rand.Read(sig)
+			if err != nil {
+				t.Fatalf("rand.Read() error = %v", err)
+			}
+			h := hash.New()
+			h.Write(tt.toBeSigned)
+			digest := h.Sum(nil)
+			signer := newMockSigner(t)
+			signer.setup(digest, sig)
+
+			msg := tt.msg
+			if err := msg.Sign(rand.Reader, tt.external, signer); err != nil {
+				t.Errorf("Sign1Message.Sign() error = %v", err)
+				return
+			}
+			if got := msg.Signature; !bytes.Equal(got, sig) {
+				t.Errorf("Sign1Message.Sign() signature = %v, want %v", got, sig)
+			}
+		})
+	}
+}
+
+func TestSign1Message_Verify(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
+	// see also conformance_test.go for strict tests.
+	tests := []struct {
+		name             string
+		externalOnSign   []byte
+		externalOnVerify []byte
+		tamper           func(m *Sign1Message) *Sign1Message
+		wantErr          bool
+	}{
+		{
+			name: "round trip on valid message",
+		},
+		{
+			name:             "external mismatch",
+			externalOnSign:   []byte("foo"),
+			externalOnVerify: []byte("bar"),
+			wantErr:          true,
+		},
+		{
+			name:             "mixed nil / empty external",
+			externalOnSign:   nil,
+			externalOnVerify: []byte{},
+		},
+		{
+			name: "nil message",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				return nil
+			},
+			wantErr: true,
+		},
+		{
+			name: "strip signature",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Signature = nil
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Signature = []byte{}
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name: "tamper protected header",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Headers.Protected["foo"] = "bar"
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name: "tamper unprotected header",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Headers.Unprotected["foo"] = "bar"
+				return m
+			},
+			wantErr: false, // allowed
+		},
+		{
+			name: "tamper payload",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Payload = []byte("foobar")
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name: "tamper signature",
+			tamper: func(m *Sign1Message) *Sign1Message {
+				m.Signature[0]++
+				return m
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// generate message and sign
+			msg := &Sign1Message{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("hello world"),
+			}
+			if err := msg.Sign(rand.Reader, tt.externalOnSign, signer); err != nil {
+				t.Errorf("Sign1Message.Sign() error = %v", err)
+				return
+			}
+
+			// tamper message
+			if tt.tamper != nil {
+				msg = tt.tamper(msg)
+			}
+
+			// verify message
+			if err := msg.Verify(tt.externalOnVerify, verifier); (err != nil) != tt.wantErr {
+				t.Errorf("Sign1Message.Verify() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+
+	// special cases
+	t.Run("nil payload", func(t *testing.T) { // payload is detached
+		msg := &Sign1Message{
+			Headers: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelAlgorithm: AlgorithmES256,
+				},
+			},
+			Payload: []byte{},
+		}
+		if err := msg.Sign(rand.Reader, nil, signer); err != nil {
+			t.Errorf("Sign1Message.Sign() error = %v", err)
+			return
+		}
+
+		// make payload nil on verify
+		msg.Payload = nil
+
+		// verify message
+		if err := msg.Verify(nil, verifier); err == nil {
+			t.Error("Sign1Message.Verify() error = nil, wantErr true")
+		}
+	})
+}
diff --git a/sign_test.go b/sign_test.go
new file mode 100644
index 0000000..0089437
--- /dev/null
+++ b/sign_test.go
@@ -0,0 +1,2147 @@
+package cose
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/rand"
+	"reflect"
+	"testing"
+
+	"google3/third_party/golang/github_com/fxamacker/cbor/v/v2/cbor"
+)
+
+func TestSignature_MarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		s       *Signature
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "valid message",
+			s: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Signature: []byte("bar"),
+			},
+			want: []byte{
+				0x83,                   // array of size 3
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+		},
+		{
+			name:    "nil signature",
+			s:       nil,
+			wantErr: true,
+		},
+		{
+			name: "nil signature",
+			s: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Signature: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			s: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Signature: []byte{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid protected header",
+			s: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: make(chan bool),
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Signature: []byte("bar"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid unprotected header",
+			s: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: make(chan bool),
+					},
+				},
+				Signature: []byte("bar"),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.s.MarshalCBOR()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Signature.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Signature.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSignature_UnmarshalCBOR(t *testing.T) {
+	// test nil pointer
+	t.Run("nil Signature pointer", func(t *testing.T) {
+		var sig *Signature
+		data := []byte{0x83, 0x40, 0xa0, 0x41, 0x00}
+		if err := sig.UnmarshalCBOR(data); err == nil {
+			t.Errorf("want error on nil *Signature")
+		}
+	})
+
+	// test others
+	tests := []struct {
+		name    string
+		data    []byte
+		want    Signature
+		wantErr bool
+	}{
+		{
+			name: "valid signature struct",
+			data: []byte{
+				0x83,
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+			want: Signature{
+				Headers: Headers{
+					RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: int64(42),
+					},
+				},
+				Signature: []byte("bar"),
+			},
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name: "tagged signature", // issue #30
+			data: []byte{
+				0x83,
+				0x40, 0xa0, // empty headers
+				0xcb, 0xa1, 0x00, // tagged signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil signature",
+			data: []byte{
+				0x83,
+				0x40, 0xa0, // empty headers
+				0xf6, // nil signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			data: []byte{
+				0x83,
+				0x40, 0xa0, // empty headers
+				0x40, // empty signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch type",
+			data: []byte{
+				0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "smaller array size",
+			data: []byte{
+				0x82,
+				0x40, 0xa0, // empty headers
+			},
+			wantErr: true,
+		},
+		{
+			name: "larger array size",
+			data: []byte{
+				0x84,
+				0x40, 0xa0, // empty headers
+				0x41, 0x00, // signature
+				0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "signature as a byte array",
+			data: []byte{
+				0x83,
+				0x40, 0xa0, // empty headers
+				0x81, 0x00, // signature
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got Signature
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("Signature.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Signature.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSignature_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 {
+		protected cbor.RawMessage
+		payload   []byte
+		external  []byte
+	}
+	tests := []struct {
+		name     string
+		sig      *Signature
+		onSign   args
+		onVerify args
+		wantErr  bool
+		check    func(t *testing.T, s *Signature)
+	}{
+		{
+			name: "valid message",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+		},
+		{
+			name: "valid message with external",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("foo"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("foo"),
+			},
+		},
+		{
+			name: "nil external",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  nil,
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  nil,
+			},
+		},
+		{
+			name: "mixed nil / empty external",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  nil,
+			},
+		},
+		{
+			name: "nil payload", // payload is detached
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   nil,
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch algorithm",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES512,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing algorithm",
+			sig:  &Signature{},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			check: func(t *testing.T, s *Signature) {
+				got, err := s.Headers.Protected.Algorithm()
+				if err != nil {
+					t.Errorf("Signature.Headers.Protected.Algorithm() error = %v", err)
+				}
+				if got != alg {
+					t.Errorf("Signature.Headers.Protected.Algorithm() = %v, want %v", got, alg)
+				}
+			},
+		},
+		{
+			name: "missing algorithm with raw protected",
+			sig: &Signature{
+				Headers: Headers{
+					RawProtected: []byte{0x40},
+				},
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing algorithm with externally supplied data",
+			sig:  &Signature{},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("foo"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("foo"),
+			},
+			check: func(t *testing.T, s *Signature) {
+				_, err := s.Headers.Protected.Algorithm()
+				if want := ErrAlgorithmNotFound; err != want {
+					t.Errorf("Signature.Headers.Protected.Algorithm() error = %v, wantErr %v", err, want)
+				}
+			},
+		},
+		{
+			name: "double signing",
+			sig: &Signature{
+				Signature: []byte("foobar"),
+			},
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil signature",
+			sig:  nil,
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil body protected header",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: nil,
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: nil,
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty body protected header",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid protected header",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			onSign: args{
+				protected: []byte{0xa0},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0xa0},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.sig.Sign(rand.Reader, signer, tt.onSign.protected, tt.onSign.payload, tt.onSign.external)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Signature.Sign() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err != nil {
+				return
+			}
+			if tt.check != nil {
+				tt.check(t, tt.sig)
+			}
+			if err := tt.sig.Verify(verifier, tt.onVerify.protected, tt.onVerify.payload, tt.onVerify.external); err != nil {
+				t.Errorf("Signature.Verify() error = %v", err)
+			}
+		})
+	}
+}
+
+func TestSignature_Sign_Internal(t *testing.T) {
+	tests := []struct {
+		name       string
+		sig        *Signature
+		protected  cbor.RawMessage
+		payload    []byte
+		external   []byte
+		toBeSigned []byte
+	}{
+		{
+			name: "valid message",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			protected: []byte{0x40, 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
+				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
+			},
+		},
+		{
+			name: "valid message with empty parent protected header",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			protected: []byte{0x40},
+			payload:   []byte("hello world"),
+			external:  []byte{},
+			toBeSigned: []byte{
+				0x85,                                                       // array type
+				0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context
+				0x40,                                           // 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
+			},
+		},
+		{
+			name: "valid message with external",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			protected: []byte{0x40},
+			payload:   []byte("hello world"),
+			external:  []byte("foo"),
+			toBeSigned: []byte{
+				0x85,                                                       // array type
+				0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context
+				0x40,                                           // body_protected
+				0x47, 0xa1, 0x01, 0x3a, 0x6d, 0x6f, 0x63, 0x6a, // sign_protected
+				0x43, 0x66, 0x6f, 0x6f, // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+		{
+			name: "nil external",
+			sig: &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: algorithmMock,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			},
+			protected: []byte{0x40},
+			payload:   []byte("hello world"),
+			external:  nil,
+			toBeSigned: []byte{
+				0x85,                                                       // array type
+				0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context
+				0x40,                                           // 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
+			},
+		},
+		{
+			name:      "nil protected header",
+			sig:       &Signature{},
+			protected: []byte{0x40},
+			payload:   []byte("hello world"),
+			external:  []byte("foo"),
+			toBeSigned: []byte{
+				0x85,                                                       // array type
+				0x69, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, // context
+				0x40,                   // body_protected
+				0x40,                   // sign_protected
+				0x43, 0x66, 0x6f, 0x6f, // external
+				0x4b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, // payload
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			hash := crypto.SHA256
+			RegisterAlgorithm(algorithmMock, "Mock", hash, nil)
+			defer resetExtendedAlgorithm()
+
+			want := make([]byte, 64)
+			_, err := rand.Read(want)
+			if err != nil {
+				t.Fatalf("rand.Read() error = %v", err)
+			}
+			h := hash.New()
+			h.Write(tt.toBeSigned)
+			digest := h.Sum(nil)
+			signer := newMockSigner(t)
+			signer.setup(digest, want)
+
+			sig := tt.sig
+			if err := sig.Sign(rand.Reader, signer, tt.protected, tt.payload, tt.external); err != nil {
+				t.Errorf("Signature.Sign() error = %v", err)
+				return
+			}
+			if got := sig.Signature; !bytes.Equal(got, want) {
+				t.Errorf("Signature.Sign() signature = %v, want %v", got, want)
+			}
+		})
+	}
+}
+
+func TestSignature_Verify(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 {
+		protected cbor.RawMessage
+		payload   []byte
+		external  []byte
+	}
+	tests := []struct {
+		name     string
+		sig      *Signature
+		onSign   args
+		onVerify args
+		tamper   func(s *Signature) *Signature
+		wantErr  bool
+	}{
+		{
+			name: "round trip on valid message",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+		},
+		{
+			name: "round trip on valid message with nil external data",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+		},
+		{
+			name: "mixed nil / empty external",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  nil,
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte{},
+			},
+		},
+		{
+			name: "nil body protected header",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: nil,
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty body protected header",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid body protected header",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0xa0},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "body protected header mismatch",
+			onSign: args{
+				protected: []byte{0x43, 0xa1, 0x00, 0x00},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x43, 0xa1, 0x00, 0x01},
+				payload:   []byte("hello world"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil payload",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte{},
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "payload mismatch",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("foobar"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "external mismatch",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("foo"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+				external:  []byte("bar"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil signature struct",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				return nil
+			},
+			wantErr: true,
+		},
+		{
+			name: "strip signature",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				s.Signature = nil
+				return s
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signature",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				s.Signature = []byte{}
+				return s
+			},
+			wantErr: true,
+		},
+		{
+			name: "tamper protected header",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				s.Headers.Protected["foo"] = "bar"
+				return s
+			},
+			wantErr: true,
+		},
+		{
+			name: "tamper unprotected header",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				s.Headers.Unprotected["foo"] = "bar"
+				return s
+			},
+			wantErr: false, // allowed
+		},
+		{
+			name: "tamper signature",
+			onSign: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			onVerify: args{
+				protected: []byte{0x40},
+				payload:   []byte("hello world"),
+			},
+			tamper: func(s *Signature) *Signature {
+				s.Signature[0]++
+				return s
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// generate signature request and sign
+			sig := &Signature{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+			}
+			if err := sig.Sign(rand.Reader, signer, tt.onSign.protected, tt.onSign.payload, tt.onSign.external); err != nil {
+				t.Errorf("Signature.Sign() error = %v", err)
+				return
+			}
+
+			// tamper signature
+			if tt.tamper != nil {
+				sig = tt.tamper(sig)
+			}
+
+			// verify signature
+			if err := sig.Verify(verifier, tt.onVerify.protected, tt.onVerify.payload, tt.onVerify.external); (err != nil) != tt.wantErr {
+				t.Errorf("Signature.Verify() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestSignMessage_MarshalCBOR(t *testing.T) {
+	tests := []struct {
+		name    string
+		m       *SignMessage
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "valid message with multiple signatures",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+						Signature: []byte("foo"),
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmPS512,
+							},
+						},
+						Signature: []byte("bar"),
+					},
+				},
+			},
+			want: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0x4b, // payload
+				0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
+				0x82,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+				0x83,                         // signature 1
+				0x44, 0xa1, 0x01, 0x38, 0x26, // protected
+				0xa0,                   // unprotected
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+		},
+		{
+			name: "valid message with one signature",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+			want: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0x4b, // payload
+				0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
+				0x81,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+			},
+		},
+		{
+			name: "nil signatures",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload:    []byte("hello world"),
+				Signatures: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signatures",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload:    []byte("hello world"),
+				Signatures: []*Signature{},
+			},
+			wantErr: true,
+		},
+		{
+			name:    "nil message",
+			m:       nil,
+			wantErr: true,
+		},
+		{
+			name: "nil payload",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: nil,
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+			want: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0xf6,                   // payload
+				0x81,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+			},
+		},
+		{
+			name: "invalid protected header",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: make(chan bool),
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: 42,
+					},
+				},
+				Payload: []byte("foo"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid unprotected header",
+			m: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelAlgorithm: AlgorithmES256,
+					},
+					Unprotected: UnprotectedHeader{
+						HeaderLabelKeyID: make(chan bool),
+					},
+				},
+				Payload: []byte("foo"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.m.MarshalCBOR()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("SignMessage.MarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("SignMessage.MarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSignMessage_UnmarshalCBOR(t *testing.T) {
+	// test nil pointer
+	t.Run("nil SignMessage pointer", func(t *testing.T) {
+		var msg *SignMessage
+		data := []byte{
+			0xd8, 0x62, 0x84, 0x40, 0xa0, 0xf6,
+			0x81, 0x83, 0x40, 0xa0, 0x41, 0x00,
+		}
+		if err := msg.UnmarshalCBOR(data); err == nil {
+			t.Errorf("want error on nil *SignMessage")
+		}
+	})
+
+	// test others
+	tests := []struct {
+		name    string
+		data    []byte
+		want    SignMessage
+		wantErr bool
+	}{
+		{
+			name: "valid message with multiple signatures",
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0x4b, // payload
+				0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
+				0x82,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+				0x83,                         // signature 1
+				0x44, 0xa1, 0x01, 0x38, 0x26, // protected
+				0xa0,                   // unprotected
+				0x43, 0x62, 0x61, 0x72, // signature
+			},
+			want: SignMessage{
+				Headers: Headers{
+					RawProtected: []byte{
+						0x4d, 0xa1,
+						0x03,
+						0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+					},
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					RawUnprotected: []byte{
+						0xa1,
+						0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+						0x64, 0x74, 0x65, 0x73, 0x74,
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: int64(42),
+							},
+						},
+						Signature: []byte("foo"),
+					},
+					{
+						Headers: Headers{
+							RawProtected: []byte{0x44, 0xa1, 0x01, 0x38, 0x26},
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmPS512,
+							},
+							RawUnprotected: []byte{0xa0},
+							Unprotected:    UnprotectedHeader{},
+						},
+						Signature: []byte("bar"),
+					},
+				},
+			},
+		},
+		{
+			name: "valid message with one signature",
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0x4b, // payload
+				0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
+				0x81,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+			},
+			want: SignMessage{
+				Headers: Headers{
+					RawProtected: []byte{
+						0x4d, 0xa1,
+						0x03,
+						0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+					},
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					RawUnprotected: []byte{
+						0xa1,
+						0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+						0x64, 0x74, 0x65, 0x73, 0x74,
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: int64(42),
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+		},
+		{
+			name: "valid message with nil payload",
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0xf6,                   // payload
+				0x81,                   // signatures
+				0x83,                   // signature 0
+				0x43, 0xa1, 0x01, 0x26, // protected
+				0xa1, 0x04, 0x18, 0x2a, // unprotected
+				0x43, 0x66, 0x6f, 0x6f, // signature
+			},
+			want: SignMessage{
+				Headers: Headers{
+					RawProtected: []byte{
+						0x4d, 0xa1,
+						0x03,
+						0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+					},
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					RawUnprotected: []byte{
+						0xa1,
+						0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+						0x64, 0x74, 0x65, 0x73, 0x74,
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: nil,
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							RawProtected: []byte{0x43, 0xa1, 0x01, 0x26},
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							RawUnprotected: []byte{0xa1, 0x04, 0x18, 0x2a},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: int64(42),
+							},
+						},
+						Signature: []byte("foo"),
+					},
+				},
+			},
+		},
+		{
+			name: "nil signatures",
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0xf6, // nil payload
+				0xf6, // signatures
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signatures",
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0xf6, // nil payload
+				0x80, // signatures
+			},
+			wantErr: true,
+		},
+		{
+			name: "tagged signature", // issue #30
+			data: []byte{
+				0xd8, 0x62, // tag
+				0x84,
+				0x4d, 0xa1, // protected
+				0x03,
+				0x6a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+				0xa1, // unprotected
+				0x65, 0x65, 0x78, 0x74, 0x72, 0x61,
+				0x64, 0x74, 0x65, 0x73, 0x74,
+				0xf6,       // nil payload
+				0x81,       // signatures
+				0x83,       // signature 0
+				0x40, 0xa0, // empty headers
+				0xcb, 0xa1, 0x00, // tagged signature
+			},
+			wantErr: true,
+		},
+		{
+			name:    "nil CBOR data",
+			data:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty CBOR data",
+			data:    []byte{},
+			wantErr: true,
+		},
+		{
+			name: "mismatch tag",
+			data: []byte{
+				0xd2, 0x84, // tag
+				0x40, 0xa0, // empty headers
+				0xf6,       // nil payload
+				0x81,       // signatures
+				0x83,       // signature 0
+				0x40, 0xa0, // empty headers
+				0x41, 0x00, // signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch type",
+			data: []byte{
+				0xd8, 0x62, 0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "smaller array size",
+			data: []byte{
+				0xd8, 0x62, 0x83, // tag
+				0x40, 0xa0, // empty headers
+				0xf6, // nil payload
+			},
+			wantErr: true,
+		},
+		{
+			name: "larger array size",
+			data: []byte{
+				0xd8, 0x62, 0x85, // tag
+				0x40, 0xa0, // empty headers
+				0xf6,       // nil payload
+				0x81,       // signatures
+				0x83,       // signature 0
+				0x40, 0xa0, // empty headers
+				0x41, 0x00, // signature
+				0x40,
+			},
+			wantErr: true,
+		},
+		{
+			name: "undefined payload",
+			data: []byte{
+				0xd8, 0x62, 0x84, // tag
+				0x40, 0xa0, // empty headers
+				0xf7,       // undefined payload
+				0x81,       // signatures
+				0x83,       // signature 0
+				0x40, 0xa0, // empty headers
+				0x41, 0x00, // signature
+			},
+			wantErr: true,
+		},
+		{
+			name: "payload as a byte array",
+			data: []byte{
+				0xd8, 0x62, 0x84, // tag
+				0x40, 0xa0, // empty headers
+				0x80,       // payload
+				0x81,       // signatures
+				0x83,       // signature 0
+				0x40, 0xa0, // empty headers
+				0x41, 0x00, // signature
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got SignMessage
+			if err := got.UnmarshalCBOR(tt.data); (err != nil) != tt.wantErr {
+				t.Errorf("SignMessage.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("SignMessage.UnmarshalCBOR() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSignMessage_Sign(t *testing.T) {
+	// generate key and set up signer / verifier
+	gen := func(alg Algorithm) (Signer, Verifier) {
+		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)
+		}
+		return signer, verifier
+	}
+	algorithms := []Algorithm{AlgorithmES256, AlgorithmES512}
+	signers := make([]Signer, 2)
+	verifiers := make([]Verifier, 2)
+	for i, alg := range algorithms {
+		signers[i], verifiers[i] = gen(alg)
+	}
+
+	// sign / verify round trip
+	tests := []struct {
+		name             string
+		msg              *SignMessage
+		externalOnSign   []byte
+		externalOnVerify []byte
+		wantErr          bool
+		check            func(t *testing.T, m *SignMessage)
+	}{
+		{
+			name: "valid message",
+			msg: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			},
+			externalOnSign:   []byte{},
+			externalOnVerify: []byte{},
+		},
+		{
+			name: "valid message with external",
+			msg: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			},
+			externalOnSign:   []byte("foo"),
+			externalOnVerify: []byte("foo"),
+		},
+		{
+			name: "nil external",
+			msg: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			},
+			externalOnSign:   nil,
+			externalOnVerify: nil,
+		},
+		{
+			name: "mixed nil / empty external",
+			msg: &SignMessage{
+				Headers: Headers{
+					Protected: ProtectedHeader{
+						HeaderLabelContentType: "text/plain",
+					},
+					Unprotected: UnprotectedHeader{
+						"extra": "test",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			},
+			externalOnSign:   []byte{},
+			externalOnVerify: nil,
+		},
+		{
+			name: "nil payload", // payload is detached
+			msg: &SignMessage{
+				Payload: nil,
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "mismatch algorithm",
+			msg: &SignMessage{
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "plain message",
+			msg: &SignMessage{
+				Payload:    []byte("hello world"),
+				Signatures: []*Signature{{}, {}},
+			},
+			check: func(t *testing.T, m *SignMessage) {
+				for i, alg := range algorithms {
+					got, err := m.Signatures[i].Headers.Protected.Algorithm()
+					if err != nil {
+						t.Errorf("SignMessage.Signatures[%d].Headers.Protected.Algorithm() error = %v", i, err)
+					}
+					if got != alg {
+						t.Errorf("SignMessage.Signatures[%d].Headers.Protected.Algorithm() = %v, want %v", i, got, alg)
+					}
+				}
+			},
+		},
+		{
+			name: "double signing",
+			msg: &SignMessage{
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{},
+					{
+						Signature: []byte("foobar"),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name:    "nil message",
+			msg:     nil,
+			wantErr: true,
+		},
+		{
+			name: "too few signers",
+			msg: &SignMessage{
+				Payload:    []byte("hello world"),
+				Signatures: []*Signature{{}, {}, {}},
+			},
+			wantErr: true,
+		},
+		{
+			name: "too many signers",
+			msg: &SignMessage{
+				Payload:    []byte("hello world"),
+				Signatures: []*Signature{{}},
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty signatures",
+			msg: &SignMessage{
+				Payload:    []byte("hello world"),
+				Signatures: []*Signature{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "nil signatures",
+			msg: &SignMessage{
+				Payload:    []byte("hello world"),
+				Signatures: nil,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.msg.Sign(rand.Reader, tt.externalOnSign, signers...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("SignMessage.Sign() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err != nil {
+				return
+			}
+			if tt.check != nil {
+				tt.check(t, tt.msg)
+			}
+			if err := tt.msg.Verify(tt.externalOnVerify, verifiers...); err != nil {
+				t.Errorf("SignMessage.Verify() error = %v", err)
+			}
+		})
+	}
+
+	// special cases
+	t.Run("no signer", func(t *testing.T) {
+		msg := &SignMessage{
+			Payload:    []byte("hello world"),
+			Signatures: []*Signature{{}},
+		}
+		if err := msg.Sign(rand.Reader, nil); err == nil {
+			t.Error("SignMessage.Sign() error = nil, wantErr true")
+		}
+	})
+}
+
+func TestSignMessage_Verify(t *testing.T) {
+	// generate key and set up signer / verifier
+	gen := func(alg Algorithm) (Signer, Verifier) {
+		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)
+		}
+		return signer, verifier
+	}
+	algorithms := []Algorithm{AlgorithmES256, AlgorithmES512}
+	signers := make([]Signer, 2)
+	verifiers := make([]Verifier, 2)
+	for i, alg := range algorithms {
+		signers[i], verifiers[i] = gen(alg)
+	}
+
+	// sign / verify round trip
+	tests := []struct {
+		name             string
+		externalOnSign   []byte
+		externalOnVerify []byte
+		verifiers        []Verifier
+		tamper           func(m *SignMessage) *SignMessage
+		wantErr          bool
+	}{
+		{
+			name:      "round trip on valid message",
+			verifiers: verifiers,
+		},
+		{
+			name:             "external mismatch",
+			externalOnSign:   []byte("foo"),
+			externalOnVerify: []byte("bar"),
+			verifiers:        verifiers,
+			wantErr:          true,
+		},
+		{
+			name:             "mixed nil / empty external",
+			externalOnSign:   nil,
+			externalOnVerify: []byte{},
+			verifiers:        verifiers,
+		},
+		{
+			name:      "nil message",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				return nil
+			},
+			wantErr: true,
+		},
+		{
+			name:      "strip signatures",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Signatures = nil
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "empty signatures",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Signatures = []*Signature{}
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "tamper protected header",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Headers.Protected["foo"] = "bar"
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "tamper unprotected header",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Headers.Unprotected["foo"] = "bar"
+				return m
+			},
+			wantErr: false, // allowed
+		},
+		{
+			name:      "tamper payload",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Payload = []byte("foobar")
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "tamper signature",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Signatures[1].Signature[0]++
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "no verifiers",
+			verifiers: nil,
+			wantErr:   true,
+		},
+
+		{
+			name:      "too few verifiers",
+			verifiers: verifiers[:1],
+			wantErr:   true,
+		},
+		{
+			name:      "too many verifiers",
+			verifiers: verifiers,
+			tamper: func(m *SignMessage) *SignMessage {
+				m.Signatures = m.Signatures[:1]
+				return m
+			},
+			wantErr: true,
+		},
+		{
+			name:      "verifier mismatch",
+			verifiers: []Verifier{verifiers[1], verifiers[0]},
+			wantErr:   true,
+		},
+	}
+	for _, tt := range tests {
+		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",
+					},
+				},
+				Payload: []byte("hello world"),
+				Signatures: []*Signature{
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES256,
+							},
+							Unprotected: UnprotectedHeader{
+								HeaderLabelKeyID: 42,
+							},
+						},
+					},
+					{
+						Headers: Headers{
+							Protected: ProtectedHeader{
+								HeaderLabelAlgorithm: AlgorithmES512,
+							},
+						},
+					},
+				},
+			}
+			if err := msg.Sign(rand.Reader, tt.externalOnSign, signers...); err != nil {
+				t.Errorf("SignMessage.Sign() error = %v", err)
+				return
+			}
+
+			// tamper message
+			if tt.tamper != nil {
+				msg = tt.tamper(msg)
+			}
+
+			// verify message
+			if err := msg.Verify(tt.externalOnVerify, tt.verifiers...); (err != nil) != tt.wantErr {
+				t.Errorf("SignMessage.Verify() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+
+	// special cases
+	t.Run("nil payload", func(t *testing.T) { // payload is detached
+		msg := &SignMessage{
+			Headers: Headers{
+				Protected: ProtectedHeader{
+					HeaderLabelContentType: "text/plain",
+				},
+				Unprotected: UnprotectedHeader{
+					"extra": "test",
+				},
+			},
+			Payload: []byte{},
+			Signatures: []*Signature{
+				{
+					Headers: Headers{
+						Protected: ProtectedHeader{
+							HeaderLabelAlgorithm: AlgorithmES256,
+						},
+						Unprotected: UnprotectedHeader{
+							HeaderLabelKeyID: 42,
+						},
+					},
+				},
+				{
+					Headers: Headers{
+						Protected: ProtectedHeader{
+							HeaderLabelAlgorithm: AlgorithmES512,
+						},
+					},
+				},
+			},
+		}
+		if err := msg.Sign(rand.Reader, nil, signers...); err != nil {
+			t.Errorf("SignMessage.Sign() error = %v", err)
+			return
+		}
+
+		// make payload nil on verify
+		msg.Payload = nil
+
+		// verify message
+		if err := msg.Verify(nil, verifiers...); err == nil {
+			t.Error("SignMessage.Verify() error = nil, wantErr true")
+		}
+	})
+}
diff --git a/signer.go b/signer.go
new file mode 100644
index 0000000..4cd1977
--- /dev/null
+++ b/signer.go
@@ -0,0 +1,80 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"errors"
+	"fmt"
+	"io"
+)
+
+// Signer is an interface for private keys to sign COSE signatures.
+type Signer interface {
+	// Algorithm returns the signing algorithm associated with the private key.
+	Algorithm() Algorithm
+
+	// Sign signs digest with the private key, possibly using entropy from rand.
+	// The resulting signature should follow RFC 8152 section 8.
+	//
+	// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8
+	Sign(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.
+//
+// Developers are encouraged to implement the `cose.Signer` interface instead of
+// the `crypto.Signer` interface for better performance.
+//
+// All signing keys implementing `crypto.Signer“ with `Public()` outputing a
+// public key of type `*rsa.PublicKey`, `*ecdsa.PublicKey`, or
+// `ed25519.PublicKey` are accepted.
+//
+// Note: `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, and `ed25519.PrivateKey`
+// implement `crypto.Signer`.
+func NewSigner(alg Algorithm, key crypto.Signer) (Signer, error) {
+	switch alg {
+	case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512:
+		vk, ok := key.Public().(*rsa.PublicKey)
+		if !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		// RFC 8230 6.1 requires RSA keys having a minimun size of 2048 bits.
+		// Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-6.1
+		if vk.N.BitLen() < 2048 {
+			return nil, errors.New("RSA key must be at least 2048 bits long")
+		}
+		return &rsaSigner{
+			alg: alg,
+			key: key,
+		}, nil
+	case AlgorithmES256, AlgorithmES384, AlgorithmES512:
+		vk, ok := key.Public().(*ecdsa.PublicKey)
+		if !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		if sk, ok := key.(*ecdsa.PrivateKey); ok {
+			return &ecdsaKeySigner{
+				alg: alg,
+				key: sk,
+			}, nil
+		}
+		return &ecdsaCryptoSigner{
+			alg:    alg,
+			key:    vk,
+			signer: key,
+		}, nil
+	case AlgorithmEd25519:
+		if _, ok := key.Public().(ed25519.PublicKey); !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		return &ed25519Signer{
+			key: key,
+		}, nil
+	default:
+		return nil, ErrAlgorithmNotSupported
+	}
+}
diff --git a/signer_test.go b/signer_test.go
new file mode 100644
index 0000000..1e28a0b
--- /dev/null
+++ b/signer_test.go
@@ -0,0 +1,168 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"encoding/hex"
+	"io"
+	"reflect"
+	"testing"
+)
+
+func signTestData(t *testing.T, alg Algorithm, key crypto.Signer) (digest, sig []byte) {
+	signer, err := NewSigner(alg, key)
+	if err != nil {
+		t.Fatalf("NewSigner() error = %v", err)
+	}
+	digest, err = alg.computeHash([]byte("hello world"))
+	if err != nil {
+		t.Fatalf("Algorithm.computeHash() error = %v", err)
+	}
+	sig, err = signer.Sign(rand.Reader, digest)
+	if err != nil {
+		t.Fatalf("Sign() error = %v", err)
+	}
+	return
+}
+
+func TestNewSigner(t *testing.T) {
+	// generate ecdsa key
+	ecdsaKey := generateTestECDSAKey(t)
+	ecdsaWrappedKey := struct {
+		crypto.Signer
+	}{
+		Signer: ecdsaKey,
+	}
+
+	// generate ed25519 key
+	_, ed25519Key := generateTestEd25519Key(t)
+
+	// generate rsa keys
+	rsaKey := generateTestRSAKey(t)
+	rsaKeyLowEntropy, err := rsa.GenerateKey(rand.Reader, 1024)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey() error = %v", err)
+	}
+
+	// run tests
+	tests := []struct {
+		name    string
+		alg     Algorithm
+		key     crypto.Signer
+		want    Signer
+		wantErr bool
+	}{
+		{
+			name: "ecdsa key signer",
+			alg:  AlgorithmES256,
+			key:  ecdsaKey,
+			want: &ecdsaKeySigner{
+				alg: AlgorithmES256,
+				key: ecdsaKey,
+			},
+		},
+		{
+			name: "ecdsa crypto signer",
+			alg:  AlgorithmES256,
+			key:  ecdsaWrappedKey,
+			want: &ecdsaCryptoSigner{
+				alg:    AlgorithmES256,
+				key:    &ecdsaKey.PublicKey,
+				signer: ecdsaWrappedKey,
+			},
+		},
+		{
+			name:    "ecdsa key mismatch",
+			alg:     AlgorithmES256,
+			key:     rsaKey,
+			wantErr: true,
+		},
+		{
+			name: "ed25519 signer",
+			alg:  AlgorithmEd25519,
+			key:  ed25519Key,
+			want: &ed25519Signer{
+				key: ed25519Key,
+			},
+		},
+		{
+			name:    "ed25519 key mismatch",
+			alg:     AlgorithmEd25519,
+			key:     rsaKey,
+			wantErr: true,
+		},
+		{
+			name: "rsa signer",
+			alg:  AlgorithmPS256,
+			key:  rsaKey,
+			want: &rsaSigner{
+				alg: AlgorithmPS256,
+				key: rsaKey,
+			},
+		},
+		{
+			name:    "rsa key mismatch",
+			alg:     AlgorithmPS256,
+			key:     ecdsaKey,
+			wantErr: true,
+		},
+		{
+			name:    "rsa key under minimum entropy",
+			alg:     AlgorithmPS256,
+			key:     rsaKeyLowEntropy,
+			wantErr: true,
+		},
+		{
+			name:    "unknown algorithm",
+			alg:     0,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := NewSigner(tt.alg, tt.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NewSigner() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+const algorithmMock Algorithm = -0x6d6f636b
+
+type mockSigner struct {
+	t *testing.T
+	m map[string]string
+}
+
+func newMockSigner(t *testing.T) *mockSigner {
+	return &mockSigner{
+		t: t,
+		m: make(map[string]string),
+	}
+}
+
+func (m *mockSigner) setup(digest, sig []byte) {
+	m.m[hex.EncodeToString(digest)] = hex.EncodeToString(sig) // deep copy
+}
+
+func (m *mockSigner) Algorithm() Algorithm {
+	return algorithmMock
+}
+
+func (m *mockSigner) Sign(rand io.Reader, digest []byte) ([]byte, error) {
+	sigHex, ok := m.m[hex.EncodeToString(digest)]
+	if !ok {
+		m.t.Fatalf("mockSigner: not setup: %v", digest)
+	}
+	sig, err := hex.DecodeString(sigHex)
+	if err != nil {
+		m.t.Fatalf("mockSigner: failed to decode: %v", sigHex)
+	}
+	return sig, nil
+}
diff --git a/testdata/sign1-sign-0000.json b/testdata/sign1-sign-0000.json
new file mode 100644
index 0000000..cee5cb5
--- /dev/null
+++ b/testdata/sign1-sign-0000.json
@@ -0,0 +1,35 @@
+{
+  "uuid": "D55A49BD-53D9-42B1-9E76-E0CF2AD33E9D",
+  "title": "Sign1 w/ external input - ECDSA w/ SHA-256 (sign)",
+  "description": "Sign with one signer using ECDSA w/ SHA-256 supplying external input",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
+    "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
+  },
+  "alg": "ES256",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a10126",
+      "cborDiag": "{1: -7}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104423131",
+      "cborDiag": "{4: '11'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653143a101264c11aa22bb33cc44dd5500669954546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A10126', h'11AA22BB33CC44DD55006699', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "external": "11aa22bb33cc44dd55006699",
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28443a10126a10442313154546869732069732074686520636f6e74656e742e58403a7487d9a528cb61dd8e99bd652c12577fc47d70ee5af2e703c420584f060fc7a8d61e4a35862b2b531a8447030ab966aeed8dd45ebc507c761431e349995770",
+      "cborDiag": "18([h'A10126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'3A7487D9A528CB61DD8E99BD652C12577FC47D70EE5AF2E703C420584F060FC7A8D61E4A35862B2B531A8447030AB966AEED8DD45EBC507C761431E349995770'])"
+    },
+    "fixedOutputLength": 32
+  }
+}
diff --git a/testdata/sign1-sign-0001.json b/testdata/sign1-sign-0001.json
new file mode 100644
index 0000000..4c2ca63
--- /dev/null
+++ b/testdata/sign1-sign-0001.json
@@ -0,0 +1,34 @@
+{
+  "uuid": "0F78DB1C-C30F-47B1-AF19-6D0C0B2F3803",
+  "title": "Sign1 - ECDSA w/ SHA-256 (sign)",
+  "description": "Sign with one signer using ECDSA w/ SHA-256",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
+    "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
+  },
+  "alg": "ES256",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a201260300",
+      "cborDiag": "{1: -7, 3: 0}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104423131",
+      "cborDiag": "{4: '11'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653145a2012603004054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A201260300', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28445a201260300a10442313154546869732069732074686520636f6e74656e742e58402ad3b9dcc1e13d04f357e11cc8acd825196620e62f0d8deca72672508b829d90e07a3f23be6aa36fd6ebd31e2ed08d1760bffd981f991bfc94a45199a54875c4",
+      "cborDiag": "18([h'A201260300', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'2AD3B9DCC1E13D04F357E11CC8ACD825196620E62F0D8DECA72672508B829D90E07A3F23BE6AA36FD6EBD31E2ED08D1760BFFD981F991BFC94A45199A54875C4'])"
+    },
+    "fixedOutputLength": 34
+  }
+}
diff --git a/testdata/sign1-sign-0002.json b/testdata/sign1-sign-0002.json
new file mode 100644
index 0000000..2183ad8
--- /dev/null
+++ b/testdata/sign1-sign-0002.json
@@ -0,0 +1,35 @@
+{
+  "uuid": "E693D0C8-C702-4E6C-A70D-0D4DA4C408A0",
+  "title": "Sign1 - ECDSA w/ SHA-384 (sign)",
+  "description": "Sign with one signer using ECDSA w/ SHA-384",
+  "key": {
+    "kty": "EC",
+    "kid": "P384",
+    "crv": "P-384",
+    "x": "kTJyP2KSsBBhnb4kjWmMF7WHVsY55xUPgb7k64rDcjatChoZ1nvjKmYmPh5STRKc",
+    "y": "mM0weMVU2DKsYDxDJkEP9hZiRZtB8fPfXbzINZj_fF7YQRynNWedHEyzAJOX2e8s",
+    "d": "ok3Nq97AXlpEusO7jIy1FZATlBP9PNReMU7DWbkLQ5dU90snHuuHVDjEPmtV0fTo"
+  },
+  "alg": "ES384",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a1013822",
+      "cborDiag": "{1: -35}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a1044450333834",
+      "cborDiag": "{4: 'P384'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653144a10138224054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A1013822', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28444a1013822a104445033383454546869732069732074686520636f6e74656e742e5860aa46c1ab71cd3c1e68ed62c27653797cb72cba3a856fd5e2f38794eee0d666e88139ec51fb62466f4865ca56df493905911e329e829c1887f6259681360a8e7f7d3fd080dcb0720066f13e1621656700c99d6e3771ac2549fde998ee9b1e2cad",
+      "cborDiag": "18([h'A1013822', {4: 'P384'}, h'546869732069732074686520636F6E74656E742E', h'AA46C1AB71CD3C1E68ED62C27653797CB72CBA3A856FD5E2F38794EEE0D666E88139EC51FB62466F4865CA56DF493905911E329E829C1887F6259681360A8E7F7D3FD080DCB0720066F13E1621656700C99D6E3771AC2549FDE998EE9B1E2CAD'])"
+    },
+    "fixedOutputLength": 35
+  }
+}
diff --git a/testdata/sign1-sign-0003.json b/testdata/sign1-sign-0003.json
new file mode 100644
index 0000000..463136d
--- /dev/null
+++ b/testdata/sign1-sign-0003.json
@@ -0,0 +1,36 @@
+{
+  "uuid": "06EFA821-9026-4CDD-A4FB-634103472BC3",
+  "title": "Sign1 - ECDSA w/ SHA-512 (sign)",
+  "description": "Sign with one signer using ECDSA w/ SHA-512",
+  "key": {
+    "kty": "EC",
+    "kid": "bilbo.baggins@hobbiton.example",
+    "use": "sig",
+    "crv": "P-521",
+    "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
+    "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1",
+    "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
+  },
+  "alg": "ES512",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a1013823",
+      "cborDiag": "{1: -36}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+      "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653144a10138234054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A1013823', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28444a1013823a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e58840128bbda237a1b55568da74cefe02cf2d2a6216f80ac757bea8effc056d2f634f6e257077b0dabe9d4b3689eb8228e20f60bc74ff84ae3a38ee9a69e158cbf80f93a017acf5877e5083548a45143b602ccd776c5eb39537a2e68dc8c47ff62e10fc42f045b781e4313fbf421903785c3dfeb181c3a93b46a67a9b0e82947ee83f7b44cf0",
+      "cborDiag": "18([h'A1013823', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'0128BBDA237A1B55568DA74CEFE02CF2D2A6216F80AC757BEA8EFFC056D2F634F6E257077B0DABE9D4B3689EB8228E20F60BC74FF84AE3A38EE9A69E158CBF80F93A017ACF5877E5083548A45143B602CCD776C5EB39537A2E68DC8C47FF62E10FC42F045B781E4313FBF421903785C3DFEB181C3A93B46A67A9B0E82947EE83F7B44CF0'])"
+    },
+    "fixedOutputLength": 62
+  }
+}
diff --git a/testdata/sign1-sign-0004.json b/testdata/sign1-sign-0004.json
new file mode 100644
index 0000000..6544727
--- /dev/null
+++ b/testdata/sign1-sign-0004.json
@@ -0,0 +1,39 @@
+{
+  "uuid": "D269214C-8717-486F-B1CF-65A0775CB5A3",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-256 (sign)",
+  "description": "Sign with one signer using RSASSA-PSS w/ SHA-256",
+  "key": {
+    "kty": "RSA",
+    "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ",
+    "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE",
+    "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E",
+    "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8",
+    "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS256",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a1013824",
+      "cborDiag": "{1: -37}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+      "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653144a10138244054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A1013824', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28444a1013824a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100788ef717e37b7a127010f87d458be5788151a734aeaf1ef52a8f7ac40d05daf1f5dc575b29c0bf0e1d326cfcae1af5dcf62b6dc26eb18a35456fd7c3477774d2c664babc5db7618309dc2ba38392c3dba61f0d27974ea5d4df329060334b8bbd9d8f41cc090913cb2cd4470ae2c8560173793e703d2dda52a4e804ececd57db5ba30200b4d0939f89adc1f13f829e516625109684b429a088e0a2766564cfd2ee1a1acc4f20ad981e4bc27e427d754481ca93ee16a7677e24cdd31d03b44fe3260ee35bbb5b57c1264af6ca879f67b39b2680d920252e37b916e970baa4762c65cc4ea071b2ab65c5c992518597c7b7fbf9608564c5c2c3caae9449d46f8df32",
+      "cborDiag": "18([h'A1013824', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'788EF717E37B7A127010F87D458BE5788151A734AEAF1EF52A8F7AC40D05DAF1F5DC575B29C0BF0E1D326CFCAE1AF5DCF62B6DC26EB18A35456FD7C3477774D2C664BABC5DB7618309DC2BA38392C3DBA61F0D27974EA5D4DF329060334B8BBD9D8F41CC090913CB2CD4470AE2C8560173793E703D2DDA52A4E804ECECD57DB5BA30200B4D0939F89ADC1F13F829E516625109684B429A088E0A2766564CFD2EE1A1ACC4F20AD981E4BC27E427D754481CA93EE16A7677E24CDD31D03B44FE3260EE35BBB5B57C1264AF6CA879F67B39B2680D920252E37B916E970BAA4762C65CC4EA071B2AB65C5C992518597C7B7FBF9608564C5C2C3CAAE9449D46F8DF32'])"
+    },
+    "fixedOutputLength": 65
+  }
+}
diff --git a/testdata/sign1-sign-0005.json b/testdata/sign1-sign-0005.json
new file mode 100644
index 0000000..99980a4
--- /dev/null
+++ b/testdata/sign1-sign-0005.json
@@ -0,0 +1,39 @@
+{
+  "uuid": "9701444D-EC51-4ED6-859D-D5E2D4C441A8",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-384 (sign)",
+  "description": "Sign with one signer using RSASSA-PSS w/ SHA-384",
+  "key": {
+    "kty": "RSA",
+    "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ",
+    "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE",
+    "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E",
+    "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8",
+    "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS384",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a1013825",
+      "cborDiag": "{1: -38}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+      "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653144a10138254054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A1013825', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28444a1013825a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e5901000c4b39bc5dfa27903ccfecb750f13d761cfe213a64eefd63d226281388727284622838431d9d312022f2a2e76270f60e7c3200df2a220f1a4abbc5b130f4aa93066d58ff5326bb4a62328f4b739b105e9ba3ff76b6397024582c10e948a6907154c3f7b8533c845f569c9d7ec0506194e6ae3d792da162fb396d6417510c0dd27f9519161d152ddb9444c20f8aff3b44816f05d7ab44cc00dfdeda4941a7ef4fe56007c1139f17f1b86cb3f00bfce015aa5033ecc92f1b4ca9077aea11007dd0ab309245f549452153a94ce2e2fb3e389023a03d07d31c772126f0817bb7248d43cec2347b377827242b4d41b494798b79c0a5f677b69a90d00d0854c812deb0",
+      "cborDiag": "18([h'A1013825', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'0C4B39BC5DFA27903CCFECB750F13D761CFE213A64EEFD63D226281388727284622838431D9D312022F2A2E76270F60E7C3200DF2A220F1A4ABBC5B130F4AA93066D58FF5326BB4A62328F4B739B105E9BA3FF76B6397024582C10E948A6907154C3F7B8533C845F569C9D7EC0506194E6AE3D792DA162FB396D6417510C0DD27F9519161D152DDB9444C20F8AFF3B44816F05D7AB44CC00DFDEDA4941A7EF4FE56007C1139F17F1B86CB3F00BFCE015AA5033ECC92F1B4CA9077AEA11007DD0AB309245F549452153A94CE2E2FB3E389023A03D07D31C772126F0817BB7248D43CEC2347B377827242B4D41B494798B79C0A5F677B69A90D00D0854C812DEB0'])"
+    },
+    "fixedOutputLength": 65
+  }
+}
diff --git a/testdata/sign1-sign-0006.json b/testdata/sign1-sign-0006.json
new file mode 100644
index 0000000..77ed781
--- /dev/null
+++ b/testdata/sign1-sign-0006.json
@@ -0,0 +1,39 @@
+{
+  "uuid": "D669DEF6-6890-4C18-A529-1F3432127D18",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-512 (sign)",
+  "description": "Sign with one signer using RSASSA-PSS w/ SHA-512",
+  "key": {
+    "kty": "RSA",
+    "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ",
+    "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE",
+    "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E",
+    "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8",
+    "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS512",
+  "sign1::sign": {
+    "payload": "546869732069732074686520636f6e74656e742e",
+    "protectedHeaders": {
+      "cborHex": "a1013826",
+      "cborDiag": "{1: -39}"
+    },
+    "unprotectedHeaders": {
+      "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+      "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}"
+    },
+    "tbsHex": {
+      "cborHex": "846a5369676e61747572653144a10138264054546869732069732074686520636f6e74656e742e",
+      "cborDiag": "[\"Signature1\", h'A1013826', h'', h'546869732069732074686520636F6E74656E742E']"
+    },
+    "detached": false,
+    "expectedOutput": {
+      "cborHex": "d28444a1013826a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100d154da53f040ca145f6db8ee0bbcf0644df969313ed5f9cfc927a594d14b59001bfa7b0a4676f96ed0906cf4d4b92f3b622d6dc1946686181f6bd43a5f74f800cb9fbba5a431d19ac23a8a6c0efe89756ce6b19a20fabf1df8a0ee55164b0ea449e25f38190c470953e878dd531e8c5d1f5a7d7ec9fc79bb49b1eff605aae6254a4dc4757909730ead4dccd5cab49595270cfb31f6263f068a377144e1663504531cd908ce7810df0b53fd2e12f3bdfbb294680c7737b36e2de1a2b2f212996469306c74955cd7d08379a2655572d8845d542b30f7b4dda2e9438c96fe392f391ba5dd1f2a2487e993daab173da0c33c2dcdf5b173b32cebde71ca0e3a616e23",
+      "cborDiag": "18([h'A1013826', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'D154DA53F040CA145F6DB8EE0BBCF0644DF969313ED5F9CFC927A594D14B59001BFA7B0A4676F96ED0906CF4D4B92F3B622D6DC1946686181F6BD43A5F74F800CB9FBBA5A431D19AC23A8A6C0EFE89756CE6B19A20FABF1DF8A0EE55164B0EA449E25F38190C470953E878DD531E8C5D1F5A7D7EC9FC79BB49B1EFF605AAE6254A4DC4757909730EAD4DCCD5CAB49595270CFB31F6263F068A377144E1663504531CD908CE7810DF0B53FD2E12F3BDFBB294680C7737B36E2DE1A2B2F212996469306C74955CD7D08379A2655572D8845D542B30F7B4DDA2E9438C96FE392F391BA5DD1F2A2487E993DAAB173DA0C33C2DCDF5B173B32CEBDE71CA0E3A616E23'])"
+    },
+    "fixedOutputLength": 65
+  }
+}
diff --git a/testdata/sign1-verify-0000.json b/testdata/sign1-verify-0000.json
new file mode 100644
index 0000000..6405093
--- /dev/null
+++ b/testdata/sign1-verify-0000.json
@@ -0,0 +1,20 @@
+{
+  "uuid": "66584A57-390B-4A52-B7B6-B7CA4FC4204F",
+  "title": "Sign1 w/ external input - ECDSA w/ SHA-256 (verify)",
+  "description": "Verify signature with one signer using ECDSA w/ SHA-256 supplying external input",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28443a10126a10442313154546869732069732074686520636f6e74656e742e58403a7487d9a528cb61dd8e99bd652c12577fc47d70ee5af2e703c420584f060fc7a8d61e4a35862b2b531a8447030ab966aeed8dd45ebc507c761431e349995770",
+      "cborDiag": "18([h'A10126', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'3A7487D9A528CB61DD8E99BD652C12577FC47D70EE5AF2E703C420584F060FC7A8D61E4A35862B2B531A8447030AB966AEED8DD45EBC507C761431E349995770'])"
+    },
+    "external": "11aa22bb33cc44dd55006699",
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0001.json b/testdata/sign1-verify-0001.json
new file mode 100644
index 0000000..5096036
--- /dev/null
+++ b/testdata/sign1-verify-0001.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "2AF74107-34AB-4DD5-BC3C-E83895CAE1A4",
+  "title": "Sign1 - ECDSA w/ SHA-256 (verify)",
+  "description": "Verify signature with one signer using ECDSA w/ SHA-256",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28445a201260300a10442313154546869732069732074686520636f6e74656e742e58402ad3b9dcc1e13d04f357e11cc8acd825196620e62f0d8deca72672508b829d90e07a3f23be6aa36fd6ebd31e2ed08d1760bffd981f991bfc94a45199a54875c4",
+      "cborDiag": "18([h'A201260300', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'2AD3B9DCC1E13D04F357E11CC8ACD825196620E62F0D8DECA72672508B829D90E07A3F23BE6AA36FD6EBD31E2ED08D1760BFFD981F991BFC94A45199A54875C4'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0002.json b/testdata/sign1-verify-0002.json
new file mode 100644
index 0000000..7836b4b
--- /dev/null
+++ b/testdata/sign1-verify-0002.json
@@ -0,0 +1,20 @@
+{
+  "uuid": "C5763BDB-5A23-4E9E-9AA2-463A8B107033",
+  "title": "Sign1 - ECDSA w/ SHA-384 (verify)",
+  "description": "Verify signature with one signer using ECDSA w/ SHA-384",
+  "key": {
+    "kty": "EC",
+    "kid": "P384",
+    "crv": "P-384",
+    "x": "kTJyP2KSsBBhnb4kjWmMF7WHVsY55xUPgb7k64rDcjatChoZ1nvjKmYmPh5STRKc",
+    "y": "mM0weMVU2DKsYDxDJkEP9hZiRZtB8fPfXbzINZj_fF7YQRynNWedHEyzAJOX2e8s"
+  },
+  "alg": "ES384",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28444a1013822a104445033383454546869732069732074686520636f6e74656e742e5860aa46c1ab71cd3c1e68ed62c27653797cb72cba3a856fd5e2f38794eee0d666e88139ec51fb62466f4865ca56df493905911e329e829c1887f6259681360a8e7f7d3fd080dcb0720066f13e1621656700c99d6e3771ac2549fde998ee9b1e2cad",
+      "cborDiag": "18([h'A1013822', {4: 'P384'}, h'546869732069732074686520636F6E74656E742E', h'AA46C1AB71CD3C1E68ED62C27653797CB72CBA3A856FD5E2F38794EEE0D666E88139EC51FB62466F4865CA56DF493905911E329E829C1887F6259681360A8E7F7D3FD080DCB0720066F13E1621656700C99D6E3771AC2549FDE998EE9B1E2CAD'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0003.json b/testdata/sign1-verify-0003.json
new file mode 100644
index 0000000..cef5320
--- /dev/null
+++ b/testdata/sign1-verify-0003.json
@@ -0,0 +1,21 @@
+{
+  "uuid": "5F6E65B5-1E2F-4242-87CE-C76E26E927D8",
+  "title": "Sign1 - ECDSA w/ SHA-512 (verify)",
+  "description": "Verify signature with one signer using ECDSA w/ SHA-512",
+  "key": {
+    "kty": "EC",
+    "kid": "bilbo.baggins@hobbiton.example",
+    "use": "sig",
+    "crv": "P-521",
+    "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
+    "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
+  },
+  "alg": "ES512",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28444a1013823a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e58840128bbda237a1b55568da74cefe02cf2d2a6216f80ac757bea8effc056d2f634f6e257077b0dabe9d4b3689eb8228e20f60bc74ff84ae3a38ee9a69e158cbf80f93a017acf5877e5083548a45143b602ccd776c5eb39537a2e68dc8c47ff62e10fc42f045b781e4313fbf421903785c3dfeb181c3a93b46a67a9b0e82947ee83f7b44cf0",
+      "cborDiag": "18([h'A1013823', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'0128BBDA237A1B55568DA74CEFE02CF2D2A6216F80AC757BEA8EFFC056D2F634F6E257077B0DABE9D4B3689EB8228E20F60BC74FF84AE3A38EE9A69E158CBF80F93A017ACF5877E5083548A45143B602CCD776C5EB39537A2E68DC8C47FF62E10FC42F045B781E4313FBF421903785C3DFEB181C3A93B46A67A9B0E82947EE83F7B44CF0'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0004.json b/testdata/sign1-verify-0004.json
new file mode 100644
index 0000000..6f13b82
--- /dev/null
+++ b/testdata/sign1-verify-0004.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "842E5FF9-74A4-45D4-B00F-F091925CF4F3",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-256 (verify)",
+  "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-256",
+  "key": {
+    "kty": "RSA",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28444a1013824a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100788ef717e37b7a127010f87d458be5788151a734aeaf1ef52a8f7ac40d05daf1f5dc575b29c0bf0e1d326cfcae1af5dcf62b6dc26eb18a35456fd7c3477774d2c664babc5db7618309dc2ba38392c3dba61f0d27974ea5d4df329060334b8bbd9d8f41cc090913cb2cd4470ae2c8560173793e703d2dda52a4e804ececd57db5ba30200b4d0939f89adc1f13f829e516625109684b429a088e0a2766564cfd2ee1a1acc4f20ad981e4bc27e427d754481ca93ee16a7677e24cdd31d03b44fe3260ee35bbb5b57c1264af6ca879f67b39b2680d920252e37b916e970baa4762c65cc4ea071b2ab65c5c992518597c7b7fbf9608564c5c2c3caae9449d46f8df32",
+      "cborDiag": "18([h'A1013824', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'788EF717E37B7A127010F87D458BE5788151A734AEAF1EF52A8F7AC40D05DAF1F5DC575B29C0BF0E1D326CFCAE1AF5DCF62B6DC26EB18A35456FD7C3477774D2C664BABC5DB7618309DC2BA38392C3DBA61F0D27974EA5D4DF329060334B8BBD9D8F41CC090913CB2CD4470AE2C8560173793E703D2DDA52A4E804ECECD57DB5BA30200B4D0939F89ADC1F13F829E516625109684B429A088E0A2766564CFD2EE1A1ACC4F20AD981E4BC27E427D754481CA93EE16A7677E24CDD31D03B44FE3260EE35BBB5B57C1264AF6CA879F67B39B2680D920252E37B916E970BAA4762C65CC4EA071B2AB65C5C992518597C7B7FBF9608564C5C2C3CAAE9449D46F8DF32'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0005.json b/testdata/sign1-verify-0005.json
new file mode 100644
index 0000000..dc0596b
--- /dev/null
+++ b/testdata/sign1-verify-0005.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "98C2AB20-415D-4190-96DD-868EF3998AA1",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-384 (verify)",
+  "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-384",
+  "key": {
+    "kty": "RSA",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS384",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28444a1013825a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e5901000c4b39bc5dfa27903ccfecb750f13d761cfe213a64eefd63d226281388727284622838431d9d312022f2a2e76270f60e7c3200df2a220f1a4abbc5b130f4aa93066d58ff5326bb4a62328f4b739b105e9ba3ff76b6397024582c10e948a6907154c3f7b8533c845f569c9d7ec0506194e6ae3d792da162fb396d6417510c0dd27f9519161d152ddb9444c20f8aff3b44816f05d7ab44cc00dfdeda4941a7ef4fe56007c1139f17f1b86cb3f00bfce015aa5033ecc92f1b4ca9077aea11007dd0ab309245f549452153a94ce2e2fb3e389023a03d07d31c772126f0817bb7248d43cec2347b377827242b4d41b494798b79c0a5f677b69a90d00d0854c812deb0",
+      "cborDiag": "18([h'A1013825', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'0C4B39BC5DFA27903CCFECB750F13D761CFE213A64EEFD63D226281388727284622838431D9D312022F2A2E76270F60E7C3200DF2A220F1A4ABBC5B130F4AA93066D58FF5326BB4A62328F4B739B105E9BA3FF76B6397024582C10E948A6907154C3F7B8533C845F569C9D7EC0506194E6AE3D792DA162FB396D6417510C0DD27F9519161D152DDB9444C20F8AFF3B44816F05D7AB44CC00DFDEDA4941A7EF4FE56007C1139F17F1B86CB3F00BFCE015AA5033ECC92F1B4CA9077AEA11007DD0AB309245F549452153A94CE2E2FB3E389023A03D07D31C772126F0817BB7248D43CEC2347B377827242B4D41B494798B79C0A5F677B69A90D00D0854C812DEB0'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-0006.json b/testdata/sign1-verify-0006.json
new file mode 100644
index 0000000..f14ed52
--- /dev/null
+++ b/testdata/sign1-verify-0006.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "A2D89260-B966-4FE4-8931-959BAD58126C",
+  "title": "Sign1 - RSASSA-PSS w/ SHA-512 (verify)",
+  "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-512",
+  "key": {
+    "kty": "RSA",
+    "e": "AQAB",
+    "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w",
+    "kid": "meriadoc.brandybuck@rsa.example"
+  },
+  "alg": "PS512",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28444a1013826a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100d154da53f040ca145f6db8ee0bbcf0644df969313ed5f9cfc927a594d14b59001bfa7b0a4676f96ed0906cf4d4b92f3b622d6dc1946686181f6bd43a5f74f800cb9fbba5a431d19ac23a8a6c0efe89756ce6b19a20fabf1df8a0ee55164b0ea449e25f38190c470953e878dd531e8c5d1f5a7d7ec9fc79bb49b1eff605aae6254a4dc4757909730ead4dccd5cab49595270cfb31f6263f068a377144e1663504531cd908ce7810df0b53fd2e12f3bdfbb294680c7737b36e2de1a2b2f212996469306c74955cd7d08379a2655572d8845d542b30f7b4dda2e9438c96fe392f391ba5dd1f2a2487e993daab173da0c33c2dcdf5b173b32cebde71ca0e3a616e23",
+      "cborDiag": "18([h'A1013826', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'D154DA53F040CA145F6DB8EE0BBCF0644DF969313ED5F9CFC927A594D14B59001BFA7B0A4676F96ED0906CF4D4B92F3B622D6DC1946686181F6BD43A5F74F800CB9FBBA5A431D19AC23A8A6C0EFE89756CE6B19A20FABF1DF8A0EE55164B0EA449E25F38190C470953E878DD531E8C5D1F5A7D7EC9FC79BB49B1EFF605AAE6254A4DC4757909730EAD4DCCD5CAB49595270CFB31F6263F068A377144E1663504531CD908CE7810DF0B53FD2E12F3BDFBB294680C7737B36E2DE1A2B2F212996469306C74955CD7D08379A2655572D8845D542B30F7B4DDA2E9438C96FE392F391BA5DD1F2A2487E993DAAB173DA0C33C2DCDF5B173B32CEBDE71CA0E3A616E23'])"
+    },
+    "shouldVerify": true
+  }
+}
diff --git a/testdata/sign1-verify-negative-0000.json b/testdata/sign1-verify-negative-0000.json
new file mode 100644
index 0000000..b0cf905
--- /dev/null
+++ b/testdata/sign1-verify-negative-0000.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "5921EA1A-A39A-462E-B6CE-2680000A79CD",
+  "title": "Protected header outer type is not bstr ",
+  "description": "The protected header is not serialised as bstr, instead it is kept as map, which the Sign1 parser should reject",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d284a10126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+      "cborDiag": "18([{1: -7}, {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])"
+    },
+    "shouldVerify": false
+  }
+}
diff --git a/testdata/sign1-verify-negative-0001.json b/testdata/sign1-verify-negative-0001.json
new file mode 100644
index 0000000..38a7b96
--- /dev/null
+++ b/testdata/sign1-verify-negative-0001.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "10F06D49-9AD3-427C-9496-B7EAD5EE721C",
+  "title": "Protected header inner type is not map",
+  "description": "The protected header is serialised as bstr but the embedded type is not a map, which the Sign1 parser should reject",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28443820126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+      "cborDiag": "18([h'820126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])"
+    },
+    "shouldVerify": false
+  }
+}
diff --git a/testdata/sign1-verify-negative-0002.json b/testdata/sign1-verify-negative-0002.json
new file mode 100644
index 0000000..943183d
--- /dev/null
+++ b/testdata/sign1-verify-negative-0002.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "A658B110-BEA2-4C6D-A9AA-C10BE3643389",
+  "title": "Protected header map contains duplicate entries",
+  "description": "The protected header map contains a duplicate label (alg), which the Sign1 parser should reject",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28445a201260126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+      "cborDiag": "18([h'A201260126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])"
+    },
+    "shouldVerify": false
+  }
+}
diff --git a/testdata/sign1-verify-negative-0003.json b/testdata/sign1-verify-negative-0003.json
new file mode 100644
index 0000000..3439a90
--- /dev/null
+++ b/testdata/sign1-verify-negative-0003.json
@@ -0,0 +1,19 @@
+{
+  "uuid": "13A8356D-5CCF-423E-8EB7-29F8A9EC64D5",
+  "title": "Unprotected header map contains duplicate entries",
+  "description": "The unprotected header map contains a duplicate label (kid), which the Sign1 parser should reject",
+  "key": {
+    "kty": "EC",
+    "crv": "P-256",
+    "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
+    "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"
+  },
+  "alg": "ES256",
+  "sign1::verify": {
+    "taggedCOSESign1": {
+      "cborHex": "d28443a10126a2044231310442313254546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+      "cborDiag": "18([h'A10126', {4: '11', 4: '12'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])"
+    },
+    "shouldVerify": false
+  }
+}
diff --git a/verifier.go b/verifier.go
new file mode 100644
index 0000000..423723f
--- /dev/null
+++ b/verifier.go
@@ -0,0 +1,63 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"errors"
+	"fmt"
+)
+
+// Verifier is an interface for public keys to verify COSE signatures.
+type Verifier interface {
+	// Algorithm returns the signing algorithm associated with the public key.
+	Algorithm() Algorithm
+
+	// Verify verifies digest with the public key, returning nil for success.
+	// Otherwise, it returns ErrVerification.
+	//
+	// Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8
+	Verify(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.
+func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) {
+	switch alg {
+	case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512:
+		vk, ok := key.(*rsa.PublicKey)
+		if !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		// RFC 8230 6.1 requires RSA keys having a minimun size of 2048 bits.
+		// Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-6.1
+		if vk.N.BitLen() < 2048 {
+			return nil, errors.New("RSA key must be at least 2048 bits long")
+		}
+		return &rsaVerifier{
+			alg: alg,
+			key: vk,
+		}, nil
+	case AlgorithmES256, AlgorithmES384, AlgorithmES512:
+		vk, ok := key.(*ecdsa.PublicKey)
+		if !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		return &ecdsaVerifier{
+			alg: alg,
+			key: vk,
+		}, nil
+	case AlgorithmEd25519:
+		vk, ok := key.(ed25519.PublicKey)
+		if !ok {
+			return nil, fmt.Errorf("%v: %w", alg, ErrAlgorithmMismatch)
+		}
+		return &ed25519Verifier{
+			key: vk,
+		}, nil
+	default:
+		return nil, ErrAlgorithmNotSupported
+	}
+}
diff --git a/verifier_test.go b/verifier_test.go
new file mode 100644
index 0000000..88ac960
--- /dev/null
+++ b/verifier_test.go
@@ -0,0 +1,104 @@
+package cose
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/rand"
+	"crypto/rsa"
+	"reflect"
+	"testing"
+)
+
+func TestNewVerifier(t *testing.T) {
+	// generate ecdsa key
+	ecdsaKey := generateTestECDSAKey(t).Public().(*ecdsa.PublicKey)
+
+	// generate ed25519 key
+	ed25519Key, _ := generateTestEd25519Key(t)
+
+	// generate rsa keys
+	rsaKey := generateTestRSAKey(t).Public().(*rsa.PublicKey)
+	var rsaKeyLowEntropy *rsa.PublicKey
+	if key, err := rsa.GenerateKey(rand.Reader, 1024); err != nil {
+		t.Fatalf("rsa.GenerateKey() error = %v", err)
+	} else {
+		rsaKeyLowEntropy = &key.PublicKey
+	}
+
+	// run tests
+	tests := []struct {
+		name    string
+		alg     Algorithm
+		key     crypto.PublicKey
+		want    Verifier
+		wantErr bool
+	}{
+		{
+			name: "ecdsa key verifier",
+			alg:  AlgorithmES256,
+			key:  ecdsaKey,
+			want: &ecdsaVerifier{
+				alg: AlgorithmES256,
+				key: ecdsaKey,
+			},
+		},
+		{
+			name:    "ecdsa key mismatch",
+			alg:     AlgorithmES256,
+			key:     rsaKey,
+			wantErr: true,
+		},
+		{
+			name: "ed25519 verifier",
+			alg:  AlgorithmEd25519,
+			key:  ed25519Key,
+			want: &ed25519Verifier{
+				key: ed25519Key,
+			},
+		},
+		{
+			name:    "ed25519 key mismatch",
+			alg:     AlgorithmEd25519,
+			key:     rsaKey,
+			wantErr: true,
+		},
+		{
+			name: "rsa verifier",
+			alg:  AlgorithmPS256,
+			key:  rsaKey,
+			want: &rsaVerifier{
+				alg: AlgorithmPS256,
+				key: rsaKey,
+			},
+		},
+		{
+			name:    "rsa key mismatch",
+			alg:     AlgorithmPS256,
+			key:     ecdsaKey,
+			wantErr: true,
+		},
+		{
+			name:    "rsa key under minimum entropy",
+			alg:     AlgorithmPS256,
+			key:     rsaKeyLowEntropy,
+			wantErr: true,
+		},
+		{
+			name:    "unknown algorithm",
+			alg:     0,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := NewVerifier(tt.alg, tt.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("NewVerifier() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NewVerifier() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}