summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:37:07 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:37:07 +0000
commit8a63b38fa49e21beba664255a2237fc163d90cf0 (patch)
tree7dc299949d6e8b66736047edf45536a9e29e4203
parentInitial commit. (diff)
downloadgolang-github-sigstore-sigstore-8a63b38fa49e21beba664255a2237fc163d90cf0.tar.xz
golang-github-sigstore-sigstore-8a63b38fa49e21beba664255a2237fc163d90cf0.zip
Adding upstream version 1.8.0.upstream/1.8.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitattributes1
-rw-r--r--.github/dependabot.yml105
-rw-r--r--.github/workflows/codeql.yml50
-rw-r--r--.github/workflows/depsreview.yml28
-rw-r--r--.github/workflows/e2e_test.yml59
-rw-r--r--.github/workflows/fuzzing.yml27
-rw-r--r--.github/workflows/reusable-release.yml75
-rw-r--r--.github/workflows/scorecard.yml31
-rw-r--r--.github/workflows/sync-module-tags.yaml35
-rw-r--r--.github/workflows/test.yml72
-rw-r--r--.github/workflows/verify.yml58
-rw-r--r--.gitignore6
-rw-r--r--.golangci.yml61
-rw-r--r--CODEOWNERS8
-rw-r--r--CODE_OF_CONDUCT.md74
-rw-r--r--COPYRIGHT.txt14
-rw-r--r--LICENSE202
-rw-r--r--Makefile95
-rw-r--r--POLICY.md92
-rw-r--r--README.md27
-rw-r--r--codecov.yml18
-rw-r--r--docs/oid-info.md23
-rw-r--r--go.mod42
-rw-r--r--go.sum208
-rw-r--r--hack/tools/go.mod29
-rw-r--r--hack/tools/go.sum1301
-rw-r--r--hack/tools/tools.go27
-rw-r--r--pkg/cryptoutils/certificate.go173
-rw-r--r--pkg/cryptoutils/certificate_test.go429
-rw-r--r--pkg/cryptoutils/doc.go17
-rw-r--r--pkg/cryptoutils/generic.go31
-rw-r--r--pkg/cryptoutils/generic_test.go34
-rw-r--r--pkg/cryptoutils/password.go94
-rw-r--r--pkg/cryptoutils/privatekey.go152
-rw-r--r--pkg/cryptoutils/privatekey_test.go296
-rw-r--r--pkg/cryptoutils/publickey.go184
-rw-r--r--pkg/cryptoutils/publickey_test.go307
-rw-r--r--pkg/cryptoutils/sans.go149
-rw-r--r--pkg/cryptoutils/sans_test.go230
-rw-r--r--pkg/fulcioroots/doc.go17
-rw-r--r--pkg/fulcioroots/fulcioroots.go133
-rw-r--r--pkg/oauth/doc.go17
-rw-r--r--pkg/oauth/interactive.go80
-rw-r--r--pkg/oauth/internal/doc.go17
-rw-r--r--pkg/oauth/internal/token.go159
-rw-r--r--pkg/oauth/internal/token_test.go231
-rw-r--r--pkg/oauth/oidc/doc.go17
-rw-r--r--pkg/oauth/oidc/interactive.go209
-rw-r--r--pkg/oauth/oidc/interactive_e2e_test.go110
-rw-r--r--pkg/oauth/oidc/pkce.go107
-rw-r--r--pkg/oauth/oidc/pkce_test.go60
-rw-r--r--pkg/oauth/oidc/token.go71
-rw-r--r--pkg/oauthflow/device.go244
-rw-r--r--pkg/oauthflow/device_test.go172
-rw-r--r--pkg/oauthflow/doc.go17
-rw-r--r--pkg/oauthflow/e2e_test.go67
-rw-r--r--pkg/oauthflow/flow.go164
-rw-r--r--pkg/oauthflow/flow_test.go187
-rw-r--r--pkg/oauthflow/interactive.go241
-rw-r--r--pkg/oauthflow/interactive_test.go47
-rw-r--r--pkg/oauthflow/pkce.go109
-rw-r--r--pkg/oauthflow/pkce_test.go61
-rw-r--r--pkg/signature/doc.go17
-rw-r--r--pkg/signature/dsse/adapters.go77
-rw-r--r--pkg/signature/dsse/adapters_test.go32
-rw-r--r--pkg/signature/dsse/doc.go17
-rw-r--r--pkg/signature/dsse/dsse.go152
-rw-r--r--pkg/signature/dsse/dsse_test.go184
-rw-r--r--pkg/signature/dsse/multidsse.go186
-rw-r--r--pkg/signature/ecdsa.go253
-rw-r--r--pkg/signature/ecdsa_test.go136
-rw-r--r--pkg/signature/ed25519.go197
-rw-r--r--pkg/signature/ed25519_test.go88
-rw-r--r--pkg/signature/kms/aws/aws_test.go158
-rw-r--r--pkg/signature/kms/aws/client.go366
-rw-r--r--pkg/signature/kms/aws/doc.go17
-rw-r--r--pkg/signature/kms/aws/e2e_test.go283
-rw-r--r--pkg/signature/kms/aws/go.mod44
-rw-r--r--pkg/signature/kms/aws/go.sum92
-rw-r--r--pkg/signature/kms/aws/signer.go247
-rw-r--r--pkg/signature/kms/azure/README.md76
-rw-r--r--pkg/signature/kms/azure/client.go447
-rw-r--r--pkg/signature/kms/azure/client_test.go409
-rw-r--r--pkg/signature/kms/azure/doc.go17
-rw-r--r--pkg/signature/kms/azure/go.mod39
-rw-r--r--pkg/signature/kms/azure/go.sum97
-rw-r--r--pkg/signature/kms/azure/integration_test.go229
-rw-r--r--pkg/signature/kms/azure/signer.go247
-rw-r--r--pkg/signature/kms/doc.go17
-rw-r--r--pkg/signature/kms/fake/doc.go17
-rw-r--r--pkg/signature/kms/fake/signer.go155
-rw-r--r--pkg/signature/kms/fake/signer_test.go128
-rw-r--r--pkg/signature/kms/gcp/client.go420
-rw-r--r--pkg/signature/kms/gcp/doc.go17
-rw-r--r--pkg/signature/kms/gcp/gcp_test.go110
-rw-r--r--pkg/signature/kms/gcp/go.mod54
-rw-r--r--pkg/signature/kms/gcp/go.sum218
-rw-r--r--pkg/signature/kms/gcp/signer.go195
-rw-r--r--pkg/signature/kms/hashivault/client.go391
-rw-r--r--pkg/signature/kms/hashivault/doc.go17
-rw-r--r--pkg/signature/kms/hashivault/e2e_test.go487
-rw-r--r--pkg/signature/kms/hashivault/go.mod46
-rw-r--r--pkg/signature/kms/hashivault/go.sum124
-rw-r--r--pkg/signature/kms/hashivault/hashivault_test.go56
-rw-r--r--pkg/signature/kms/hashivault/signer.go233
-rw-r--r--pkg/signature/kms/kms.go78
-rw-r--r--pkg/signature/message.go111
-rw-r--r--pkg/signature/options.go57
-rw-r--r--pkg/signature/options/context.go37
-rw-r--r--pkg/signature/options/digest.go35
-rw-r--r--pkg/signature/options/doc.go17
-rw-r--r--pkg/signature/options/keyversion.go50
-rw-r--r--pkg/signature/options/noop.go49
-rw-r--r--pkg/signature/options/rand.go41
-rw-r--r--pkg/signature/options/remoteverification.go32
-rw-r--r--pkg/signature/options/rpcauth.go58
-rw-r--r--pkg/signature/options/signeropts.go40
-rw-r--r--pkg/signature/payload/doc.go17
-rw-r--r--pkg/signature/payload/payload.go122
-rw-r--r--pkg/signature/payload/payload_test.go165
-rw-r--r--pkg/signature/publickey.go25
-rw-r--r--pkg/signature/rsapkcs1v15.go225
-rw-r--r--pkg/signature/rsapkcs1v15_test.go66
-rw-r--r--pkg/signature/rsapss.go260
-rw-r--r--pkg/signature/rsapss_test.go123
-rw-r--r--pkg/signature/signer.go89
-rw-r--r--pkg/signature/signerverifier.go69
-rw-r--r--pkg/signature/ssh/README.md118
-rw-r--r--pkg/signature/ssh/armor.go104
-rw-r--r--pkg/signature/ssh/doc.go17
-rw-r--r--pkg/signature/ssh/sign.go134
-rw-r--r--pkg/signature/ssh/sign_test.go353
-rw-r--r--pkg/signature/ssh/verify.go58
-rw-r--r--pkg/signature/sv_test.go173
-rw-r--r--pkg/signature/util.go55
-rw-r--r--pkg/signature/util_test.go91
-rw-r--r--pkg/signature/verifier.go100
-rw-r--r--pkg/signature/verifier_test.go36
-rw-r--r--pkg/tuf/client.go742
-rw-r--r--pkg/tuf/client_test.go887
-rw-r--r--pkg/tuf/repository/root.json140
-rw-r--r--pkg/tuf/repository/targets/artifact.pub4
-rw-r--r--pkg/tuf/repository/targets/ctfe.pub4
-rw-r--r--pkg/tuf/repository/targets/ctfe_2022.pub4
-rw-r--r--pkg/tuf/repository/targets/fulcio.crt.pem13
-rw-r--r--pkg/tuf/repository/targets/fulcio_intermediate_v1.crt.pem14
-rw-r--r--pkg/tuf/repository/targets/fulcio_v1.crt.pem13
-rw-r--r--pkg/tuf/repository/targets/rekor.pub4
-rw-r--r--pkg/tuf/repository/targets/trusted_root.json114
-rw-r--r--pkg/tuf/status_type.go60
-rw-r--r--pkg/tuf/status_type_test.go84
-rw-r--r--pkg/tuf/testutils.go129
-rw-r--r--pkg/tuf/usage_type.go64
-rw-r--r--pkg/tuf/usage_type_test.go84
-rw-r--r--test/cert_utils.go183
-rw-r--r--test/e2e/dex-config.yml42
-rw-r--r--test/e2e/dexidp.Dockerfile16
-rw-r--r--test/e2e/docker-compose.yml44
-rwxr-xr-xtest/e2e/e2e-test.sh74
-rw-r--r--test/e2e/localstack.Dockerfile16
-rw-r--r--test/e2e/vault.Dockerfile16
-rw-r--r--test/fuzz/README.md20
-rw-r--r--test/fuzz/corpus/ecdsa/09
-rw-r--r--test/fuzz/corpus/ecdsa/19
-rw-r--r--test/fuzz/corpus/ecdsa/23
-rw-r--r--test/fuzz/corpus/ed25519/07
-rw-r--r--test/fuzz/corpus/ed25519/17
-rw-r--r--test/fuzz/corpus/ed25519/23
-rw-r--r--test/fuzz/corpus/pem/02
-rw-r--r--test/fuzz/corpus/pem/00aa063b6a11aa6532146400d321a29ac57772df-62
-rw-r--r--test/fuzz/corpus/pem/039e15380cca84b1e0566d3212c336e5f3d83b07-21
-rw-r--r--test/fuzz/corpus/pem/0ad367ffa4f81383a24ef6abe1dc34ea4f77c25f-34
-rw-r--r--test/fuzz/corpus/pem/0b80059edfb2da2efb180e4d86c3975fb0e1eaba-82
-rw-r--r--test/fuzz/corpus/pem/17
-rw-r--r--test/fuzz/corpus/pem/126c5ff159d29cb0be86edd93ac59fea84eeb8f9-29
-rw-r--r--test/fuzz/corpus/pem/143b2870fa376ee37d700e46bad6b95da7cc6bf8-51
-rw-r--r--test/fuzz/corpus/pem/16bef3faa75283175c0f6ee02820f32cfda7b5d0-4bin0 -> 65 bytes
-rw-r--r--test/fuzz/corpus/pem/17dc2d28046108693e97a37eadcd107aa04870e3-33
-rw-r--r--test/fuzz/corpus/pem/1b83cfdc173ac48684d838336c7c1a55280e0acf-31
-rw-r--r--test/fuzz/corpus/pem/1bb1cb35daa2c2c232ace34d07f6067ac86da2ef-15
-rw-r--r--test/fuzz/corpus/pem/1bfab2d6d235f0e89b0a2c9abb0c3afbb61ac831-12
-rw-r--r--test/fuzz/corpus/pem/1c25ea2b045f73b72084eacf83a91a1d4e536194-13
-rw-r--r--test/fuzz/corpus/pem/1c293c6c956336a2e08d55087f43d331b571623e-67
-rw-r--r--test/fuzz/corpus/pem/2264b923a08d8b8784e39896da6d85095075eb2a-29
-rw-r--r--test/fuzz/corpus/pem/242cc35d953670092808e9d961d5511a818e3b6d-12
-rw-r--r--test/fuzz/corpus/pem/28023cd89a1827d1e70fe20215bdcf36b71fa499-43
-rw-r--r--test/fuzz/corpus/pem/32d7f4d00dbb5c8f962ef8ff8a31629ccc721aa1-44
-rw-r--r--test/fuzz/corpus/pem/3fb4188486c64a812260611d749dc9fc7ca15da3-12
-rw-r--r--test/fuzz/corpus/pem/40bdf4011e9fafe0e9b1b4e7cd4eef84a669cb06-26
-rw-r--r--test/fuzz/corpus/pem/46c44389cde762038f7ed276eeb5eb16287b41b0-24
-rw-r--r--test/fuzz/corpus/pem/48feeb9a655d20bfdfd263ba92fdbf6600bba7fa-510
-rw-r--r--test/fuzz/corpus/pem/4a339febca054df203b4627e14d0016fdd518143-74
-rw-r--r--test/fuzz/corpus/pem/4bc1c12e85085d1b3e0f0b2551d21422067a8b7b-41
-rw-r--r--test/fuzz/corpus/pem/4c1795357af14c6c54d4fa99b6fad04f7a431df9-17
-rw-r--r--test/fuzz/corpus/pem/4d47644faa08a59937b8f013b99ca3b5ac4fffeb-52
-rw-r--r--test/fuzz/corpus/pem/514b139e4ce9d356890d06f4af51df2f1afc3c13-24
-rw-r--r--test/fuzz/corpus/pem/53371d74058793bbeea98a1ed896d235647ee54f-26
-rw-r--r--test/fuzz/corpus/pem/55e49de57989fda88102051841cc1837835663f9-42
-rw-r--r--test/fuzz/corpus/pem/5727a70cf1fd7c94c5e8b67a644718be30ef4ba1-62
-rw-r--r--test/fuzz/corpus/pem/57d856a3a72f72aa0aff7cef1463125e85ebb717-612
-rw-r--r--test/fuzz/corpus/pem/5e41a95a406ec15af97f98b309ced43f926dcdeb-24
-rw-r--r--test/fuzz/corpus/pem/6177b5c12494f44da46e86b545f6bf16e6d9a37f-22
-rw-r--r--test/fuzz/corpus/pem/64a855b2b07b06e17e543094b2fea14fc3684e43-23
-rw-r--r--test/fuzz/corpus/pem/6ba8ad653a36b5f8d8a45c659af0e5b61ec552d8-18
-rw-r--r--test/fuzz/corpus/pem/73f7efa4dc5b350d91f9dec193009afe6089be97-13
-rw-r--r--test/fuzz/corpus/pem/75b35c51db6d069ba0ad6051524168aaf2594bfa-33
-rw-r--r--test/fuzz/corpus/pem/77ac6f147fab168ade8c977a5c25f2dc27ae5a52-41
-rw-r--r--test/fuzz/corpus/pem/78d3194b5b93a6448ebbd539a1f12cb5273b00eb-12
-rw-r--r--test/fuzz/corpus/pem/7a1a027faf971e46bc3fc08280e58ea47d46696c-92
-rw-r--r--test/fuzz/corpus/pem/800630235c61d15f3d21c1e72c1d4fa9c2452a06-32
-rw-r--r--test/fuzz/corpus/pem/83a880604a0db06142e5c50c06edcc7568c42f34-72
-rw-r--r--test/fuzz/corpus/pem/84d7f874ed4f1821d5188f8bfc5a970af2a8f13c-12
-rw-r--r--test/fuzz/corpus/pem/86156bb87353421d7a4dbd22ee8c87da90319c85-33
-rw-r--r--test/fuzz/corpus/pem/864196a44d1c943bc3e3426a550c33836e24114c-42
-rw-r--r--test/fuzz/corpus/pem/8684efa95476c052138696132158f32eba800ce1-34
-rw-r--r--test/fuzz/corpus/pem/879034c826380a27c9b61e8549decc9c6d080510-32
-rw-r--r--test/fuzz/corpus/pem/8cea0a2091af85c9740e519c9b2bcbbef33223db-34
-rw-r--r--test/fuzz/corpus/pem/8deec9a0749c64dc6541da31b465762e2bf11341-62
-rw-r--r--test/fuzz/corpus/pem/8f51cef2a076c2850e056835015a018e42fd7168-3bin0 -> 65 bytes
-rw-r--r--test/fuzz/corpus/pem/95c694ba9fc0fc5cb833114b9dc4ed6ae7644c79-31
-rw-r--r--test/fuzz/corpus/pem/97d56637605bf32936e7344bc0839dc0d396d002-22
-rw-r--r--test/fuzz/corpus/pem/9c65264e4b0b01adedc9ca30fbd20d7a3b6e4686-72
-rw-r--r--test/fuzz/corpus/pem/9d6ffa203cf753ab82943975ac34ae76b23a9503-42
-rw-r--r--test/fuzz/corpus/pem/a06a30151c284ca655d30170ab5143c9c6c2df8a-412
-rw-r--r--test/fuzz/corpus/pem/a2004bc3882e0e0770e3abcf9768fa5c3d642041-23
-rw-r--r--test/fuzz/corpus/pem/a345640366934585f446163074acf57c275b597b-33
-rw-r--r--test/fuzz/corpus/pem/a5d8e3d21838da80e7830e4d3f608c64e0cc4a32-512
-rw-r--r--test/fuzz/corpus/pem/a6f71960b5a3fe7787c0792d08963e4272bc76de-23
-rw-r--r--test/fuzz/corpus/pem/a8a0c9af9edc2f5aa62fdb4355692d843605a6af-23
-rw-r--r--test/fuzz/corpus/pem/ac2fde6a33a2f3dc35cce4f7a02fa2c87f2c09e8-23
-rw-r--r--test/fuzz/corpus/pem/afb44221440eed241164b63ef1c14e0b5b0ffaad-33
-rw-r--r--test/fuzz/corpus/pem/b9f075ae1a41e19f9e7e6176ab3c0dd1ef967161-53
-rw-r--r--test/fuzz/corpus/pem/bb55dfd080331ac9fed155558a8a5adb134b4e7f-23
-rw-r--r--test/fuzz/corpus/pem/cd1b2b5fb3598b0c314a6d73a8e0a7073b77e3da-23
-rw-r--r--test/fuzz/corpus/pem/d0663f723b7260b2f16b364dbd67f2dd2c32ff00-23
-rw-r--r--test/fuzz/corpus/pem/d13f816660c54829c658000f1b61b7b4b50f10b1-33
-rw-r--r--test/fuzz/corpus/pem/d51c3d46b0aef042186906efb3e342bbd37676dc-52
-rw-r--r--test/fuzz/corpus/pem/d680b4047f01df32c39741c6b9fda24bcd8b2ee5-33
-rw-r--r--test/fuzz/corpus/pem/d76412e8a07f0804d7f8b728da50966f4663b728-33
-rw-r--r--test/fuzz/corpus/pem/d89ae5c348e4931f4b5267efcdb798e42c248f5d-52
-rw-r--r--test/fuzz/corpus/pem/d95f213104887b0fe67d3de603a2ec49fde13f31-22
-rw-r--r--test/fuzz/corpus/pem/dfea2feae7c55cf54c241dafd5f951ee4de74164-52
-rw-r--r--test/fuzz/corpus/pem/e00036d8be7b3b07286b8ce7624c0aeb4e1c1550-49
-rw-r--r--test/fuzz/corpus/pem/e6ec65342f830e1780b16840d6a5767cf225553d-23
-rw-r--r--test/fuzz/corpus/pem/f0811f31ec1c422958fd861786088c9cf71d3fb5-13
-rw-r--r--test/fuzz/corpus/pem/f142d087e3fa84dd0140414b824f3a5398ef7e21-14
-rw-r--r--test/fuzz/corpus/pem/f458c0ff1410a9e9b66df63fb530936d44e6c31a-16
-rw-r--r--test/fuzz/corpus/pem/fab11055c6a34ab2fd0257b4b073eb6a945be42c-310
-rw-r--r--test/fuzz/corpus/pem/fac9f9666c93e22462b864764842be9eb9f17e45-14
-rw-r--r--test/fuzz/corpus/pem/fbdd93e11fdd678a319ec8069279d612d02f8df2-15
-rw-r--r--test/fuzz/corpus/pem/ffc5d83a112d1776b9a006fcab571901cd61fba3-3bin0 -> 24 bytes
-rw-r--r--test/fuzz/corpus/rsa/028
-rw-r--r--test/fuzz/corpus/rsa/13
-rw-r--r--test/fuzz/corpus/signature/01
-rw-r--r--test/fuzz/corpus/signature/11
-rw-r--r--test/fuzz/corpus/signature/21
-rw-r--r--test/fuzz/dsse/fuzz_test.go81
-rw-r--r--test/fuzz/fuzz_password_test.go44
-rw-r--r--test/fuzz/go.mod34
-rw-r--r--test/fuzz/go.sum91
-rw-r--r--test/fuzz/pem/fuzzcert_test.go85
-rw-r--r--test/fuzz/signature/fuzz_signature_test.go180
-rw-r--r--test/fuzz/tools.go25
263 files changed, 20883 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..eb594e9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+/pkg/generated/** linguist-generated
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..f74ddfa
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,105 @@
+---
+version: 2
+updates:
+ - package-ecosystem: gomod
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # hack/tools/go.mod
+ - package-ecosystem: gomod
+ directory: "./hack/tools"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # test/fuzz/go.mod
+ - package-ecosystem: gomod
+ directory: "./test/fuzz"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # pkg/signature/kms/aws/go.mod
+ - package-ecosystem: gomod
+ directory: "./pkg/signature/kms/aws"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # pkg/signature/kms/azure/go.mod
+ - package-ecosystem: gomod
+ directory: "./pkg/signature/kms/azure"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # pkg/signature/kms/gcp/go.mod
+ - package-ecosystem: gomod
+ directory: "./pkg/signature/kms/gcp"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ # pkg/signature/kms/hashivault/go.mod
+ - package-ecosystem: gomod
+ directory: "./pkg/signature/kms/hashivault"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
+
+ - package-ecosystem: "docker"
+ directory: "/test/e2e"
+ schedule:
+ interval: "weekly"
+ groups:
+ all:
+ update-types:
+ - "minor"
+ - "patch"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..a640ccc
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,50 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: CodeQL
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ main ]
+
+permissions:
+ security-events: write
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'go' ]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
diff --git a/.github/workflows/depsreview.yml b/.github/workflows/depsreview.yml
new file mode 100644
index 0000000..104f524
--- /dev/null
+++ b/.github/workflows/depsreview.yml
@@ -0,0 +1,28 @@
+#
+# Copyright 2022 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout Repository'
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@01bc87099ba56df1e897b6874784491ea6309bc4 # 3.1.4
diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml
new file mode 100644
index 0000000..83332fc
--- /dev/null
+++ b/.github/workflows/e2e_test.yml
@@ -0,0 +1,59 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ GO_VERSION: '1.21'
+
+jobs:
+ e2e_test:
+ name: e2e integration tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Set up Go
+ uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+
+ - name: Cache Modules
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Pull chrome (from ubuntu repository)
+ run: |
+ wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
+ sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
+ sudo apt update && sudo apt install -y google-chrome-stable
+
+ - name: Docker Build
+ working-directory: ./test/e2e
+ run: docker-compose build
+
+ - name: Integration test
+ run: make test-e2e
diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml
new file mode 100644
index 0000000..a979765
--- /dev/null
+++ b/.github/workflows/fuzzing.yml
@@ -0,0 +1,27 @@
+name: CIFuzz
+on:
+ push:
+ branches:
+ - main
+jobs:
+ Fuzzing:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Build Fuzzers
+ id: build
+ uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+ with:
+ oss-fuzz-project-name: "sigstore"
+ dry-run: false
+ - name: Run Fuzzers
+ uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+ with:
+ oss-fuzz-project-name: "sigstore"
+ fuzz-seconds: 600
+ dry-run: false
+ - name: Upload Crash
+ uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
+ if: failure() && steps.build.outcome == 'success'
+ with:
+ name: artifacts
+ path: ./out/artifacts
diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml
new file mode 100644
index 0000000..9dadcfd
--- /dev/null
+++ b/.github/workflows/reusable-release.yml
@@ -0,0 +1,75 @@
+name: Cut Release
+
+on:
+ workflow_call:
+ inputs:
+ release_tag:
+ required: false
+ type: string
+ description: 'Release tag. Will increment patch version if not specified.'
+ default: ''
+ key_ring:
+ required: false
+ type: string
+ description: 'Key ring for cosign key'
+ key_name:
+ required: false
+ type: string
+ description: 'Key name for cosign key'
+ workload_identity_provider:
+ required: true
+ type: string
+ description: 'Workload idenitty provider to authenticate acceses.'
+ service_account:
+ required: true
+ type: string
+ description: 'Service account to run the release.'
+ repo:
+ required: true
+ type: string
+ description: 'The Sigstore repo to release.'
+
+
+jobs:
+ cut-release:
+ name: Cut release
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ contents: read
+ env:
+ PROJECT_ID: 'projectsigstore'
+ RELEASE_TAG: ${{ inputs.release_tag }}
+ steps:
+ - name: Check actor access
+ if: ${{ !contains( fromJson('["bobcallaway","cpanato","dlorenc","lukehinds","priyawadhwa","vaikas","haydentherapper","znewman01"]'), github.actor ) }}
+ run: exit 1
+
+ - name: Checkout out repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ path: ./src/github.com/sigstore/${{ inputs.repo }}
+
+ - name: Set release tag if not specified
+ if: ${{ inputs.release_tag == '' }}
+ run: |
+ git fetch --all --tags
+ LATEST_DIGEST=`git rev-list --tags --max-count=1`
+ LATEST_TAG=`git describe --tags ${LATEST_DIGEST}`
+ NEW_VERSION=`echo "${TAG}" | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g'`
+ echo "RELEASE_TAG=${NEW_VERSION}" >> $GITHUB_ENV
+
+ - name: Authenticate to Google Cloud
+ uses: google-github-actions/auth@67e9c72af6e0492df856527b474995862b7b6591 # v2.0.0
+ with:
+ workload_identity_provider: ${{ inputs.workload_identity_provider }}
+ service_account: ${{ inputs.service_account }}
+
+ - name: Setup gcloud
+ uses: google-github-actions/setup-gcloud@825196879a077b7efa50db2e88409f44de4635c2 # v2.0.0
+ with:
+ project_id: ${{ env.PROJECT_ID }}
+
+ - name: Start cloudbuild job
+ working-directory: ./src/github.com/sigstore/${{ inputs.repo }}
+ run: gcloud builds submit --no-source --async --config release/cloudbuild.yaml --substitutions _GIT_TAG=${{ env.RELEASE_TAG }},_TOOL_ORG=sigstore,_TOOL_REPO=${{ inputs.repo }},_STORAGE_LOCATION=${{ inputs.repo }}-releases,_KEY_RING=release-cosign,_KEY_NAME=cosign,_GITHUB_USER=sigstore-bot --project=${{ env.PROJECT_ID }}
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 0000000..e3958c9
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,31 @@
+name: Scorecard supply-chain security
+on:
+ # (Optional) For Branch-Protection check. Only the default branch is supported. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+ # branch_protection_rule:
+ # To guarantee Maintained check is occasionally updated. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+ schedule:
+ - cron: '36 0 * * 0'
+ push:
+ branches: ["main"]
+
+# Declare default permissions as none.
+permissions: {}
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ permissions:
+ # Needed to upload the results to code-scanning dashboard.
+ security-events: write
+ # Needed to publish results and get a badge (see publish_results below).
+ id-token: write
+ uses: sigstore/community/.github/workflows/reusable-scorecard.yml@d0c95c8803672313d0bf72e1a44021be5b583c24 # main
+ # (Optional) Disable publish results:
+ # with:
+ # publish_results: false
+
+ # (Optional) Enable Branch-Protection check:
+ # secrets:
+ # scorecard_token: ${{ secrets.SCORECARD_TOKEN }}
diff --git a/.github/workflows/sync-module-tags.yaml b/.github/workflows/sync-module-tags.yaml
new file mode 100644
index 0000000..bb28453
--- /dev/null
+++ b/.github/workflows/sync-module-tags.yaml
@@ -0,0 +1,35 @@
+on:
+ push:
+ tags: ['v*']
+
+permissions:
+ contents: write
+
+# When a repository has multiple Go modules, in order to version the non-root
+# modules, the repo must have a tag named like '<module>/<version>'. This
+# workflow responds to pushes of tags named like "vXXX" by also tagging
+# <module>/<version> accordingly, for all sub-Go-modules.
+#
+# For example, when tagging v2.3.4, this workflow will tag the same commit with
+# pkg/signature/kms/aws/v2.3.4, so that the kms/aws module is also tagged
+# v2.3.4.
+
+jobs:
+ sync-tags:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ module:
+ - pkg/signature/kms/aws
+ - pkg/signature/kms/azure
+ - pkg/signature/kms/gcp
+ - pkg/signature/kms/hashivault
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - run: |
+ tag="${{ matrix.module }}/${{ github.ref_name }}"
+ echo tagging "${tag}"
+ git config user.name "${GITHUB_ACTOR}"
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git tag -a "${tag}" -m "syncing module ${{ matrix.module }} @ ${{ github.ref_name }}"
+ git push origin "${tag}"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..f1c50fe
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,72 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: test
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ unit_test:
+ name: unit tests
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ go-version:
+ - 1.20.x
+ - 1.21.x
+
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Set up Go
+ uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+
+ - name: Cache Modules
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - run: |
+ for submodule in \
+ . \
+ pkg/signature/kms/aws \
+ pkg/signature/kms/azure \
+ pkg/signature/kms/gcp \
+ pkg/signature/kms/hashivault; do
+ pushd $submodule
+
+ go mod tidy
+ go build -v ./...
+ go test -v ./...
+
+ popd
+ done
+
+ - name: Ensure no files were modified as a result of the build
+ run: git update-index --refresh && git diff-index --quiet HEAD -- || git diff --exit-code
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..d2b9780
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,58 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: verify
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ GO_VERSION: '1.21'
+
+jobs:
+ license-check:
+ name: license boilerplate check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - name: Install addlicense
+ run: go install github.com/google/addlicense@v1.0.0
+ - name: Check license headers
+ run: |
+ set -e
+ addlicense -l apache -c 'The Sigstore Authors' -v *
+ git diff --exit-code
+
+ golangci:
+ name: lint checks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0
+ timeout-minutes: 5
+ with:
+ version: v1.54
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eefc503
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+.idea/*
+sigstore
+*fuzz.zip
+bin*
+.vscode/*
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..47810fc
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,61 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+linters:
+ enable:
+ - asciicheck
+ - errcheck
+ - errorlint
+ - forcetypeassert
+ - gofmt
+ - gofumpt
+ - goimports
+ - gosec
+ - gocritic
+ - importas
+ - prealloc
+ - revive
+ - misspell
+ - stylecheck
+ - tparallel
+ - unconvert
+ - unparam
+ - unused
+ - whitespace
+output:
+ uniq-by-line: false
+issues:
+ include:
+ # revive `package-comments` and `exported` rules.
+ - EXC0012
+ - EXC0013
+ - EXC0014
+ - EXC0015
+ exclude-rules:
+ - path: _test\.go
+ linters:
+ - errcheck
+ - gosec
+ - path: pkg/tuf
+ linters:
+ - revive
+ max-issues-per-linter: 0
+ max-same-issues: 0
+linters-settings:
+ gofumpt:
+ extra-rules: true
+run:
+ issues-exit-code: 1
+ timeout: 10m
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..8aa2799
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,8 @@
+@sigstore/sigstore-codeowners
+
+# The CODEOWNERS are managed via a GitHub team, but the current list is (in alphabetical order):
+
+# bobcallaway
+# dekkagaijin
+# dlorenc
+# lukehinds
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..440768d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,74 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at <maintainers@sigstore.dev>. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file
diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt
new file mode 100644
index 0000000..7a01c84
--- /dev/null
+++ b/COPYRIGHT.txt
@@ -0,0 +1,14 @@
+
+Copyright 2021 The Sigstore Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..21635a7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,95 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+.PHONY: all pkg test test-e2e clean lint fuzz help
+
+all: pkg fuzz
+
+TOOLS_DIR := hack/tools
+TOOLS_BIN_DIR := $(abspath $(TOOLS_DIR)/bin)
+FUZZ_DIR := ./test/fuzz
+INTEGRATION_TEST_DIR := ./test/e2e
+GO-FUZZ-BUILD := $(TOOLS_BIN_DIR)/go-fuzz-build
+
+GOLANGCI_LINT_DIR = $(shell pwd)/bin
+GOLANGCI_LINT_BIN = $(GOLANGCI_LINT_DIR)/golangci-lint
+
+LDFLAGS ?=
+
+golangci-lint:
+ rm -f $(GOLANGCI_LINT_BIN) || :
+ set -e ;\
+ GOBIN=$(GOLANGCI_LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.2 ;\
+
+lint: golangci-lint ## Run golangci-lint
+ $(GOLANGCI_LINT_BIN) run -v --new-from-rev=HEAD~ ./...
+
+pkg: ## Build pkg
+ CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" ./...
+
+test: ## Run Tests for all Go modules.
+ go test ./...
+ cd ./pkg/signature/kms/aws && go test ./... && cd -
+ cd ./pkg/signature/kms/azure && go test ./... && cd -
+ cd ./pkg/signature/kms/gcp && go test ./... && cd -
+ cd ./pkg/signature/kms/hashivault && go test ./... && cd -
+
+tidy: ## Run go mod tidy all Go modules.
+ go mod tidy
+ cd ./pkg/signature/kms/aws && go mod tidy && cd -
+ cd ./pkg/signature/kms/azure && go mod tidy && cd -
+ cd ./pkg/signature/kms/gcp && go mod tidy && cd -
+ cd ./pkg/signature/kms/hashivault && go mod tidy && cd -
+
+test-e2e: ## Run E2E Tests
+ cd $(INTEGRATION_TEST_DIR); ./e2e-test.sh
+
+fuzz: $(GO-FUZZ-BUILD) ## Run Fuzz tests
+ cd $(FUZZ_DIR);$(GO-FUZZ-BUILD) -o pem-fuzz.zip ./pem
+ cd $(FUZZ_DIR);$(GO-FUZZ-BUILD) -o signature-fuzz.zip ./signature
+ cd $(FUZZ_DIR);$(GO-FUZZ-BUILD) -o fuzz-fuzz.zip .
+ cd $(FUZZ_DIR);$(GO-FUZZ-BUILD) -o dsse-fuzz.zip ./dsse
+
+clean: ## Clean workspace
+ rm -rf sigstore
+ rm -f $(FUZZ_DIR)/*fuzz.zip
+ rm -rf $(TOOLS_BIN_DIR)
+
+## --------------------------------------
+## Tooling Binaries
+## --------------------------------------
+
+$(GO-FUZZ-BUILD): $(TOOLS_DIR)/go.mod
+ cd $(TOOLS_DIR);go build -trimpath -tags=tools -o $(TOOLS_BIN_DIR)/go-fuzz-build github.com/dvyukov/go-fuzz/go-fuzz-build
+
+##################
+# help
+##################
+
+help: ## Display this help
+ @awk \
+ -v "col=${COLOR}" -v "nocol=${NOCOLOR}" \
+ ' \
+ BEGIN { \
+ FS = ":.*##" ; \
+ printf "\nUsage:\n make %s<target>%s\n", col, nocol \
+ } \
+ /^[a-zA-Z_-]+:.*?##/ { \
+ printf " %s%-15s%s %s\n", col, $$1, nocol, $$2 \
+ } \
+ /^##@/ { \
+ printf "\n%s%s%s\n", col, substr($$0, 5), nocol \
+ } \
+ ' $(MAKEFILE_LIST)
diff --git a/POLICY.md b/POLICY.md
new file mode 100644
index 0000000..0c567cf
--- /dev/null
+++ b/POLICY.md
@@ -0,0 +1,92 @@
+# Cryptographic Algorithms / KMS Policy
+
+sigstore/sigstore contains interfaces to provide signing and verification options with in-memory and cloud-provider KMS keys.
+This document specifies the currently supported signing and hashing algorithms and KMS providers, along with criteria for proposing new algorithms and providers.
+
+## Signing algorithms
+
+Sigstore supports the following signing algorithms:
+
+* RSA, with key sizes:
+ * 2048
+ * 3072
+ * 4096
+* ECDSA, with curves:
+ * NIST P-224 (secp224r1)
+ * NIST P-256 (secp256r1, prime256v1)
+ * NIST P-384 (secp384r1)
+ * NIST P-521 (secp521r1)
+* Ed25519
+
+Sigstore supports both the RSA-PKCS#1v1.5 and RSA-PSS signature schemes, and will only support well-known schemes implemented by the Golang crypto package.
+Sigstore will not support non-standard RSA key sizes. Sigstore will only support well-known ECDSA curves implemented by the Golang crypto package.
+
+### Post-quantum signing algorithms
+
+Post-quantum (PQ) computing will require new signing algorithms, as modern signing algorithms that depend on the difficulty of the integer factorization problem and discrete logarithm problem will be easily broken by quantum computing.
+
+Sigstore does not yet have a stance on which PQ signing algorithms will be supported.
+NIST is [currently selecting](https://csrc.nist.gov/Projects/post-quantum-cryptography/selected-algorithms-2022)
+a set of recommended signing algorithms. We present some high-level thoughts on the top candidates and existing hash-based schemes:
+
+* LMS/XMSS - Hash-based signature schemes that are quick to produce and verify with a small public key size, but larger in signature size.
+ NIST selected these two algorithms for hash-based signature schemes in 2020 ([SP 800.208](https://csrc.nist.gov/pubs/sp/800/208/final)).
+ LMS/XMSS have a significant drawback in that these signatures are stateful. Signing key reuse over a given amount breaks the security of LMS/XMSS.
+ These signing algorithms could work well for Sigstore's usage of ephemeral keys, since a key should only be used once for a signing event. However, these would not work for:
+ * Self-managed keys that may be reused, unless the signer keeps track of usage
+ * CAs (Fulcio) that sign certificates or transparency logs (Rekor) that sign log checkpoints, unless the services keep track of usage. For Sigstore, service key rotation is currently a manual process involving a TUF root signing event, so we are unable to automatically rotate service key material.
+ * TUF metadata itself, which is signed, unless the metadata keys are rotated out before being reused too frequently.
+* SPHINCS+ - Hash-based signature scheme that is quick to verify with a small public key size, but with a very large signature size and is very slow to verify.
+ Note that this scheme is stateless, so reuse is not a concern. Neither of the drawbacks are a significant concern in code signing,
+ although storage costs would increase for transparency log operators. Code signing can be slow, because it's a one-time process that can be automated.
+* CRYSTALS-Dilithium - Lattice-based signature scheme with fast signing and verification, but larger public key and signature sizes.
+ Dilithium offers good tradeoffs between signing/verification time and public key/signature sizes, though the larger key and signature sizes will increase storage costs for transparency log operators.
+* Falcon - Lattice-based signature scheme with fast verification, with larger public key and signature sizes, but smaller than Dilithium, and slower signing than Dilithium.
+ Like Dilithium, Falcon offers good tradeoffs between signing/verification time and public key/signature sizes.
+ However, Falcon is likely to be a complex implementation and there are some concerns around its use of floating point operations.
+
+This is not an exhaustive list, and this list may be updated as candidates are removed or if new signing algorithms are designed.
+We recommend reading Cloudflare's [scheme comparison](https://blog.cloudflare.com/sizing-up-post-quantum-signatures/)
+and [a deep dive into signing](https://blog.cloudflare.com/post-quantum-signatures/) to learn more.
+
+We will add support for PQ signing algorithms once the Golang crypto package adopts these signing algorithms.
+We will accept PRs with experimental support for NIST candidates once well-known and vetted Golang libraries are created for PQ signing.
+We will not accept PRs for PQ signing algorithms based on C implementations with a Go shim, though we encourage experimentation on forks and welcome any feedback on recommended algorithms in a GitHub discussion.
+
+## Hashing algorithms
+
+Sigstore supports the following hashing algorithms:
+
+* SHA256
+* SHA384
+* SHA512
+
+Supported but discouraged algorithms include:
+
+* SHA1 - SHA1 is allowed only in limited cases for compatibility with certain file types that require SHA1.
+* SHA224
+
+Sigstore will add support for SHA3 once the SHA3 implementation is moved from Golang's x/crypto package to its standard crypto package.
+
+## KMS providers
+
+Sigstore currently supports the following KMS providers:
+
+* Amazon Web Services
+* Google Cloud Platform
+* Hashicorp Vault
+* Microsoft Azure
+
+PRs for new providers will only be accepted if the following conditions are met:
+
+* Maintainers of Sigstore are familiar with the provider and able to debug issues with the provider and with end-to-end tests.
+* There is significant community interest in the KMS provider.
+* The provider is well-maintained with regular contributions (if open source) and releases should be frequent. The license must be compatible with the Apache 2.0 license.
+* The provider has not had significant security and/or privacy vulnerabilities. Sigstore reserves the right to remove support for a provider if it is shown to not take security and/or privacy seriously.
+* The PR contains sufficient unit and end-to-end tests.
+
+Please file an issue before starting implementation of the KMS provider to confirm that the provider meets these requirements.
+
+We encourage maintaining a private fork of sigstore/sigstore and Cosign if you wish to support a provider that does not meet these requirements.
+
+Sigstore's roadmap for future client updates includes a porcelain-and-plumbing model. One goal will be to add plugin support such that users could bring their own signing and verification modules without requiring a fork of Cosign.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c968621
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# sigstore framework
+[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/sigstore.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:sigstore) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5716/badge)](https://bestpractices.coreinfrastructure.org/projects/5716)
+
+sigstore/sigstore contains common [Sigstore](https://www.sigstore.dev/) code: that is, code shared by infrastructure (e.g., [Fulcio](https://github.com/sigstore/fulcio) and [Rekor](https://github.com/sigstore/rekor)) and Go language clients (e.g., [Cosign](https://github.com/sigstore/cosign) and [Gitsign](https://github.com/sigstore/gitsign)).
+
+This library currently provides:
+
+* A signing interface (support for ecdsa, ed25519, rsa, DSSE (in-toto))
+* OpenID Connect fulcio client code
+
+The following KMS systems are available:
+* AWS Key Management Service
+* Azure Key Vault
+* HashiCorp Vault
+* Google Cloud Platform Key Management Service
+
+For example code, look at the relevant test code for each main code file.
+
+## Fuzzing
+The fuzzing tests are within https://github.com/sigstore/sigstore/tree/main/test/fuzz
+
+## Security
+
+Should you discover any security issues, please refer to sigstores [security
+process](https://github.com/sigstore/.github/blob/main/SECURITY.md)
+
+For container signing, you want [cosign](https://github.com/sigstore/cosign)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..c0287fa
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,18 @@
+# Copyright 2023 The Sigstore Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+coverage:
+ status:
+ project: off
+ patch: off
diff --git a/docs/oid-info.md b/docs/oid-info.md
new file mode 100644
index 0000000..98f56dd
--- /dev/null
+++ b/docs/oid-info.md
@@ -0,0 +1,23 @@
+# Sigstore OID information
+
+## Description
+
+Sigstore maintains its own Private Enterprise Number (57264) with the Internet
+Assigned Numbers Authority to help identify and organize additional metadata in
+code signing certificates issued by Fulcio instances. This document aims to
+provide a simple directory of values in use with an explanation of their
+meaning.
+
+## Directory
+
+Note that all values begin from the root OID 1.3.6.1.4.1.57264 (registered by
+Sigstore)[http://oid-info.com/get/1.3.6.1.4.1.57264].
+
+When adding additional OIDs under the root, please update the above link with
+the child OID.
+
+| OID | Name | Details |
+| ------------------- | ------------------- | ------------------------------------------------------------- |
+| 1.3.6.1.4.1.57264.1 | Fulcio | https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md |
+| 1.3.6.1.4.1.57264.2 | Timestamp Authority | https://github.com/sigstore/timestamp-authority/blob/main/docs/tsa-policy.md#52-identification |
+| 1.3.6.1.4.1.57264.3 | Rekor | https://github.com/sigstore/rekor |
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..f76ccc0
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,42 @@
+module github.com/sigstore/sigstore
+
+go 1.20
+
+require (
+ github.com/coreos/go-oidc/v3 v3.9.0
+ github.com/go-jose/go-jose/v3 v3.0.1
+ github.com/go-rod/rod v0.114.5
+ github.com/go-test/deep v1.1.0
+ github.com/google/go-cmp v0.6.0
+ github.com/google/go-containerregistry v0.17.0
+ github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
+ github.com/secure-systems-lab/go-securesystemslib v0.8.0
+ github.com/segmentio/ksuid v1.0.4
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
+ github.com/stretchr/testify v1.8.4
+ github.com/theupdateframework/go-tuf v0.7.0
+ golang.org/x/crypto v0.17.0
+ golang.org/x/oauth2 v0.15.0
+ golang.org/x/term v0.15.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ github.com/ysmood/fetchup v0.2.3 // indirect
+ github.com/ysmood/goob v0.4.0 // indirect
+ github.com/ysmood/got v0.34.1 // indirect
+ github.com/ysmood/gson v0.7.3 // indirect
+ github.com/ysmood/leakless v0.8.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..488b854
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,208 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
+github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
+github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-rod/rod v0.114.5 h1:1x6oqnslwFVuXJbJifgxspJUd3O4ntaGhRLHt+4Er9c=
+github.com/go-rod/rod v0.114.5/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
+github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
+github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
+github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
+github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
+github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
+github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
+github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
+github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
+github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
+github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
+github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw=
+github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
+github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s=
+github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM=
+github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
+github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
+github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
+github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
+github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak=
+github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
+go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
+gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
new file mode 100644
index 0000000..9a45524
--- /dev/null
+++ b/hack/tools/go.mod
@@ -0,0 +1,29 @@
+module github.com/sigstore/sigstore/hack/tools
+
+go 1.20
+
+require (
+ github.com/AdaLogics/go-fuzz-headers v0.0.0-20211123104302-8fea106b46e2
+ github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1
+)
+
+require (
+ github.com/containerd/stargz-snapshotter/estargz v0.10.0 // indirect
+ github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+ github.com/google/go-containerregistry v0.7.0 // indirect
+ github.com/klauspost/compress v1.13.6 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.0.2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/vbatts/tar-split v0.11.2 // indirect
+ golang.org/x/mod v0.5.1 // indirect
+ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
+ golang.org/x/sys v0.1.0 // indirect
+ golang.org/x/tools v0.1.7 // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+)
+
+require (
+ github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
+ github.com/stephens2424/writerset v1.0.2 // indirect
+)
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
new file mode 100644
index 0000000..a0a1ae0
--- /dev/null
+++ b/hack/tools/go.sum
@@ -0,0 +1,1301 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20211123104302-8fea106b46e2 h1:ZsXCKWs2ZsvKzvc8HnGZg1QYg57N93qX4cb/SGhap8U=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20211123104302-8fea106b46e2/go.mod h1:qoZ1l1qQI1pAOwl9634GFAN4Z4cADOGCYOw3u9QAitw=
+github.com/AdamKorcz/go-fuzz-headers v0.0.0-20210312213058-32f4d319f0d2/go.mod h1:VPevheIvXETHZT/ddjwarP3POR5p/cnH9Hy5yoFnQjc=
+github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
+github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
+github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
+github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
+github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
+github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
+github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
+github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apex/log v1.4.0/go.mod h1:UMNC4vQNC7hb5gyr47r18ylK1n34rV7GO+gb0wpXvcE=
+github.com/apex/logs v0.0.7/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
+github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
+github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
+github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
+github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
+github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
+github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
+github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=
+github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
+github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
+github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
+github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
+github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
+github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
+github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
+github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
+github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
+github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
+github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
+github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
+github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
+github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
+github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=
+github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=
+github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
+github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=
+github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=
+github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=
+github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=
+github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
+github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/stargz-snapshotter/estargz v0.10.0 h1:glqzafvxBBAMo+x2w2sdDjUDZeTqqLJmqZPY05qehCU=
+github.com/containerd/stargz-snapshotter/estargz v0.10.0/go.mod h1:aE5PCyhFMwR8sbrErO5eM2GcvkyXTTJremG883D4qF0=
+github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
+github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
+github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
+github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
+github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=
+github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=
+github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
+github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
+github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
+github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
+github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
+github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
+github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
+github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/cli v20.10.10+incompatible h1:kcbwdgWbrBOH8QwQzaJmyriHwF7XIl4HT1qh0HTRys4=
+github.com/docker/cli v20.10.10+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v20.10.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM=
+github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
+github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1 h1:YQOLTC8zvFaNSEuMexG0i7pY26bOksnQFsSJfGclo54=
+github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
+github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
+github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
+github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-containerregistry v0.7.0 h1:u0onUUOcyoCDHEiJoyR1R1gx5er1+r06V5DBhUU5ndk=
+github.com/google/go-containerregistry v0.7.0/go.mod h1:2zaoelrL0d08gGbpdP3LqyUuBmhWbpD6IOe2s9nLS2k=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
+github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc90/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
+github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
+github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
+github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
+github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
+github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
+github.com/opencontainers/umoci v0.4.7/go.mod h1:lgJ4bnwJezsN1o/5d7t/xdRPvmf8TvBko5kKYJsYvgo=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rootless-containers/proto v0.1.0/go.mod h1:vgkUFZbQd0gcE/K/ZwtE4MYjZPu0UNHLXIQxhyqAFh8=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
+github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
+github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=
+github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
+github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
+github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
+github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
+github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
+github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vbatts/go-mtree v0.5.0/go.mod h1:7JbaNHyBMng+RP8C3Q4E+4Ca8JnGQA2R/MB+jb4tSOk=
+github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
+github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211111162719-482062a4217b/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
+k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
+k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
+k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
+k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
+k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
+k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
+k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
+k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
+k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
+k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
+k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
+k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
+k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/hack/tools/tools.go b/hack/tools/tools.go
new file mode 100644
index 0000000..4ae1534
--- /dev/null
+++ b/hack/tools/tools.go
@@ -0,0 +1,27 @@
+//go:build tools
+// +build tools
+
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// This package imports things required by build scripts, to force `go mod` to see them as dependencies
+package tools
+
+import (
+ _ "github.com/AdaLogics/go-fuzz-headers"
+ _ "github.com/dvyukov/go-fuzz/go-fuzz"
+ _ "github.com/dvyukov/go-fuzz/go-fuzz-build"
+ _ "github.com/dvyukov/go-fuzz/go-fuzz-dep"
+)
diff --git a/pkg/cryptoutils/certificate.go b/pkg/cryptoutils/certificate.go
new file mode 100644
index 0000000..348f47b
--- /dev/null
+++ b/pkg/cryptoutils/certificate.go
@@ -0,0 +1,173 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package cryptoutils implements support for working with encoded certificates, public keys, and private keys
+package cryptoutils
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "time"
+)
+
+const (
+ // CertificatePEMType is the string "CERTIFICATE" to be used during PEM encoding and decoding
+ CertificatePEMType PEMType = "CERTIFICATE"
+)
+
+// MarshalCertificateToPEM converts the provided X509 certificate into PEM format
+func MarshalCertificateToPEM(cert *x509.Certificate) ([]byte, error) {
+ if cert == nil {
+ return nil, errors.New("nil certificate provided")
+ }
+ return PEMEncode(CertificatePEMType, cert.Raw), nil
+}
+
+// MarshalCertificatesToPEM converts the provided X509 certificates into PEM format
+func MarshalCertificatesToPEM(certs []*x509.Certificate) ([]byte, error) {
+ buf := bytes.Buffer{}
+ for _, cert := range certs {
+ pemBytes, err := MarshalCertificateToPEM(cert)
+ if err != nil {
+ return nil, err
+ }
+ _, _ = buf.Write(pemBytes)
+ }
+ return buf.Bytes(), nil
+}
+
+// UnmarshalCertificatesFromPEM extracts one or more X509 certificates from the provided
+// byte slice, which is assumed to be in PEM-encoded format.
+func UnmarshalCertificatesFromPEM(pemBytes []byte) ([]*x509.Certificate, error) {
+ result := []*x509.Certificate{}
+ remaining := pemBytes
+ remaining = bytes.TrimSpace(remaining)
+
+ for len(remaining) > 0 {
+ var certDer *pem.Block
+ certDer, remaining = pem.Decode(remaining)
+
+ if certDer == nil {
+ return nil, errors.New("error during PEM decoding")
+ }
+
+ cert, err := x509.ParseCertificate(certDer.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, cert)
+ }
+ return result, nil
+}
+
+// UnmarshalCertificatesFromPEMLimited extracts one or more X509 certificates from the provided
+// byte slice, which is assumed to be in PEM-encoded format. Fails after a specified
+// number of iterations. A reasonable limit is 10 iterations.
+func UnmarshalCertificatesFromPEMLimited(pemBytes []byte, iterations int) ([]*x509.Certificate, error) {
+ result := []*x509.Certificate{}
+ remaining := pemBytes
+ remaining = bytes.TrimSpace(remaining)
+
+ count := 0
+ for len(remaining) > 0 {
+ if count == iterations {
+ return nil, errors.New("too many certificates specified in PEM block")
+ }
+ var certDer *pem.Block
+ certDer, remaining = pem.Decode(remaining)
+
+ if certDer == nil {
+ return nil, errors.New("error during PEM decoding")
+ }
+
+ cert, err := x509.ParseCertificate(certDer.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, cert)
+ count++
+ }
+ return result, nil
+}
+
+// LoadCertificatesFromPEM extracts one or more X509 certificates from the provided
+// io.Reader.
+func LoadCertificatesFromPEM(pem io.Reader) ([]*x509.Certificate, error) {
+ fileBytes, err := io.ReadAll(pem)
+ if err != nil {
+ return nil, err
+ }
+ return UnmarshalCertificatesFromPEM(fileBytes)
+}
+
+func formatTime(t time.Time) string {
+ return t.UTC().Format(time.RFC3339)
+}
+
+// CheckExpiration verifies that epoch is during the validity period of
+// the certificate provided.
+//
+// It returns nil if issueTime < epoch < expirationTime, and error otherwise.
+func CheckExpiration(cert *x509.Certificate, epoch time.Time) error {
+ if cert == nil {
+ return errors.New("certificate is nil")
+ }
+ if cert.NotAfter.Before(epoch) {
+ return fmt.Errorf("certificate expiration time %s is before %s", formatTime(cert.NotAfter), formatTime(epoch))
+ }
+ if cert.NotBefore.After(epoch) {
+ return fmt.Errorf("certificate issued time %s is before %s", formatTime(cert.NotBefore), formatTime(epoch))
+ }
+ return nil
+}
+
+// ParseCSR parses a PKCS#10 PEM-encoded CSR.
+func ParseCSR(csr []byte) (*x509.CertificateRequest, error) {
+ derBlock, _ := pem.Decode(csr)
+ if derBlock == nil || derBlock.Bytes == nil {
+ return nil, errors.New("no CSR found while decoding")
+ }
+ correctType := false
+ acceptedHeaders := []string{"CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST"}
+ for _, v := range acceptedHeaders {
+ if derBlock.Type == v {
+ correctType = true
+ }
+ }
+ if !correctType {
+ return nil, fmt.Errorf("DER type %v is not of any type %v for CSR", derBlock.Type, acceptedHeaders)
+ }
+
+ return x509.ParseCertificateRequest(derBlock.Bytes)
+}
+
+// GenerateSerialNumber creates a compliant serial number as per RFC 5280 4.1.2.2.
+// Serial numbers must be positive, and can be no longer than 20 bytes.
+// The serial number is generated with 159 bits, so that the first bit will always
+// be 0, resulting in a positive serial number.
+func GenerateSerialNumber() (*big.Int, error) {
+ // Pick a random number from 0 to 2^159.
+ serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
+ if err != nil {
+ return nil, errors.New("error generating serial number")
+ }
+ return serial, nil
+}
diff --git a/pkg/cryptoutils/certificate_test.go b/pkg/cryptoutils/certificate_test.go
new file mode 100644
index 0000000..7cf2df2
--- /dev/null
+++ b/pkg/cryptoutils/certificate_test.go
@@ -0,0 +1,429 @@
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "math/big"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+const (
+ cert1PEM = `-----BEGIN CERTIFICATE-----
+MIICyTCCAlCgAwIBAgITYZXhosLz4+Q/XCUwBySVDmU2jTAKBggqhkjOPQQDAzAq
+MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
+MDMwOTA0NDYwOVoXDTIxMDMwOTA1MDYwMlowOjEbMBkGA1UECgwSbG9yZW5jLmRA
+Z21haWwuY29tMRswGQYDVQQDDBJsb3JlbmMuZEBnbWFpbC5jb20wdjAQBgcqhkjO
+PQIBBgUrgQQAIgNiAARIA8thgk3Zext2UWP1aBE1uoIAqetevPiEDuGKtSUPYxBv
+AhzrwhDTbHrj6vMQCBNE7o4AfewyJAZf6CKbee8WIakPfAjRSTQjjnZBzKvSHn4K
+u8SByXjFN0rde8qDqo+jggEmMIIBIjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAww
+CgYIKwYBBQUHAwMwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUQeDktDb9QFrYxF8H
+xBXkAHQmvqwwHwYDVR0jBBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG0+wwgY0GCCsG
+AQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVu
+dC02MDNmZTdlNy0wMDAwLTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQuc3RvcmFnZS5n
+b29nbGVhcGlzLmNvbS9jYTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5jcnQwHQYDVR0R
+BBYwFIESbG9yZW5jLmRAZ21haWwuY29tMAoGCCqGSM49BAMDA2cAMGQCMAgjOcjN
+P3w/xB8bi/hKXJ9RdNH/DMADiusGtd1d/xxyFVj1xYosQ7y1G6y84VDBvQIwMfQG
+8Tp8zsxDg5Oz4qUBZ/AKmkPJHhgmiHftwbb5I1S+1xdhzJtJ8Eg0M00/nqok
+-----END CERTIFICATE-----
+`
+
+ cert2PEM = `-----BEGIN CERTIFICATE-----
+MIICyzCCAlGgAwIBAgIUAMtHjhf/mTVArZNaNcBC1UDyDa0wCgYIKoZIzj0EAwMw
+KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
+MTAzMDkwNDQ3MTFaFw0yMTAzMDkwNTA3MDNaMDoxGzAZBgNVBAoMEmxvcmVuYy5k
+QGdtYWlsLmNvbTEbMBkGA1UEAwwSbG9yZW5jLmRAZ21haWwuY29tMHYwEAYHKoZI
+zj0CAQYFK4EEACIDYgAERYBuXY15kRDrwW6kpfwbp/5DmrnGnaZLvUwG1QVGV4g7
+/dqNNoXpOU0TstLFSsawgc5YvcgKOw52EGBg5Fi9kUslF1M6bsAarMSTeZl7pzHb
+sh8B8+U/jn2HZtfCzalvo4IBJjCCASIwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM
+MAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFK2qQ8zm+0Yz5KM8
+6ybuUZPrAVX7MB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMIGNBggr
+BgEFBQcBAQSBgDB+MHwGCCsGAQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRl
+bnQtNjAzZmU3ZTctMDAwMC0yMjI3LWJmNzUtZjRmNWU4MGQyOTU0LnN0b3JhZ2Uu
+Z29vZ2xlYXBpcy5jb20vY2EzNmExZTk2MjQyYjlmY2IxNDYvY2EuY3J0MB0GA1Ud
+EQQWMBSBEmxvcmVuYy5kQGdtYWlsLmNvbTAKBggqhkjOPQQDAwNoADBlAjA6Nk0l
+eD2ey2UnPUcH6gklWo2szEobbv+uEKPYFzZ8fQn3v7nIdz8p3oSajT2oxFMCMQCQ
+zisxAae2OlC3p3RbPuobGVvKI6EYmfnohdvD5OgZPRskNhGAiR2e3MDQuuexbAw=
+-----END CERTIFICATE-----
+`
+
+ somethingElsePEM = `-----BEGIN OTHER THING-----
+MIICyjCCAlGgAwIBAgIUAJ7YnfKI0PIi90IkP1ZGVt1hbswwCgYIKoZIzj0EAwMw
+KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
+MTAzMDkwNDQ4MDJaFw0yMTAzMDkwNTA3NTVaMDoxGzAZBgNVBAoMEmxvcmVuYy5k
+QGdtYWlsLmNvbTEbMBkGA1UEAwwSbG9yZW5jLmRAZ21haWwuY29tMHYwEAYHKoZI
+zj0CAQYFK4EEACIDYgAEPFq3DlRqPtYRM2XF7eKw4pZFCIo3i1mBTFNzQRL/GufH
+G+bQuF9D9qDWwD2sFLDauzTbaIUvpxEdu0ab0rYi0x1sV/RLyZhc7KmpFxGuPEkr
+agrlBGDUnxXkUj53NGLCo4IBJjCCASIwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM
+MAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFOYy000hj60o55n3
+yX/sE3Kbk65NMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMIGNBggr
+BgEFBQcBAQSBgDB+MHwGCCsGAQUFBzAChnBodHRwOi8vcHJpdmF0ZWNhLWNvbnRl
+bnQtNjAzZmU3ZTctMDAwMC0yMjI3LWJmNzUtZjRmNWU4MGQyOTU0LnN0b3JhZ2Uu
+Z29vZ2xlYXBpcy5jb20vY2EzNmExZTk2MjQyYjlmY2IxNDYvY2EuY3J0MB0GA1Ud
+EQQWMBSBEmxvcmVuYy5kQGdtYWlsLmNvbTAKBggqhkjOPQQDAwNnADBkAjAYFQVj
+6qsh4cQ05NDOOCBrIdwtaYuiQHCy6y/+0ujrJG8prwc7zx+mFsL0Fsd9g/QCMGZq
+vn2snyjRWmm8kGr62QR0T6TKIElf07EmdrAglZodYXcHhv0b0JUmdTnn30vRDQ==
+-----BEGIN OTHER THING-----
+`
+)
+
+var (
+ // `x509.Certificate.Equal` only compares `Raw` fields.
+ cert1 = &x509.Certificate{
+ Raw: []byte{0x30, 0x82, 0x2, 0xc9, 0x30, 0x82, 0x2, 0x50, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x13, 0x61, 0x95, 0xe1, 0xa2, 0xc2, 0xf3, 0xe3, 0xe4, 0x3f, 0x5c, 0x25, 0x30, 0x7, 0x24, 0x95, 0xe, 0x65, 0x36, 0x8d, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x30, 0x2a, 0x31, 0x15, 0x30, 0x13, 0x6, 0x3, 0x55, 0x4, 0xa, 0x13, 0xc, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x31, 0x11, 0x30, 0xf, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x8, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x30, 0x1e, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x34, 0x34, 0x36, 0x30, 0x39, 0x5a, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x35, 0x30, 0x36, 0x30, 0x32, 0x5a, 0x30, 0x3a, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x76, 0x30, 0x10, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x5, 0x2b, 0x81, 0x4, 0x0, 0x22, 0x3, 0x62, 0x0, 0x4, 0x48, 0x3, 0xcb, 0x61, 0x82, 0x4d, 0xd9, 0x7b, 0x1b, 0x76, 0x51, 0x63, 0xf5, 0x68, 0x11, 0x35, 0xba, 0x82, 0x0, 0xa9, 0xeb, 0x5e, 0xbc, 0xf8, 0x84, 0xe, 0xe1, 0x8a, 0xb5, 0x25, 0xf, 0x63, 0x10, 0x6f, 0x2, 0x1c, 0xeb, 0xc2, 0x10, 0xd3, 0x6c, 0x7a, 0xe3, 0xea, 0xf3, 0x10, 0x8, 0x13, 0x44, 0xee, 0x8e, 0x0, 0x7d, 0xec, 0x32, 0x24, 0x6, 0x5f, 0xe8, 0x22, 0x9b, 0x79, 0xef, 0x16, 0x21, 0xa9, 0xf, 0x7c, 0x8, 0xd1, 0x49, 0x34, 0x23, 0x8e, 0x76, 0x41, 0xcc, 0xab, 0xd2, 0x1e, 0x7e, 0xa, 0xbb, 0xc4, 0x81, 0xc9, 0x78, 0xc5, 0x37, 0x4a, 0xdd, 0x7b, 0xca, 0x83, 0xaa, 0x8f, 0xa3, 0x82, 0x1, 0x26, 0x30, 0x82, 0x1, 0x22, 0x30, 0xe, 0x6, 0x3, 0x55, 0x1d, 0xf, 0x1, 0x1, 0xff, 0x4, 0x4, 0x3, 0x2, 0x7, 0x80, 0x30, 0x13, 0x6, 0x3, 0x55, 0x1d, 0x25, 0x4, 0xc, 0x30, 0xa, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x3, 0x3, 0x30, 0xc, 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0xe, 0x4, 0x16, 0x4, 0x14, 0x41, 0xe0, 0xe4, 0xb4, 0x36, 0xfd, 0x40, 0x5a, 0xd8, 0xc4, 0x5f, 0x7, 0xc4, 0x15, 0xe4, 0x0, 0x74, 0x26, 0xbe, 0xac, 0x30, 0x1f, 0x6, 0x3, 0x55, 0x1d, 0x23, 0x4, 0x18, 0x30, 0x16, 0x80, 0x14, 0xc8, 0xc5, 0x1d, 0x0, 0x41, 0x9a, 0x24, 0x29, 0x32, 0x51, 0x24, 0xeb, 0xd, 0xae, 0x4a, 0xed, 0x4a, 0x6, 0xd3, 0xec, 0x30, 0x81, 0x8d, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x1, 0x1, 0x4, 0x81, 0x80, 0x30, 0x7e, 0x30, 0x7c, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x30, 0x2, 0x86, 0x70, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x63, 0x61, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x36, 0x30, 0x33, 0x66, 0x65, 0x37, 0x65, 0x37, 0x2d, 0x30, 0x30, 0x30, 0x30, 0x2d, 0x32, 0x32, 0x32, 0x37, 0x2d, 0x62, 0x66, 0x37, 0x35, 0x2d, 0x66, 0x34, 0x66, 0x35, 0x65, 0x38, 0x30, 0x64, 0x32, 0x39, 0x35, 0x34, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x61, 0x33, 0x36, 0x61, 0x31, 0x65, 0x39, 0x36, 0x32, 0x34, 0x32, 0x62, 0x39, 0x66, 0x63, 0x62, 0x31, 0x34, 0x36, 0x2f, 0x63, 0x61, 0x2e, 0x63, 0x72, 0x74, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0x11, 0x4, 0x16, 0x30, 0x14, 0x81, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x3, 0x67, 0x0, 0x30, 0x64, 0x2, 0x30, 0x8, 0x23, 0x39, 0xc8, 0xcd, 0x3f, 0x7c, 0x3f, 0xc4, 0x1f, 0x1b, 0x8b, 0xf8, 0x4a, 0x5c, 0x9f, 0x51, 0x74, 0xd1, 0xff, 0xc, 0xc0, 0x3, 0x8a, 0xeb, 0x6, 0xb5, 0xdd, 0x5d, 0xff, 0x1c, 0x72, 0x15, 0x58, 0xf5, 0xc5, 0x8a, 0x2c, 0x43, 0xbc, 0xb5, 0x1b, 0xac, 0xbc, 0xe1, 0x50, 0xc1, 0xbd, 0x2, 0x30, 0x31, 0xf4, 0x6, 0xf1, 0x3a, 0x7c, 0xce, 0xcc, 0x43, 0x83, 0x93, 0xb3, 0xe2, 0xa5, 0x1, 0x67, 0xf0, 0xa, 0x9a, 0x43, 0xc9, 0x1e, 0x18, 0x26, 0x88, 0x77, 0xed, 0xc1, 0xb6, 0xf9, 0x23, 0x54, 0xbe, 0xd7, 0x17, 0x61, 0xcc, 0x9b, 0x49, 0xf0, 0x48, 0x34, 0x33, 0x4d, 0x3f, 0x9e, 0xaa, 0x24},
+ }
+ cert2 = &x509.Certificate{
+ Raw: []byte{0x30, 0x82, 0x2, 0xcb, 0x30, 0x82, 0x2, 0x51, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x14, 0x0, 0xcb, 0x47, 0x8e, 0x17, 0xff, 0x99, 0x35, 0x40, 0xad, 0x93, 0x5a, 0x35, 0xc0, 0x42, 0xd5, 0x40, 0xf2, 0xd, 0xad, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x30, 0x2a, 0x31, 0x15, 0x30, 0x13, 0x6, 0x3, 0x55, 0x4, 0xa, 0x13, 0xc, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x31, 0x11, 0x30, 0xf, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x8, 0x73, 0x69, 0x67, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x30, 0x1e, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x34, 0x34, 0x37, 0x31, 0x31, 0x5a, 0x17, 0xd, 0x32, 0x31, 0x30, 0x33, 0x30, 0x39, 0x30, 0x35, 0x30, 0x37, 0x30, 0x33, 0x5a, 0x30, 0x3a, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0xa, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x1b, 0x30, 0x19, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x76, 0x30, 0x10, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x5, 0x2b, 0x81, 0x4, 0x0, 0x22, 0x3, 0x62, 0x0, 0x4, 0x45, 0x80, 0x6e, 0x5d, 0x8d, 0x79, 0x91, 0x10, 0xeb, 0xc1, 0x6e, 0xa4, 0xa5, 0xfc, 0x1b, 0xa7, 0xfe, 0x43, 0x9a, 0xb9, 0xc6, 0x9d, 0xa6, 0x4b, 0xbd, 0x4c, 0x6, 0xd5, 0x5, 0x46, 0x57, 0x88, 0x3b, 0xfd, 0xda, 0x8d, 0x36, 0x85, 0xe9, 0x39, 0x4d, 0x13, 0xb2, 0xd2, 0xc5, 0x4a, 0xc6, 0xb0, 0x81, 0xce, 0x58, 0xbd, 0xc8, 0xa, 0x3b, 0xe, 0x76, 0x10, 0x60, 0x60, 0xe4, 0x58, 0xbd, 0x91, 0x4b, 0x25, 0x17, 0x53, 0x3a, 0x6e, 0xc0, 0x1a, 0xac, 0xc4, 0x93, 0x79, 0x99, 0x7b, 0xa7, 0x31, 0xdb, 0xb2, 0x1f, 0x1, 0xf3, 0xe5, 0x3f, 0x8e, 0x7d, 0x87, 0x66, 0xd7, 0xc2, 0xcd, 0xa9, 0x6f, 0xa3, 0x82, 0x1, 0x26, 0x30, 0x82, 0x1, 0x22, 0x30, 0xe, 0x6, 0x3, 0x55, 0x1d, 0xf, 0x1, 0x1, 0xff, 0x4, 0x4, 0x3, 0x2, 0x7, 0x80, 0x30, 0x13, 0x6, 0x3, 0x55, 0x1d, 0x25, 0x4, 0xc, 0x30, 0xa, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x3, 0x3, 0x30, 0xc, 0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0xe, 0x4, 0x16, 0x4, 0x14, 0xad, 0xaa, 0x43, 0xcc, 0xe6, 0xfb, 0x46, 0x33, 0xe4, 0xa3, 0x3c, 0xeb, 0x26, 0xee, 0x51, 0x93, 0xeb, 0x1, 0x55, 0xfb, 0x30, 0x1f, 0x6, 0x3, 0x55, 0x1d, 0x23, 0x4, 0x18, 0x30, 0x16, 0x80, 0x14, 0xc8, 0xc5, 0x1d, 0x0, 0x41, 0x9a, 0x24, 0x29, 0x32, 0x51, 0x24, 0xeb, 0xd, 0xae, 0x4a, 0xed, 0x4a, 0x6, 0xd3, 0xec, 0x30, 0x81, 0x8d, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x1, 0x1, 0x4, 0x81, 0x80, 0x30, 0x7e, 0x30, 0x7c, 0x6, 0x8, 0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x30, 0x2, 0x86, 0x70, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x63, 0x61, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x36, 0x30, 0x33, 0x66, 0x65, 0x37, 0x65, 0x37, 0x2d, 0x30, 0x30, 0x30, 0x30, 0x2d, 0x32, 0x32, 0x32, 0x37, 0x2d, 0x62, 0x66, 0x37, 0x35, 0x2d, 0x66, 0x34, 0x66, 0x35, 0x65, 0x38, 0x30, 0x64, 0x32, 0x39, 0x35, 0x34, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x61, 0x33, 0x36, 0x61, 0x31, 0x65, 0x39, 0x36, 0x32, 0x34, 0x32, 0x62, 0x39, 0x66, 0x63, 0x62, 0x31, 0x34, 0x36, 0x2f, 0x63, 0x61, 0x2e, 0x63, 0x72, 0x74, 0x30, 0x1d, 0x6, 0x3, 0x55, 0x1d, 0x11, 0x4, 0x16, 0x30, 0x14, 0x81, 0x12, 0x6c, 0x6f, 0x72, 0x65, 0x6e, 0x63, 0x2e, 0x64, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0xa, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x4, 0x3, 0x3, 0x3, 0x68, 0x0, 0x30, 0x65, 0x2, 0x30, 0x3a, 0x36, 0x4d, 0x25, 0x78, 0x3d, 0x9e, 0xcb, 0x65, 0x27, 0x3d, 0x47, 0x7, 0xea, 0x9, 0x25, 0x5a, 0x8d, 0xac, 0xcc, 0x4a, 0x1b, 0x6e, 0xff, 0xae, 0x10, 0xa3, 0xd8, 0x17, 0x36, 0x7c, 0x7d, 0x9, 0xf7, 0xbf, 0xb9, 0xc8, 0x77, 0x3f, 0x29, 0xde, 0x84, 0x9a, 0x8d, 0x3d, 0xa8, 0xc4, 0x53, 0x2, 0x31, 0x0, 0x90, 0xce, 0x2b, 0x31, 0x1, 0xa7, 0xb6, 0x3a, 0x50, 0xb7, 0xa7, 0x74, 0x5b, 0x3e, 0xea, 0x1b, 0x19, 0x5b, 0xca, 0x23, 0xa1, 0x18, 0x99, 0xf9, 0xe8, 0x85, 0xdb, 0xc3, 0xe4, 0xe8, 0x19, 0x3d, 0x1b, 0x24, 0x36, 0x11, 0x80, 0x89, 0x1d, 0x9e, 0xdc, 0xc0, 0xd0, 0xba, 0xe7, 0xb1, 0x6c, 0xc},
+ }
+)
+
+func assertCertsEqual(t *testing.T, wanted, got []*x509.Certificate) {
+ t.Helper()
+ if len(wanted) != len(got) {
+ t.Errorf("got %d certs, wanted %d", len(got), len(wanted))
+ }
+ for i, cert := range wanted {
+ if d := cmp.Diff(cert, got[i]); d != "" {
+ t.Errorf("wanted[%d] != got[%d] (-want +got): %s", i, i, d)
+ }
+ }
+}
+
+func TestCertificatesFromPEM(t *testing.T) {
+ testCases := []struct {
+ name string
+ pemBytes []byte
+
+ expected []*x509.Certificate
+ expectErr bool
+ }{
+ {
+ name: "one cert",
+ pemBytes: []byte(cert1PEM),
+
+ expected: []*x509.Certificate{cert1},
+ },
+ {
+ name: "one cert with trailing newline",
+ pemBytes: append([]byte(cert1PEM), '\n'),
+
+ expected: []*x509.Certificate{cert1},
+ },
+ {
+ name: "two certs",
+ pemBytes: []byte(cert1PEM + cert2PEM),
+ expected: []*x509.Certificate{cert1, cert2},
+ },
+ {
+ name: "two certs with newline between and after certificates",
+ pemBytes: []byte(cert1PEM + "\n" + cert2PEM + "\n"),
+ expected: []*x509.Certificate{cert1, cert2},
+ },
+ {
+ name: "one cert and one unknown PEM",
+ pemBytes: []byte(cert1PEM + somethingElsePEM),
+
+ expectErr: true,
+ },
+ {
+ name: "unknown PEM",
+ pemBytes: []byte(somethingElsePEM),
+
+ expectErr: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Run("Unmarshal", func(t *testing.T) {
+ got, err := UnmarshalCertificatesFromPEM(tc.pemBytes)
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("UnmarshalCertificatesFromPEM() returned unexpected error: %v", err)
+ }
+ } else if tc.expectErr {
+ t.Fatalf("UnmarshalCertificatesFromPEM() should have returned an error, got: %v", got)
+ }
+ assertCertsEqual(t, tc.expected, got)
+ })
+ t.Run("UnmarshalLimited", func(t *testing.T) {
+ got, err := UnmarshalCertificatesFromPEMLimited(tc.pemBytes, 10 /*iterations*/)
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("UnmarshalCertificatesFromPEM() returned unexpected error: %v", err)
+ }
+ } else if tc.expectErr {
+ t.Fatalf("UnmarshalCertificatesFromPEM() should have returned an error, got: %v", got)
+ }
+ assertCertsEqual(t, tc.expected, got)
+ })
+ t.Run("Load", func(t *testing.T) {
+ got, err := LoadCertificatesFromPEM(bytes.NewReader(tc.pemBytes))
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("LoadCertificatesFromPEM() returned unexpected error: %v", err)
+ }
+ } else if tc.expectErr {
+ t.Fatalf("LoadCertificatesFromPEM() should have returned an error, got: %v", got)
+ }
+ assertCertsEqual(t, tc.expected, got)
+ })
+ })
+ }
+}
+
+func TestUnmarshalCertificatesFromPEMLimited(t *testing.T) {
+ var pemCerts []byte
+ iterations := 10
+ for i := 0; i < iterations; i++ {
+ pemCerts = append(pemCerts, []byte(cert1PEM)...)
+ }
+ certs, err := UnmarshalCertificatesFromPEMLimited(pemCerts, iterations)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if len(certs) != iterations {
+ t.Fatalf("unexpected number of certificates, expected %d, got %d", iterations, len(certs))
+ }
+
+ // append one more certificate to cause a failure
+ pemCerts = append(pemCerts, []byte(cert1PEM)...)
+ _, err = UnmarshalCertificatesFromPEMLimited(pemCerts, iterations)
+ if err == nil || err.Error() != "too many certificates specified in PEM block" {
+ t.Fatalf("expected error with too many certificates, got %v", err)
+ }
+}
+
+func TestMarshalCertificateToPEM(t *testing.T) {
+ testCases := []struct {
+ name string
+ cert *x509.Certificate
+
+ expected []byte
+ expectErr bool
+ }{
+ {
+ name: "cert1",
+ cert: cert1,
+ expected: []byte(cert1PEM),
+ },
+ {
+ name: "cert2",
+ cert: cert2,
+ expected: []byte(cert2PEM),
+ },
+ {
+ name: "nil",
+ cert: nil,
+ expectErr: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := MarshalCertificateToPEM(tc.cert)
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("MarshalCertificateToPEM() returned unexpected error: %v", err)
+ }
+ } else if tc.expectErr {
+ t.Fatalf("MarshalCertificateToPEM() should have returned an error, got: %v", got)
+ }
+ if d := cmp.Diff(tc.expected, got); d != "" {
+ t.Errorf("MarshalCertificateToPEM() returned unexpected PEM (-want +got): %s", d)
+ }
+ })
+ }
+}
+
+func TestMarshalCertificatesToPEM(t *testing.T) {
+ testCases := []struct {
+ name string
+ certs []*x509.Certificate
+
+ expected []byte
+ expectErr bool
+ }{
+ {
+ name: "one cert",
+ certs: []*x509.Certificate{cert1},
+ expected: []byte(cert1PEM),
+ },
+ {
+ name: "two certs",
+ certs: []*x509.Certificate{cert1, cert2},
+ expected: []byte(cert1PEM + cert2PEM),
+ },
+ {
+ name: "nil cert",
+ certs: []*x509.Certificate{cert1, nil},
+ expectErr: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := MarshalCertificatesToPEM(tc.certs)
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("MarshalCertificatesToPEM() returned unexpected error: %v", err)
+ }
+ } else if tc.expectErr {
+ t.Fatalf("MarshalCertificatesToPEM() should have returned an error, got: %v", got)
+ }
+ if d := cmp.Diff(tc.expected, got); d != "" {
+ t.Errorf("MarshalCertificatesToPEM() returned unexpected PEM (-want +got): %s", d)
+ }
+ })
+ }
+}
+
+func errorsEqual(a, b error) bool {
+ if errors.Is(a, b) {
+ // both are nil
+ return true
+ }
+ return a != nil && b != nil && a.Error() == b.Error()
+}
+
+func TestCheckExpiration(t *testing.T) {
+ testCases := []struct {
+ name string
+ cert *x509.Certificate
+ epoch time.Time
+
+ expected error
+ }{
+ {
+ name: "valid",
+ cert: &x509.Certificate{
+ NotAfter: time.Unix(4444, 0),
+ NotBefore: time.Unix(2222, 0),
+ },
+ epoch: time.Unix(3333, 0),
+ expected: nil,
+ },
+ {
+ name: "expired",
+ cert: &x509.Certificate{
+ NotAfter: time.Unix(4444, 0),
+ NotBefore: time.Unix(2222, 0),
+ },
+ epoch: time.Unix(5555, 0),
+ expected: errors.New("certificate expiration time 1970-01-01T01:14:04Z is before 1970-01-01T01:32:35Z"),
+ },
+ {
+ name: "not valid yet",
+ cert: &x509.Certificate{
+ NotAfter: time.Unix(4444, 0),
+ NotBefore: time.Unix(2222, 0),
+ },
+ epoch: time.Unix(1111, 0),
+ expected: errors.New("certificate issued time 1970-01-01T00:37:02Z is before 1970-01-01T00:18:31Z"),
+ },
+ {
+ name: "nil",
+ cert: nil,
+ expected: errors.New("certificate is nil"),
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if err := CheckExpiration(tc.cert, tc.epoch); !errorsEqual(err, tc.expected) {
+ t.Errorf("CheckExpiration() should have returned: %v, got: %v", tc.expected, err)
+ }
+ })
+ }
+}
+
+func TestParseCSR(t *testing.T) {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ csrTmpl := &x509.CertificateRequest{Subject: pkix.Name{CommonName: "test"}}
+ derCSR, err := x509.CreateCertificateRequest(rand.Reader, csrTmpl, priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // success with type CERTIFICATE REQUEST
+ pemCSR := pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE REQUEST",
+ Bytes: derCSR,
+ })
+ parsedCSR, err := ParseCSR(pemCSR)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if parsedCSR.Subject.CommonName != "test" {
+ t.Fatalf("unexpected CSR common name")
+ }
+
+ // success with type NEW CERTIFICATE REQUEST
+ pemCSR = pem.EncodeToMemory(&pem.Block{
+ Type: "NEW CERTIFICATE REQUEST",
+ Bytes: derCSR,
+ })
+ parsedCSR, err = ParseCSR(pemCSR)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if parsedCSR.Subject.CommonName != "test" {
+ t.Fatalf("unexpected CSR common name")
+ }
+
+ // fails with invalid PEM encoded block
+ _, err = ParseCSR([]byte{1, 2, 3})
+ if err == nil || !strings.Contains(err.Error(), "no CSR found while decoding") {
+ t.Fatalf("expected error parsing invalid CSR, got %v", err)
+ }
+
+ // fails with invalid DER type
+ pemCSR = pem.EncodeToMemory(&pem.Block{
+ Type: "BEGIN CERTIFICATE",
+ Bytes: derCSR,
+ })
+ _, err = ParseCSR(pemCSR)
+ if err == nil || !strings.Contains(err.Error(), "DER type BEGIN CERTIFICATE is not of any type") {
+ t.Fatalf("expected error parsing invalid CSR, got %v", err)
+ }
+}
+
+func TestGenerateSerialNumber(t *testing.T) {
+ serialNumber, err := GenerateSerialNumber()
+ if err != nil {
+ t.Fatalf("unexpected error generating serial number: %v", err)
+ }
+ if serialNumber.Cmp(big.NewInt(0)) == -1 {
+ t.Fatalf("serial number is negative: %v", serialNumber)
+ }
+ if serialNumber.Cmp(big.NewInt(0)) == 0 {
+ t.Fatalf("serial number is 0: %v", serialNumber)
+ }
+ maxSerial := (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)
+ // Serial number must be less than max serial number.
+ if serialNumber.Cmp(maxSerial) >= 0 {
+ t.Fatalf("serial number is too large: %v", serialNumber)
+ }
+}
diff --git a/pkg/cryptoutils/doc.go b/pkg/cryptoutils/doc.go
new file mode 100644
index 0000000..4e7e73d
--- /dev/null
+++ b/pkg/cryptoutils/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package cryptoutils contains utilities related to handling cryptographic materials.
+package cryptoutils
diff --git a/pkg/cryptoutils/generic.go b/pkg/cryptoutils/generic.go
new file mode 100644
index 0000000..3fa6e7b
--- /dev/null
+++ b/pkg/cryptoutils/generic.go
@@ -0,0 +1,31 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "encoding/pem"
+)
+
+// PEMType is a specific type for string constants used during PEM encoding and decoding
+type PEMType string
+
+// PEMEncode encodes the specified byte slice in PEM format using the provided type string
+func PEMEncode(typeStr PEMType, bytes []byte) []byte {
+ return pem.EncodeToMemory(&pem.Block{
+ Type: string(typeStr),
+ Bytes: bytes,
+ })
+}
diff --git a/pkg/cryptoutils/generic_test.go b/pkg/cryptoutils/generic_test.go
new file mode 100644
index 0000000..1f48d94
--- /dev/null
+++ b/pkg/cryptoutils/generic_test.go
@@ -0,0 +1,34 @@
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestPEMEncode(t *testing.T) {
+ const testPemType PEMType = "ARBITRARY PEM TYPE"
+ expected := []byte(`-----BEGIN ARBITRARY PEM TYPE-----
+AAECAwQ=
+-----END ARBITRARY PEM TYPE-----
+`)
+ b := []byte{0x00, 0x01, 0x02, 0x03, 0x04}
+ pemBytes := PEMEncode(testPemType, b)
+ if d := cmp.Diff(expected, pemBytes); d != "" {
+ t.Errorf("MarshalCertificateToPEM() returned unexpected PEM (-want +got): %s", d)
+ }
+}
diff --git a/pkg/cryptoutils/password.go b/pkg/cryptoutils/password.go
new file mode 100644
index 0000000..89dd05e
--- /dev/null
+++ b/pkg/cryptoutils/password.go
@@ -0,0 +1,94 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ "golang.org/x/term"
+)
+
+// PassFunc is a type of function that takes a boolean (representing whether confirmation is desired) and returns the password as read, along with an error if one occurred
+type PassFunc func(bool) ([]byte, error)
+
+// Read is for fuzzing
+var Read = readPasswordFn
+
+// readPasswordFn reads the password from the following sources, in order of preference:
+//
+// - COSIGN_PASSWORD environment variable
+//
+// - user input from from terminal (if present)
+//
+// - provided to stdin from pipe
+func readPasswordFn() func() ([]byte, error) {
+ if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok {
+ return func() ([]byte, error) {
+ return []byte(pw), nil
+ }
+ }
+ if term.IsTerminal(0) {
+ return func() ([]byte, error) {
+ return term.ReadPassword(0)
+ }
+ }
+ // Handle piped in passwords.
+ return func() ([]byte, error) {
+ return io.ReadAll(os.Stdin)
+ }
+}
+
+// StaticPasswordFunc returns a PassFunc which returns the provided password.
+func StaticPasswordFunc(pw []byte) PassFunc {
+ return func(bool) ([]byte, error) {
+ return pw, nil
+ }
+}
+
+// SkipPassword is a PassFunc that does not interact with a user, but
+// simply returns nil for both the password result and error struct.
+func SkipPassword(_ bool) ([]byte, error) {
+ return nil, nil
+}
+
+// GetPasswordFromStdIn gathers the password from stdin with an
+// optional confirmation step.
+func GetPasswordFromStdIn(confirm bool) ([]byte, error) {
+ read := Read()
+ fmt.Fprint(os.Stderr, "Enter password for private key: ")
+ pw1, err := read()
+ fmt.Fprintln(os.Stderr)
+ if err != nil {
+ return nil, err
+ }
+ if !confirm {
+ return pw1, nil
+ }
+ fmt.Fprint(os.Stderr, "Enter again: ")
+ pw2, err := read()
+ fmt.Fprintln(os.Stderr)
+ if err != nil {
+ return nil, err
+ }
+
+ if string(pw1) != string(pw2) {
+ return nil, errors.New("passwords do not match")
+ }
+ return pw1, nil
+}
diff --git a/pkg/cryptoutils/privatekey.go b/pkg/cryptoutils/privatekey.go
new file mode 100644
index 0000000..325813d
--- /dev/null
+++ b/pkg/cryptoutils/privatekey.go
@@ -0,0 +1,152 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+
+ "github.com/secure-systems-lab/go-securesystemslib/encrypted"
+)
+
+const (
+ // PrivateKeyPEMType is the string "PRIVATE KEY" to be used during PEM encoding and decoding
+ PrivateKeyPEMType PEMType = "PRIVATE KEY"
+ // ECPrivateKeyPEMType is the string "EC PRIVATE KEY" used to parse SEC 1 EC private keys
+ ECPrivateKeyPEMType PEMType = "EC PRIVATE KEY"
+ // PKCS1PrivateKeyPEMType is the string "RSA PRIVATE KEY" used to parse PKCS#1-encoded private keys
+ PKCS1PrivateKeyPEMType PEMType = "RSA PRIVATE KEY"
+ encryptedCosignPrivateKeyPEMType PEMType = "ENCRYPTED COSIGN PRIVATE KEY"
+ // EncryptedSigstorePrivateKeyPEMType is the string "ENCRYPTED SIGSTORE PRIVATE KEY" to be used during PEM encoding and decoding
+ EncryptedSigstorePrivateKeyPEMType PEMType = "ENCRYPTED SIGSTORE PRIVATE KEY"
+)
+
+func pemEncodeKeyPair(priv crypto.PrivateKey, pub crypto.PublicKey, pf PassFunc) (privPEM, pubPEM []byte, err error) {
+ pubPEM, err = MarshalPublicKeyToPEM(pub)
+ if err != nil {
+ return nil, nil, err
+ }
+ derBytes, err := MarshalPrivateKeyToDER(priv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if pf == nil {
+ return PEMEncode(PrivateKeyPEMType, derBytes), pubPEM, nil
+ }
+ password, err := pf(true)
+ if err != nil {
+ return nil, nil, err
+ }
+ if password == nil {
+ return PEMEncode(PrivateKeyPEMType, derBytes), pubPEM, nil
+ }
+ if derBytes, err = encrypted.Encrypt(derBytes, password); err != nil {
+ return nil, nil, err
+ }
+ return PEMEncode(EncryptedSigstorePrivateKeyPEMType, derBytes), pubPEM, nil
+}
+
+// GeneratePEMEncodedECDSAKeyPair generates an ECDSA keypair, optionally password encrypted using a provided PassFunc, and PEM encoded.
+func GeneratePEMEncodedECDSAKeyPair(curve elliptic.Curve, pf PassFunc) (privPEM, pubPEM []byte, err error) {
+ priv, err := ecdsa.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+ return pemEncodeKeyPair(priv, priv.Public(), pf)
+}
+
+// GeneratePEMEncodedRSAKeyPair generates an RSA keypair, optionally password encrypted using a provided PassFunc, and PEM encoded.
+func GeneratePEMEncodedRSAKeyPair(keyLengthBits int, pf PassFunc) (privPEM, pubPEM []byte, err error) {
+ priv, err := rsa.GenerateKey(rand.Reader, keyLengthBits)
+ if err != nil {
+ return nil, nil, err
+ }
+ return pemEncodeKeyPair(priv, priv.Public(), pf)
+}
+
+// MarshalPrivateKeyToEncryptedDER marshals the private key and encrypts the DER-encoded value using the specified password function
+func MarshalPrivateKeyToEncryptedDER(priv crypto.PrivateKey, pf PassFunc) ([]byte, error) {
+ derKey, err := MarshalPrivateKeyToDER(priv)
+ if err != nil {
+ return nil, err
+ }
+ password, err := pf(true)
+ if err != nil {
+ return nil, err
+ }
+ if password == nil {
+ return nil, errors.New("password was nil")
+ }
+ return encrypted.Encrypt(derKey, password)
+}
+
+// UnmarshalPEMToPrivateKey converts a PEM-encoded byte slice into a crypto.PrivateKey
+func UnmarshalPEMToPrivateKey(pemBytes []byte, pf PassFunc) (crypto.PrivateKey, error) {
+ derBlock, _ := pem.Decode(pemBytes)
+ if derBlock == nil {
+ return nil, errors.New("PEM decoding failed")
+ }
+ switch derBlock.Type {
+ case string(PrivateKeyPEMType):
+ return x509.ParsePKCS8PrivateKey(derBlock.Bytes)
+ case string(PKCS1PrivateKeyPEMType):
+ return x509.ParsePKCS1PrivateKey(derBlock.Bytes)
+ case string(ECPrivateKeyPEMType):
+ return x509.ParseECPrivateKey(derBlock.Bytes)
+ case string(EncryptedSigstorePrivateKeyPEMType), string(encryptedCosignPrivateKeyPEMType):
+ derBytes := derBlock.Bytes
+ if pf != nil {
+ password, err := pf(false)
+ if err != nil {
+ return nil, err
+ }
+ if password != nil {
+ derBytes, err = encrypted.Decrypt(derBytes, password)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return x509.ParsePKCS8PrivateKey(derBytes)
+ }
+ return nil, fmt.Errorf("unknown private key PEM file type: %v", derBlock.Type)
+}
+
+// MarshalPrivateKeyToDER converts a crypto.PrivateKey into a PKCS8 ASN.1 DER byte slice
+func MarshalPrivateKeyToDER(priv crypto.PrivateKey) ([]byte, error) {
+ if priv == nil {
+ return nil, errors.New("empty key")
+ }
+ return x509.MarshalPKCS8PrivateKey(priv)
+}
+
+// MarshalPrivateKeyToPEM converts a crypto.PrivateKey into a PKCS#8 PEM-encoded byte slice
+func MarshalPrivateKeyToPEM(priv crypto.PrivateKey) ([]byte, error) {
+ derBytes, err := MarshalPrivateKeyToDER(priv)
+ if err != nil {
+ return nil, err
+ }
+ return PEMEncode(PrivateKeyPEMType, derBytes), nil
+}
diff --git a/pkg/cryptoutils/privatekey_test.go b/pkg/cryptoutils/privatekey_test.go
new file mode 100644
index 0000000..7489ba2
--- /dev/null
+++ b/pkg/cryptoutils/privatekey_test.go
@@ -0,0 +1,296 @@
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func verifyRSAKeyPEMs(t *testing.T, privPEM, pubPEM []byte, expectedKeyLengthBits int, testPassFunc PassFunc) {
+ t.Helper()
+
+ if priv, err := UnmarshalPEMToPrivateKey(privPEM, testPassFunc); err != nil {
+ t.Errorf("UnmarshalPEMToPrivateKey returned error: %v", err)
+ } else if rsaPriv, ok := priv.(*rsa.PrivateKey); !ok {
+ t.Errorf("expected unmarshaled key to be of type *rsa.PrivateKey, was %T", priv)
+ } else if rsaPriv.Size() != expectedKeyLengthBits/8 {
+ t.Errorf("private key size was %d, expected %d", rsaPriv.Size(), expectedKeyLengthBits/8)
+ }
+
+ if pub, err := UnmarshalPEMToPublicKey(pubPEM); err != nil {
+ t.Errorf("UnmarshalPEMToPublicKey returned error: %v", err)
+ } else if rsaPub, ok := pub.(*rsa.PublicKey); !ok {
+ t.Errorf("expected unmarshaled public key to be of type *rsa.PublicKey, was %T", pub)
+ } else if rsaPub.Size() != expectedKeyLengthBits/8 {
+ t.Errorf("public key size was %d, expected %d", rsaPub.Size(), expectedKeyLengthBits/8)
+ }
+}
+
+func TestGeneratePEMEncodedRSAKeyPair(t *testing.T) {
+ t.Parallel()
+
+ const testKeyBits = 2048
+
+ testCases := []struct {
+ name string
+ initialPassFunc PassFunc
+ goodPFs []PassFunc
+ badPFs []PassFunc
+ }{
+ {
+ name: "encrypted",
+ initialPassFunc: StaticPasswordFunc([]byte("TestGenerateEncryptedRSAKeyPair password")),
+ badPFs: []PassFunc{SkipPassword, nil},
+ },
+ {
+ name: "nil pass func",
+ initialPassFunc: nil,
+ goodPFs: []PassFunc{SkipPassword, nil},
+ },
+ {
+ name: "SkipPassword func",
+ initialPassFunc: SkipPassword,
+ goodPFs: []PassFunc{SkipPassword, nil},
+ },
+ }
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ privPEM, pubPEM, err := GeneratePEMEncodedRSAKeyPair(testKeyBits, tc.initialPassFunc)
+ if err != nil {
+ t.Fatalf("GeneratePEMEncodedRSAKeyPair returned error: %v", err)
+ }
+
+ for _, badPF := range tc.badPFs {
+ if priv, err := UnmarshalPEMToPrivateKey(privPEM, SkipPassword); err == nil {
+ t.Errorf("UnmarshalPEMToPrivateKey(pf=%v) should have returned error, got: %v", badPF, priv)
+ }
+ }
+ for _, goodPF := range tc.goodPFs {
+ if _, err := UnmarshalPEMToPrivateKey(privPEM, goodPF); err != nil {
+ t.Errorf("UnmarshalPEMToPrivateKey(pf=%v) returned error: %v", goodPF, err)
+ }
+ }
+ verifyRSAKeyPEMs(t, privPEM, pubPEM, testKeyBits, tc.initialPassFunc)
+ })
+ }
+}
+
+func verifyECDSAKeyPEMs(t *testing.T, privPEM, pubPEM []byte, expectedCurve elliptic.Curve, testPassFunc PassFunc) {
+ t.Helper()
+
+ if priv, err := UnmarshalPEMToPrivateKey(privPEM, testPassFunc); err != nil {
+ t.Errorf("UnmarshalPEMToPrivateKey returned error: %v", err)
+ } else if ecdsaPriv, ok := priv.(*ecdsa.PrivateKey); !ok {
+ t.Errorf("expected unmarshaled key to be of type *ecdsa.PrivateKey, was %T", priv)
+ } else if ecdsaPriv.Curve != expectedCurve {
+ t.Errorf("expected elliptic curve %v, got %d", expectedCurve, ecdsaPriv.Curve)
+ }
+
+ if pub, err := UnmarshalPEMToPublicKey(pubPEM); err != nil {
+ t.Errorf("UnmarshalPEMToPublicKey returned error: %v", err)
+ } else if ecdsaPub, ok := pub.(*ecdsa.PublicKey); !ok {
+ t.Errorf("expected unmarshaled key to be of type *ecdsa.PublicKey, was %T", pub)
+ } else if ecdsaPub.Curve != expectedCurve {
+ t.Errorf("expected elliptic curve %v, got %d", expectedCurve, ecdsaPub.Curve)
+ }
+}
+
+func TestGeneratePEMEncodedECDSAKeyPair(t *testing.T) {
+ t.Parallel()
+
+ testCurve := elliptic.P256()
+
+ testCases := []struct {
+ name string
+ initialPassFunc PassFunc
+ goodPFs []PassFunc
+ badPFs []PassFunc
+ }{
+ {
+ name: "encrypted",
+ initialPassFunc: StaticPasswordFunc([]byte("TestGenerateEncryptedRSAKeyPair password")),
+ badPFs: []PassFunc{SkipPassword, nil},
+ },
+ {
+ name: "nil pass func",
+ initialPassFunc: nil,
+ goodPFs: []PassFunc{SkipPassword, nil},
+ },
+ {
+ name: "SkipPassword func",
+ initialPassFunc: SkipPassword,
+ goodPFs: []PassFunc{SkipPassword, nil},
+ },
+ }
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ privPEM, pubPEM, err := GeneratePEMEncodedECDSAKeyPair(testCurve, tc.initialPassFunc)
+ if err != nil {
+ t.Fatalf("GeneratePEMEncodedRSAKeyPair returned error: %v", err)
+ }
+
+ for _, badPF := range tc.badPFs {
+ if priv, err := UnmarshalPEMToPrivateKey(privPEM, SkipPassword); err == nil {
+ t.Errorf("UnmarshalPEMToPrivateKey(pf=%v) should have returned error, got: %v", badPF, priv)
+ }
+ }
+ for _, goodPF := range tc.goodPFs {
+ if _, err := UnmarshalPEMToPrivateKey(privPEM, goodPF); err != nil {
+ t.Errorf("UnmarshalPEMToPrivateKey(pf=%v) returned error: %v", goodPF, err)
+ }
+ }
+ verifyECDSAKeyPEMs(t, privPEM, pubPEM, testCurve, tc.initialPassFunc)
+ })
+ }
+}
+
+func verifyPrivateKeyPEMRoundtrip(t *testing.T, pub crypto.PrivateKey) {
+ t.Helper()
+ pemBytes, err := MarshalPrivateKeyToPEM(pub)
+ if err != nil {
+ t.Fatalf("MarshalPrivateKeyToPEM returned error: %v", err)
+ }
+ rtPub, err := UnmarshalPEMToPrivateKey(pemBytes, nil)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPrivateKey returned error: %v", err)
+ }
+ if d := cmp.Diff(pub, rtPub); d != "" {
+ t.Errorf("round-tripped public key was malformed (-before +after): %s", d)
+ }
+}
+
+func TestECDSAPrivateKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ verifyPrivateKeyPEMRoundtrip(t, priv)
+}
+
+func TestEd25519PrivateKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ _, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ verifyPrivateKeyPEMRoundtrip(t, priv)
+}
+
+func TestRSAPrivateKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ verifyPrivateKeyPEMRoundtrip(t, priv)
+}
+
+func TestUnmarshalPEMToPrivateKey(t *testing.T) {
+ // test PKCS#8 PEM-encoded private keys
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ pkcs8PrivateKey, err := x509.MarshalPKCS8PrivateKey(priv)
+ if err != nil {
+ t.Fatalf("x509.MarshalPKCS8PrivateKey failed: %v", err)
+ }
+ pkcs8PEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "PRIVATE KEY",
+ Bytes: pkcs8PrivateKey,
+ })
+ k, err := UnmarshalPEMToPrivateKey(pkcs8PEMBlock, nil)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPrivateKey for PKCS#8 failed: %v", err)
+ }
+ if !priv.Equal(k) {
+ t.Fatalf("private keys for PKCS#8 are not equal")
+ }
+
+ // test PKCS#1 PEM-encoded RSA private keys
+ priv, err = rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ rsaPrivKey := x509.MarshalPKCS1PrivateKey(priv)
+ pkcs1PEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: rsaPrivKey,
+ })
+ k, err = UnmarshalPEMToPrivateKey(pkcs1PEMBlock, nil)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPrivateKey for PKCS#1 failed: %v", err)
+ }
+ if !priv.Equal(k) {
+ t.Fatalf("private keys for PKCS1 are not equal")
+ }
+
+ // test SEC 1 EC private keys
+ ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ ecPrivKey, err := x509.MarshalECPrivateKey(ecdsaKey)
+ if err != nil {
+ t.Fatalf("x509.MarshalECPrivateKey failed: %v", err)
+ }
+ ecPEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "EC PRIVATE KEY",
+ Bytes: ecPrivKey,
+ })
+ k, err = UnmarshalPEMToPrivateKey(ecPEMBlock, nil)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPrivateKey for SEC 1 failed: %v", err)
+ }
+ if !ecdsaKey.Equal(k) {
+ t.Fatalf("private keys for SEC 1 (EC) are not equal")
+ }
+
+ // test Sigstore formatted private keys
+ privSigstorePEM, _, err := GeneratePEMEncodedECDSAKeyPair(elliptic.P256(), StaticPasswordFunc([]byte("pw")))
+ if err != nil {
+ t.Fatalf("GeneratePEMEncodedECDSAKeyPair failed: %v", err)
+ }
+ _, err = UnmarshalPEMToPrivateKey(privSigstorePEM, StaticPasswordFunc([]byte("pw")))
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPrivateKey for Sigstore encoded key failed: %v", err)
+ }
+
+ // test other PEM formats return an error
+ invalidPEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: rsaPrivKey,
+ })
+ _, err = UnmarshalPEMToPrivateKey(invalidPEMBlock, nil)
+ if err == nil || !strings.Contains(err.Error(), "unknown private key PEM file type") {
+ t.Fatalf("expected error unmarshalling invalid PEM block, got: %v", err)
+ }
+}
diff --git a/pkg/cryptoutils/publickey.go b/pkg/cryptoutils/publickey.go
new file mode 100644
index 0000000..a8b2805
--- /dev/null
+++ b/pkg/cryptoutils/publickey.go
@@ -0,0 +1,184 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "crypto/sha1" // nolint:gosec
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "encoding/hex"
+ "encoding/pem"
+ "errors"
+ "fmt"
+
+ "github.com/letsencrypt/boulder/goodkey"
+)
+
+const (
+ // PublicKeyPEMType is the string "PUBLIC KEY" to be used during PEM encoding and decoding
+ PublicKeyPEMType PEMType = "PUBLIC KEY"
+ // PKCS1PublicKeyPEMType is the string "RSA PUBLIC KEY" used to parse PKCS#1-encoded public keys
+ PKCS1PublicKeyPEMType PEMType = "RSA PUBLIC KEY"
+)
+
+// subjectPublicKeyInfo is used to construct a subject key ID.
+// https://tools.ietf.org/html/rfc5280#section-4.1.2.7
+type subjectPublicKeyInfo struct {
+ Algorithm pkix.AlgorithmIdentifier
+ SubjectPublicKey asn1.BitString
+}
+
+// UnmarshalPEMToPublicKey converts a PEM-encoded byte slice into a crypto.PublicKey
+func UnmarshalPEMToPublicKey(pemBytes []byte) (crypto.PublicKey, error) {
+ derBytes, _ := pem.Decode(pemBytes)
+ if derBytes == nil {
+ return nil, errors.New("PEM decoding failed")
+ }
+ switch derBytes.Type {
+ case string(PublicKeyPEMType):
+ return x509.ParsePKIXPublicKey(derBytes.Bytes)
+ case string(PKCS1PublicKeyPEMType):
+ return x509.ParsePKCS1PublicKey(derBytes.Bytes)
+ default:
+ return nil, fmt.Errorf("unknown Public key PEM file type: %v. Are you passing the correct public key?",
+ derBytes.Type)
+ }
+}
+
+// MarshalPublicKeyToDER converts a crypto.PublicKey into a PKIX, ASN.1 DER byte slice
+func MarshalPublicKeyToDER(pub crypto.PublicKey) ([]byte, error) {
+ if pub == nil {
+ return nil, errors.New("empty key")
+ }
+ return x509.MarshalPKIXPublicKey(pub)
+}
+
+// MarshalPublicKeyToPEM converts a crypto.PublicKey into a PEM-encoded byte slice
+func MarshalPublicKeyToPEM(pub crypto.PublicKey) ([]byte, error) {
+ derBytes, err := MarshalPublicKeyToDER(pub)
+ if err != nil {
+ return nil, err
+ }
+ return PEMEncode(PublicKeyPEMType, derBytes), nil
+}
+
+// SKID generates a 160-bit SHA-1 hash of the value of the BIT STRING
+// subjectPublicKey (excluding the tag, length, and number of unused bits).
+// https://tools.ietf.org/html/rfc5280#section-4.2.1.2
+func SKID(pub crypto.PublicKey) ([]byte, error) {
+ derPubBytes, err := x509.MarshalPKIXPublicKey(pub)
+ if err != nil {
+ return nil, err
+ }
+ var spki subjectPublicKeyInfo
+ if _, err := asn1.Unmarshal(derPubBytes, &spki); err != nil {
+ return nil, err
+ }
+ skid := sha1.Sum(spki.SubjectPublicKey.Bytes) // nolint:gosec
+ return skid[:], nil
+}
+
+// EqualKeys compares two public keys. Supports RSA, ECDSA and ED25519.
+// If not equal, the error message contains hex-encoded SHA1 hashes of the DER-encoded keys
+func EqualKeys(first, second crypto.PublicKey) error {
+ switch pub := first.(type) {
+ case *rsa.PublicKey:
+ if !pub.Equal(second) {
+ return fmt.Errorf(genErrMsg(first, second, "rsa"))
+ }
+ case *ecdsa.PublicKey:
+ if !pub.Equal(second) {
+ return fmt.Errorf(genErrMsg(first, second, "ecdsa"))
+ }
+ case ed25519.PublicKey:
+ if !pub.Equal(second) {
+ return fmt.Errorf(genErrMsg(first, second, "ed25519"))
+ }
+ default:
+ return errors.New("unsupported key type")
+ }
+ return nil
+}
+
+// genErrMsg generates an error message for EqualKeys
+func genErrMsg(first, second crypto.PublicKey, keyType string) string {
+ msg := fmt.Sprintf("%s public keys are not equal", keyType)
+ // Calculate SKID to include in error message
+ firstSKID, err := SKID(first)
+ if err != nil {
+ return msg
+ }
+ secondSKID, err := SKID(second)
+ if err != nil {
+ return msg
+ }
+ return fmt.Sprintf("%s (%s, %s)", msg, hex.EncodeToString(firstSKID), hex.EncodeToString(secondSKID))
+}
+
+// ValidatePubKey validates the parameters of an RSA, ECDSA, or ED25519 public key.
+func ValidatePubKey(pub crypto.PublicKey) error {
+ switch pk := pub.(type) {
+ case *rsa.PublicKey:
+ // goodkey policy enforces:
+ // * Size of key: 2048 <= size <= 4096, size % 8 = 0
+ // * Exponent E = 65537 (Default exponent for OpenSSL and Golang)
+ // * Small primes check for modulus
+ // * Weak keys generated by Infineon hardware (see https://crocs.fi.muni.cz/public/papers/rsa_ccs17)
+ // * Key is easily factored with Fermat's factorization method
+ p, err := goodkey.NewKeyPolicy(&goodkey.Config{FermatRounds: 100}, nil)
+ if err != nil {
+ // Should not occur, only chances to return errors are if fermat rounds
+ // are <0 or when loading blocked/weak keys from disk (not used here)
+ return errors.New("unable to initialize key policy")
+ }
+ // ctx is unused
+ return p.GoodKey(context.Background(), pub)
+ case *ecdsa.PublicKey:
+ // Unable to use goodkey policy because P-521 curve is not supported
+ return validateEcdsaKey(pk)
+ case ed25519.PublicKey:
+ return validateEd25519Key(pk)
+ }
+ return errors.New("unsupported public key type")
+}
+
+// Enforce that the ECDSA key curve is one of:
+// * NIST P-256 (secp256r1, prime256v1)
+// * NIST P-384
+// * NIST P-521.
+// Other EC curves, like secp256k1, are not supported by Go.
+func validateEcdsaKey(pub *ecdsa.PublicKey) error {
+ switch pub.Curve {
+ case elliptic.P224():
+ return fmt.Errorf("unsupported ec curve, expected NIST P-256, P-384, or P-521")
+ case elliptic.P256(), elliptic.P384(), elliptic.P521():
+ return nil
+ default:
+ return fmt.Errorf("unexpected ec curve")
+ }
+}
+
+// No validations currently, ED25519 supports only one key size.
+func validateEd25519Key(_ ed25519.PublicKey) error {
+ return nil
+}
diff --git a/pkg/cryptoutils/publickey_test.go b/pkg/cryptoutils/publickey_test.go
new file mode 100644
index 0000000..f9582a6
--- /dev/null
+++ b/pkg/cryptoutils/publickey_test.go
@@ -0,0 +1,307 @@
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func verifyPublicKeyPEMRoundtrip(t *testing.T, pub crypto.PublicKey) {
+ t.Helper()
+ pemBytes, err := MarshalPublicKeyToPEM(pub)
+ if err != nil {
+ t.Fatalf("MarshalPublicKeyToPEM returned error: %v", err)
+ }
+ rtPub, err := UnmarshalPEMToPublicKey(pemBytes)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPublicKey returned error: %v", err)
+ }
+ if d := cmp.Diff(pub, rtPub); d != "" {
+ t.Errorf("round-tripped public key was malformed (-before +after): %s", d)
+ }
+}
+
+func TestECDSAPublicKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ verifyPublicKeyPEMRoundtrip(t, priv.Public())
+}
+
+func TestEd25519PublicKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ pub, _, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ verifyPublicKeyPEMRoundtrip(t, pub)
+}
+
+func TestRSAPublicKeyPEMRoundtrip(t *testing.T) {
+ t.Parallel()
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ verifyPublicKeyPEMRoundtrip(t, priv.Public())
+}
+
+func TestSKIDRSA(t *testing.T) {
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ skid, err := SKID(priv.Public())
+ if err != nil {
+ t.Fatalf("SKID failed: %v", err)
+ }
+ // Expect SKID is 160 bits (20 bytes)
+ if len(skid) != 20 {
+ t.Fatalf("SKID failed: %v", skid)
+ }
+}
+
+func TestSKIDECDSA(t *testing.T) {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ skid, err := SKID(priv.Public())
+ if err != nil {
+ t.Fatalf("SKID failed: %v", err)
+ }
+ // Expect SKID is 160 bits (20 bytes)
+ if len(skid) != 20 {
+ t.Fatalf("SKID failed: %v", skid)
+ }
+}
+
+func TestSKIDED25519(t *testing.T) {
+ pub, _, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ skid, err := SKID(pub)
+ if err != nil {
+ t.Fatalf("SKID failed: %v", err)
+ }
+ // Expect SKID is 160 bits (20 bytes)
+ if len(skid) != 20 {
+ t.Fatalf("SKID failed: %v", skid)
+ }
+}
+
+func TestEqualKeys(t *testing.T) {
+ // Test RSA (success and failure)
+ privRsa, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ privRsa2, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ if err := EqualKeys(privRsa.Public(), privRsa.Public()); err != nil {
+ t.Fatalf("unexpected error for rsa equality, got %v", err)
+ }
+ if err := EqualKeys(privRsa.Public(), privRsa2.Public()); err == nil || !strings.Contains(err.Error(), "rsa public keys are not equal") {
+ t.Fatalf("expected error for different rsa keys, got %v", err)
+ }
+ // Test ECDSA (success and failure)
+ privEcdsa, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ privEcdsa2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ if err := EqualKeys(privEcdsa.Public(), privEcdsa.Public()); err != nil {
+ t.Fatalf("unexpected error for ecdsa equality, got %v", err)
+ }
+ if err := EqualKeys(privEcdsa.Public(), privEcdsa2.Public()); err == nil || !strings.Contains(err.Error(), "ecdsa public keys are not equal") {
+ t.Fatalf("expected error for different ecdsa keys, got %v", err)
+ }
+ // Test ED25519 (success and failure)
+ pubEd, _, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ pubEd2, _, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ if err := EqualKeys(pubEd, pubEd); err != nil {
+ t.Fatalf("unexpected error for ed25519 equality, got %v", err)
+ }
+ if err := EqualKeys(pubEd, pubEd2); err == nil || !strings.Contains(err.Error(), "ed25519 public keys are not equal") {
+ t.Fatalf("expected error for different ed25519 keys, got %v", err)
+ }
+ // Keys of different type are not equal
+ if err := EqualKeys(privRsa.Public(), pubEd); err == nil || !strings.Contains(err.Error(), "are not equal") {
+ t.Fatalf("expected error for different key types, got %v", err)
+ }
+ // Fails with unexpected key type
+ type PublicKey struct{}
+ if err := EqualKeys(PublicKey{}, PublicKey{}); err == nil || err.Error() != "unsupported key type" {
+ t.Fatalf("expected error for unsupported key type, got %v", err)
+ }
+}
+
+func TestValidatePubKeyUnsupported(t *testing.T) {
+ // Fails with unexpected key type
+ type PublicKey struct{}
+ err := ValidatePubKey(PublicKey{})
+ if err == nil || err.Error() != "unsupported public key type" {
+ t.Errorf("expected unsupported public key type, got %v", err)
+ }
+}
+
+func TestValidatePubKeyRsa(t *testing.T) {
+ // Validate common RSA key sizes
+ for _, bits := range []int{2048, 3072, 4096} {
+ priv, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err != nil {
+ t.Errorf("unexpected error validating public key: %v", err)
+ }
+ }
+ // Fails with small key size
+ priv, err := rsa.GenerateKey(rand.Reader, 1024)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err == nil || err.Error() != "key size not supported: 1024" {
+ t.Errorf("expected rsa key size not supported, got %v", err)
+ }
+ // Fails with large key size
+ priv, err = rsa.GenerateKey(rand.Reader, 5000)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err == nil || err.Error() != "key size not supported: 5000" {
+ t.Errorf("expected rsa key size not supported, got %v", err)
+ }
+ // Fails with key size that's not a multiple of 8
+ priv, err = rsa.GenerateKey(rand.Reader, 4095)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err == nil || err.Error() != "key size not supported: 4095" {
+ t.Errorf("expected rsa key size not supported, got %v", err)
+ }
+}
+
+func TestValidatePubKeyEcdsa(t *testing.T) {
+ for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} {
+ priv, err := ecdsa.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err != nil {
+ t.Errorf("unexpected error validating public key: %v", err)
+ }
+ }
+ // Fails with smalller curve
+ priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatalf("ecdsa.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(priv.Public()); err == nil || err.Error() != "unsupported ec curve, expected NIST P-256, P-384, or P-521" {
+ t.Errorf("expected unsupported curve, got %v", err)
+ }
+ // Fails with unknown curve
+ err = ValidatePubKey(&ecdsa.PublicKey{})
+ if err == nil || err.Error() != "unexpected ec curve" {
+ t.Errorf("expected unexpected curve, got %v", err)
+ }
+}
+
+func TestValidatePubKeyEd25519(t *testing.T) {
+ pub, _, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey failed: %v", err)
+ }
+ if err := ValidatePubKey(pub); err != nil {
+ t.Errorf("unexpected error validating public key: %v", err)
+ }
+ // Only success, ED25519 keys do not support customization
+}
+
+func TestUnmarshalPEMToPublicKey(t *testing.T) {
+ // test PKIX PEM-encoded public keys
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ pkixPubKey, err := x509.MarshalPKIXPublicKey(priv.Public())
+ if err != nil {
+ t.Fatalf("x509.MarshalPKIXPublicKey failed: %v", err)
+ }
+ pkixPEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "PUBLIC KEY",
+ Bytes: pkixPubKey,
+ })
+ k, err := UnmarshalPEMToPublicKey(pkixPEMBlock)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPublicKey for PKIX failed: %v", err)
+ }
+ if EqualKeys(priv.Public(), k) != nil {
+ t.Fatalf("public keys for PKIX are not equal")
+ }
+
+ // test PKCS#1 PEM-encoded RSA public keys
+ priv, err = rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey failed: %v", err)
+ }
+ rsaPubKey := x509.MarshalPKCS1PublicKey(&priv.PublicKey)
+ pkcs1PEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: rsaPubKey,
+ })
+ k, err = UnmarshalPEMToPublicKey(pkcs1PEMBlock)
+ if err != nil {
+ t.Fatalf("UnmarshalPEMToPublicKey for PKCS#1 failed: %v", err)
+ }
+ if EqualKeys(priv.Public(), k) != nil {
+ t.Fatalf("public keys for PKCS1 are not equal")
+ }
+
+ // test other PEM formats return an error
+ invalidPEMBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "EC PUBLIC KEY",
+ Bytes: rsaPubKey,
+ })
+ _, err = UnmarshalPEMToPublicKey(invalidPEMBlock)
+ if err == nil || !strings.Contains(err.Error(), "unknown Public key PEM file type") {
+ t.Fatalf("expected error unmarshalling invalid PEM block, got: %v", err)
+ }
+}
diff --git a/pkg/cryptoutils/sans.go b/pkg/cryptoutils/sans.go
new file mode 100644
index 0000000..d237ef5
--- /dev/null
+++ b/pkg/cryptoutils/sans.go
@@ -0,0 +1,149 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "errors"
+ "fmt"
+)
+
+var (
+ // OIDOtherName is the OID for the OtherName SAN per RFC 5280
+ OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7}
+ // SANOID is the OID for Subject Alternative Name per RFC 5280
+ SANOID = asn1.ObjectIdentifier{2, 5, 29, 17}
+)
+
+// OtherName describes a name related to a certificate which is not in one
+// of the standard name formats. RFC 5280, 4.2.1.6:
+//
+// OtherName ::= SEQUENCE {
+// type-id OBJECT IDENTIFIER,
+// value [0] EXPLICIT ANY DEFINED BY type-id }
+//
+// OtherName for Fulcio-issued certificates only supports UTF-8 strings as values.
+type OtherName struct {
+ ID asn1.ObjectIdentifier
+ Value string `asn1:"utf8,explicit,tag:0"`
+}
+
+// MarshalOtherNameSAN creates a Subject Alternative Name extension
+// with an OtherName sequence. RFC 5280, 4.2.1.6:
+//
+// SubjectAltName ::= GeneralNames
+// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
+// GeneralName ::= CHOICE {
+//
+// otherName [0] OtherName,
+// ... }
+func MarshalOtherNameSAN(name string, critical bool) (*pkix.Extension, error) {
+ o := OtherName{
+ ID: OIDOtherName,
+ Value: name,
+ }
+ bytes, err := asn1.MarshalWithParams(o, "tag:0")
+ if err != nil {
+ return nil, err
+ }
+
+ sans, err := asn1.Marshal([]asn1.RawValue{{FullBytes: bytes}})
+ if err != nil {
+ return nil, err
+ }
+ return &pkix.Extension{
+ Id: SANOID,
+ Critical: critical,
+ Value: sans,
+ }, nil
+}
+
+// UnmarshalOtherNameSAN extracts a UTF-8 string from the OtherName
+// field in the Subject Alternative Name extension.
+func UnmarshalOtherNameSAN(exts []pkix.Extension) (string, error) {
+ var otherNames []string
+
+ for _, e := range exts {
+ if !e.Id.Equal(SANOID) {
+ continue
+ }
+
+ var seq asn1.RawValue
+ rest, err := asn1.Unmarshal(e.Value, &seq)
+ if err != nil {
+ return "", err
+ } else if len(rest) != 0 {
+ return "", fmt.Errorf("trailing data after X.509 extension")
+ }
+ if !seq.IsCompound || seq.Tag != asn1.TagSequence || seq.Class != asn1.ClassUniversal {
+ return "", asn1.StructuralError{Msg: "bad SAN sequence"}
+ }
+
+ rest = seq.Bytes
+ for len(rest) > 0 {
+ var v asn1.RawValue
+ rest, err = asn1.Unmarshal(rest, &v)
+ if err != nil {
+ return "", err
+ }
+
+ // skip all GeneralName fields except OtherName
+ if v.Tag != 0 {
+ continue
+ }
+
+ var other OtherName
+ if _, err := asn1.UnmarshalWithParams(v.FullBytes, &other, "tag:0"); err != nil {
+ return "", fmt.Errorf("could not parse requested OtherName SAN: %w", err)
+ }
+ if !other.ID.Equal(OIDOtherName) {
+ return "", fmt.Errorf("unexpected OID for OtherName, expected %v, got %v", OIDOtherName, other.ID)
+ }
+ otherNames = append(otherNames, other.Value)
+ }
+ }
+
+ if len(otherNames) == 0 {
+ return "", errors.New("no OtherName found")
+ }
+ if len(otherNames) != 1 {
+ return "", errors.New("expected only one OtherName")
+ }
+
+ return otherNames[0], nil
+}
+
+// GetSubjectAlternateNames extracts all subject alternative names from
+// the certificate, including email addresses, DNS, IP addresses, URIs,
+// and OtherName SANs
+func GetSubjectAlternateNames(cert *x509.Certificate) []string {
+ sans := []string{}
+ sans = append(sans, cert.DNSNames...)
+ sans = append(sans, cert.EmailAddresses...)
+ for _, ip := range cert.IPAddresses {
+ sans = append(sans, ip.String())
+ }
+ for _, uri := range cert.URIs {
+ sans = append(sans, uri.String())
+ }
+ // ignore error if there's no OtherName SAN
+ otherName, _ := UnmarshalOtherNameSAN(cert.Extensions)
+ if len(otherName) > 0 {
+ sans = append(sans, otherName)
+ }
+ return sans
+}
diff --git a/pkg/cryptoutils/sans_test.go b/pkg/cryptoutils/sans_test.go
new file mode 100644
index 0000000..83516bb
--- /dev/null
+++ b/pkg/cryptoutils/sans_test.go
@@ -0,0 +1,230 @@
+// Copyright 2022 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cryptoutils
+
+import (
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "encoding/hex"
+ "net"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/sigstore/sigstore/test"
+)
+
+func TestMarshalAndUnmarshalOtherNameSAN(t *testing.T) {
+ otherName := "foo!example.com"
+ critical := true
+
+ ext, err := MarshalOtherNameSAN(otherName, critical)
+ if err != nil {
+ t.Fatalf("unexpected error for MarshalOtherNameSAN: %v", err)
+ }
+ if ext.Critical != critical {
+ t.Fatalf("expected extension to be critical")
+ }
+ if !ext.Id.Equal(SANOID) {
+ t.Fatalf("expected extension's OID to be SANs OID")
+ }
+ // https://lapo.it/asn1js/#MCGgHwYKKwYBBAGDvzABB6ARDA9mb28hZXhhbXBsZS5jb20
+ // 30 - Constructed sequence
+ // 21 - length of sequence
+ // A0 - Context-specific (class 2) (bits 8,7) with Constructed bit (bit 6) and 0 tag
+ // 1F - length of context-specific field (OID)
+ // 06 - OID tag
+ // 0A - length of OID
+ // 2B 06 01 04 01 83 BF 30 01 07 - OID
+ // A0 - Context-specific (class 2) with Constructed bit and 0 tag
+ // (needed for EXPLICIT encoding, which wraps field in outer encoding)
+ // 11 - length of context-specific field (string)
+ // 0C - UTF8String tag
+ // 0F - length of string
+ // 66 6F 6F 21 65 78 61 6D 70 6C 65 2E 63 6F 6D - string
+ if hex.EncodeToString(ext.Value) != "3021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d" {
+ t.Fatalf("unexpected ASN.1 encoding")
+ }
+
+ on, err := UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err != nil {
+ t.Fatalf("unexpected error for UnmarshalOtherNameSAN: %v", err)
+ }
+ if on != otherName {
+ t.Fatalf("unexpected OtherName, expected %s, got %s", otherName, on)
+ }
+}
+
+func TestUnmarshalOtherNameSANFailures(t *testing.T) {
+ var err error
+
+ // failure: no SANs extension
+ ext := &pkix.Extension{
+ Id: asn1.ObjectIdentifier{},
+ Critical: true,
+ Value: []byte{},
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "no OtherName found") {
+ t.Fatalf("expected error finding no OtherName, got %v", err)
+ }
+
+ // failure: bad sequence
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: []byte{},
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "sequence truncated") {
+ t.Fatalf("expected error with invalid ASN.1, got %v", err)
+ }
+
+ // failure: extra data after valid sequence
+ b, _ := hex.DecodeString("3021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d" + "30")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "trailing data after X.509 extension") {
+ t.Fatalf("expected error with extra data, got %v", err)
+ }
+
+ // failure: non-universal class (Change last two bits: 30 = 00110000 => 10110000 -> B0)
+ b, _ = hex.DecodeString("B021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
+ t.Fatalf("expected error with non-universal class, got %v", err)
+ }
+
+ // failure: not compound sequence (Change 6th bit: 30 = 00110000 => 00010000 -> 10)
+ b, _ = hex.DecodeString("1021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
+ t.Fatalf("expected error with non-compound sequence, got %v", err)
+ }
+
+ // failure: non-sequence tag (Change lower 5 bits: 30 = 00110000 => 00100010 -> 12)
+ b, _ = hex.DecodeString("1221a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
+ t.Fatalf("expected error with non-sequence tag, got %v", err)
+ }
+
+ // failure: no GeneralName with tag=0 (Change lower 5 bits of first sequence field: 3021a01f -> 3021a11f)
+ b, _ = hex.DecodeString("3021a11f060a2b0601040183bf300108a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "no OtherName found") {
+ t.Fatalf("expected error with no GeneralName, got %v", err)
+ }
+
+ // failure: invalid OtherName (Change tag of UTF8String field to 1: a0110c0f -> a1110c0f)
+ b, _ = hex.DecodeString("3021a01f060a2b0601040183bf300108a1110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "could not parse requested OtherName SAN") {
+ t.Fatalf("expected error with invalid OtherName, got %v", err)
+ }
+
+ // failure: OtherName has wrong OID (2b0601040183bf300107 -> 2b0601040183bf300108)
+ b, _ = hex.DecodeString("3021a01f060a2b0601040183bf300108a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "unexpected OID for OtherName") {
+ t.Fatalf("expected error with wrong OID, got %v", err)
+ }
+
+ // failure: multiple OtherName fields (Increase sequence size from 0x21 -> 0x42, duplicate OtherName)
+ b, _ = hex.DecodeString("3042a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6da01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
+ ext = &pkix.Extension{
+ Id: SANOID,
+ Critical: true,
+ Value: b,
+ }
+ _, err = UnmarshalOtherNameSAN([]pkix.Extension{*ext})
+ if err == nil || !strings.Contains(err.Error(), "expected only one OtherName") {
+ t.Fatalf("expected error with multiple OtherName fields, got %v", err)
+ }
+}
+
+func TestGetSubjectAltnernativeNames(t *testing.T) {
+ rootCert, rootKey, _ := test.GenerateRootCa()
+ subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey)
+
+ // generate with OtherName, which will override other SANs
+ ext, err := MarshalOtherNameSAN("subject-othername", true)
+ if err != nil {
+ t.Fatalf("error marshalling SANs: %v", err)
+ }
+ exts := []pkix.Extension{*ext}
+ leafCert, _, _ := test.GenerateLeafCert("unused", "oidc-issuer", subCert, subKey, exts...)
+
+ sans := GetSubjectAlternateNames(leafCert)
+ if len(sans) != 1 {
+ t.Fatalf("expected 1 SAN field, got %d", len(sans))
+ }
+ if sans[0] != "subject-othername" {
+ t.Fatalf("unexpected OtherName SAN value")
+ }
+
+ // generate with all other SANs
+ leafCert, _, _ = test.GenerateLeafCertWithSubjectAlternateNames([]string{"subject-dns"}, []string{"subject-email"}, []net.IP{{1, 2, 3, 4}}, []*url.URL{{Path: "testURL"}}, "oidc-issuer", subCert, subKey)
+ sans = GetSubjectAlternateNames(leafCert)
+ if len(sans) != 4 {
+ t.Fatalf("expected 1 SAN field, got %d", len(sans))
+ }
+ if sans[0] != "subject-dns" {
+ t.Fatalf("unexpected DNS SAN value")
+ }
+ if sans[1] != "subject-email" {
+ t.Fatalf("unexpected email SAN value")
+ }
+ if sans[2] != "1.2.3.4" {
+ t.Fatalf("unexpected IP SAN value")
+ }
+ if sans[3] != "testURL" {
+ t.Fatalf("unexpected URL SAN value")
+ }
+}
diff --git a/pkg/fulcioroots/doc.go b/pkg/fulcioroots/doc.go
new file mode 100644
index 0000000..f483104
--- /dev/null
+++ b/pkg/fulcioroots/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package fulcioroots assists with extracting trust root information for Fulcio
+package fulcioroots
diff --git a/pkg/fulcioroots/fulcioroots.go b/pkg/fulcioroots/fulcioroots.go
new file mode 100644
index 0000000..d6cf901
--- /dev/null
+++ b/pkg/fulcioroots/fulcioroots.go
@@ -0,0 +1,133 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package fulcioroots fetches Fulcio root and intermediate certificates from TUF metadata
+package fulcioroots
+
+import (
+ "bytes"
+ "context"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "sync"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "github.com/sigstore/sigstore/pkg/tuf"
+)
+
+var (
+ rootsOnce sync.Once
+ roots []*x509.Certificate
+ intermediates []*x509.Certificate
+ singletonRootErr error
+)
+
+// This is the root in the fulcio project.
+var fulcioTargetStr = `fulcio.crt.pem`
+
+// This is the v1 migrated root.
+var fulcioV1TargetStr = `fulcio_v1.crt.pem`
+
+// This is the untrusted v1 intermediate CA certificate, used or chain building.
+var fulcioV1IntermediateTargetStr = `fulcio_intermediate_v1.crt.pem`
+
+// Get returns the Fulcio root certificate.
+func Get() (*x509.CertPool, error) {
+ pool := x509.NewCertPool()
+ if err := GetWithCertPool(pool); err != nil {
+ return nil, err
+ }
+ return pool, nil
+}
+
+// GetWithCertPool returns the Fulcio root certificate appended to the given CertPool.
+func GetWithCertPool(pool *x509.CertPool) error {
+ rootsOnce.Do(func() {
+ roots, intermediates, singletonRootErr = initRoots()
+ if singletonRootErr != nil {
+ return
+ }
+ })
+ if singletonRootErr != nil {
+ return singletonRootErr
+ }
+
+ for _, c := range roots {
+ pool.AddCert(c)
+ }
+ return nil
+}
+
+// GetIntermediates returns the Fulcio intermediate certificates.
+func GetIntermediates() (*x509.CertPool, error) {
+ pool := x509.NewCertPool()
+ if err := GetIntermediatesWithCertPool(pool); err != nil {
+ return nil, err
+ }
+ return pool, nil
+}
+
+// GetIntermediatesWithCertPool returns the Fulcio intermediate certificates appended to the given CertPool.
+func GetIntermediatesWithCertPool(pool *x509.CertPool) error {
+ rootsOnce.Do(func() {
+ roots, intermediates, singletonRootErr = initRoots()
+ if singletonRootErr != nil {
+ return
+ }
+ })
+ if singletonRootErr != nil {
+ return singletonRootErr
+ }
+
+ for _, c := range intermediates {
+ pool.AddCert(c)
+ }
+ return nil
+}
+
+func initRoots() ([]*x509.Certificate, []*x509.Certificate, error) {
+ tufClient, err := tuf.NewFromEnv(context.Background())
+ if err != nil {
+ return nil, nil, fmt.Errorf("initializing tuf: %w", err)
+ }
+ // Retrieve from the embedded or cached TUF root. If expired, a network
+ // call is made to update the root.
+ targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr, fulcioV1IntermediateTargetStr})
+ if err != nil {
+ return nil, nil, fmt.Errorf("error getting targets: %w", err)
+ }
+ if len(targets) == 0 {
+ return nil, nil, errors.New("none of the Fulcio roots have been found")
+ }
+ rootPool := []*x509.Certificate{}
+ intermediatePool := []*x509.Certificate{}
+ for _, t := range targets {
+ certs, err := cryptoutils.UnmarshalCertificatesFromPEM(t.Target)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error unmarshalling certificates: %w", err)
+ }
+ for _, cert := range certs {
+ // root certificates are self-signed
+ if bytes.Equal(cert.RawSubject, cert.RawIssuer) {
+ rootPool = append(rootPool, cert)
+ } else {
+ intermediatePool = append(intermediatePool, cert)
+ }
+ }
+ }
+
+ return rootPool, intermediatePool, nil
+}
diff --git a/pkg/oauth/doc.go b/pkg/oauth/doc.go
new file mode 100644
index 0000000..10b8e8e
--- /dev/null
+++ b/pkg/oauth/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oauth contains types and utilities related to OAuth2.
+package oauth
diff --git a/pkg/oauth/interactive.go b/pkg/oauth/interactive.go
new file mode 100644
index 0000000..e745911
--- /dev/null
+++ b/pkg/oauth/interactive.go
@@ -0,0 +1,80 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oauth implements OAuth/OIDC support for device and token flows
+package oauth
+
+const (
+ // InteractiveSuccessHTML is the page displayed upon success when using a web browser during an interactive Oauth token flow.
+ InteractiveSuccessHTML = `<!DOCTYPE html>
+<html>
+ <head>
+ <title>Sigstore Authentication</title>
+ <link id="favicon" rel="icon" type="image/svg"/>
+ <style>
+ :root { font-family: "Trebuchet MS", sans-serif; height: 100%; color: #444444; overflow: hidden; }
+ body { display: flex; justify-content: center; height: 100%; margin: 0 10%; background: #FFEAD7; }
+ .container { display: flex; flex-direction: column; justify-content: space-between; }
+ .sigstore { color: #2F2E71; font-weight: bold; }
+ .header { position: absolute; top: 30px; left: 22px; }
+ .title { font-size: 3.5em; margin-bottom: 30px; animation: 750ms ease-in-out 0s 1 show; }
+ .content { font-size: 1.5em; animation: 250ms hide, 750ms ease-in-out 250ms 1 show; }
+ .anchor { position: relative; }
+ .links { display: flex; justify-content: space-between; font-size: 1.2em; padding: 60px 0; position: absolute; bottom: 0; left: 0; right: 0; animation: 500ms hide, 750ms ease-in-out 500ms 1 show; }
+ .link { color: #444444; text-decoration: none; user-select: none; }
+ .link:hover { color: #6349FF; }
+ .link:hover>.arrow { transform: scaleX(1.5) translateX(3px); }
+ .link:hover>.sigstore { color: inherit; }
+ .link, .arrow { transition: 200ms; }
+ .arrow { display: inline-block; margin-left: 6px; transform: scaleX(1.5); }
+ @keyframes hide { 0%, 100% { opacity: 0; } }
+ @keyframes show { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; } }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div>
+ <a class="header" href="https://sigstore.dev">
+ <svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="28.14" height="30.3">
+ <circle r="7" cx="14" cy="15" fill="#FFEAD7"></circle>
+ <path fill="#2F2E71" d="M27.8 10.9c-.3-1.2-.9-2.2-1.7-3.1-.6-.7-1.3-1.3-2-2-.7-.6-1.2-1.3-1.5-2.1-.2-.4-.4-.8-.7-1.2-.5-.7-1.3-1.2-2.1-1.6-1.3-.7-2.7-.9-4.2-.9-.8 0-1.6.1-2.4.3-1.2.2-2.3.7-3.4 1.3-.7.4-1.3.9-1.9 1.4-1 .8-2 1.6-2.8 2.6-.6.8-1.4 1.3-2.2 1.8-.8.4-1.4 1-2 1.6-.6.6-.9 1.3-.9 2.1 0 .6.1 1.2.2 1.7.2.9.6 1.7.9 2.6.2.5.3 1 .3 1.5s0 1-.1 1.5c-.1 1.1 0 2.3.2 3.4.2 1 .8 1.8 1.8 2.2.1.1.3.1.4.1.2.1.2.2.1.3l-.1.1c-.4.5-.7 1.1-.6 1.8.1 1.1 1.3 1.8 2.3 1.3.6-.2 1.2 0 1.4.4.1.1.1.2.2.3.2.5.4.9.7 1.3.4.5.9.7 1.6.6.4-.1.8-.2 1.2-.4.7-.4 1.3-.9 2-1.5.2-.2.4-.2.7-.2.4 0 .8.2 1.2.5.6.4 1.2.7 1.9.9 1.3.4 2.5.5 3.8.2 1.3-.3 2.4-.9 3.4-1.6.7-.5 1.2-1 1.6-1.7.4-.7.6-1.4.8-2.2.3-1.1.4-2.2.4-3.4.1-1 .2-1.9.5-2.8.2-.7.5-1.4.8-2.1.2-.6.4-1.2.5-1.9.1-1.1 0-2.1-.3-3.1zM14.9.8c.3-.1.7-.1 1-.1h.3c1.1 0 2.1.2 3.1.5.6.2 1.2.6 1.7 1s.7.9.9 1.4v.1c0 .1 0 .2-.1.2s-.1 0-.2-.1c-.4-.4-.7-.8-1.1-1.1-.6-.5-1.2-.9-2-1.1-1.1-.3-2.1-.5-3.2-.7h-.6c.1 0 .1 0 .2-.1-.1 0 0 0 0 0zm-4.5 12.4c.6 0 1.1.5 1.2 1.2 0 .6-.5 1.2-1.2 1.2-.6 0-1.2-.5-1.1-1.2 0-.7.5-1.2 1.1-1.2zm3.8 1.3v-3.4c0-2.3 2-3.1 3.6-2.5.3.1.6.3.9.5.2.2.2.5.1.8-.2.2-.4.3-.7.1-.2-.1-.5-.2-.7-.3-.6-.2-1.3 0-1.6.4-.1.2-.2.4-.2.7-.1.5 0 .9 0 1.4v5.9c0 1.2-.6 2.1-1.8 2.4-1 .3-1.9.2-2.7-.6-.2-.2-.3-.5-.1-.7.1-.2.4-.3.7-.2.3.1.6.3.9.4 1 .1 1.7-.3 1.7-1.4-.1-1.2-.1-2.3-.1-3.5zm-8.8 7.6h-.1c-.1-.1-.2-.1-.3-.2-.2-.2-.4-.3-.6-.5-.3-.3-.5-.6-.7-1-.4-.8-.8-1.7-1-2.7-.1-.5-.2-1-.2-1.5s-.1-1-.2-1.4c-.1-.7-.2-1.5-.2-2.2 0-.9.1-1.7.4-2.5.3-.9.7-1.7 1.4-2.4.6-.6 1.1-1.2 1.7-1.8.1-.1.3-.2.4-.2 0 .1-.1.3-.2.4-.3.4-.6.7-.9 1.1-.5.6-.9 1.2-1.2 1.8-.4.7-.7 1.4-.9 2.2-.1.4-.2.8-.2 1.2 0 .4-.1.8 0 1.3 0 .6.1 1.1.2 1.6.1.6.2 1.1.2 1.7 0 .7.2 1.4.4 2.1 0 .2.2.3.2.5.3.6.6 1.1 1.1 1.5.2.2.4.5.6.7 0 0 0 .1.1.1v.2zM8 24.6c-.4 0-.7.1-1.1.2-.4.1-.6-.1-.7-.5 0-.1-.1-.3 0-.4.1-.3.3-.3.5-.1.2.2.5.4.7.5.1.1.2.1.4.1.1 0 .2.1.4.2H8zm7.6 2.1c-.3.2-.7.3-1.1.3-.3 0-.6-.1-.9-.1h-.2c-.4.1-.7.1-1.1.2-.1 0-.3 0-.4.1H11c-.4 0-.7-.2-1-.5-.1-.1-.2-.3-.3-.5-.1-.1-.1-.2-.1-.4 0-.1.1-.1.2-.1h.1c.5.3 1.1.4 1.6.5.7.1 1.4.2 2.1.2.4 0 .7.1 1.1.1h.8c.2.1.1.1.1.2zm3.7-2.5c-.7.4-1.5.7-2.3.9-.2 0-.5.1-.7.1-.2 0-.5 0-.7.1-.4.1-.8 0-1.2 0-.3 0-.6-.1-.9 0h-.2c-.4-.1-.9-.2-1.3-.3-.5-.1-1-.3-1.4-.5-.4-.1-.8-.3-1.1-.5-.2-.1-.4-.3-.6-.4-.6-.6-1.2-1.1-1.7-1.6-.4-.5-.8-.9-1.2-1.4-.4-.6-.7-1.2-1-1.9l-.3-.9c-.1-.3-.2-.5-.2-.8v-.8c.3.8.5 1.7.9 2.5.7 1.6 1.7 3 3 4.1 1.4 1.1 2.9 1.8 4.6 2.1.9.2 1.8.2 2.7.2 1.1-.1 2.2-.3 3.2-.8.2-.1.3-.2.5-.2 0 .1 0 .1-.1.1zm.1-8.7c-.6 0-1.1-.5-1.1-1.2 0-.6.5-1.2 1.2-1.2.6 0 1.1.5 1.1 1.2s-.5 1.3-1.2 1.2zm6.2 5.7c0 .4-.1.8-.2 1.2-.1.4-.1.9-.3 1.3-.1.4-.2.7-.4 1.1-.1.3-.3.6-.6.8-.3.2-.5.4-.9.5-.4.2-.7.3-1.2.3h-.9c-.2-.1-.2-.1-.1-.3.1-.2.3-.3.5-.4.3-.2.6-.5.8-.7.7-.7 1.3-1.6 1.9-2.4.4-.4.6-1 .9-1.5.1-.1.1-.2.2-.3.3.2.3.3.3.4zm-15-16.8c1.7-.8 3.5-1.1 5.3-.9.4 0 .8.1 1.1.3l1.8.6c.6.2 1.2.5 1.7.8.7.4 1.3.9 1.9 1.5.8.8 1.5 1.6 2 2.6.3.6.5 1.2.7 1.8.2.7.4 1.5.4 2.2v.9c0 .4-.1.8-.1 1.2v-1c0-1.2-.3-2.3-.6-3.4l-.6-1.5c-.2-.6-.5-1.1-.9-1.6-.1-.1-.3-.1-.4-.2-.1 0-.1 0-.2-.1-.5-.5-1.1-1-1.7-1.5-.8-.6-1.7-1.1-2.6-1.4-.4-.2-.8-.3-1.2-.4-.9-.2-1.8-.4-2.7-.3h-.9c-.3 0-.6.1-1 .2-.6.1-1.2.3-1.7.5h-.1s-.1 0 0-.1c0 0 0-.1-.1-.2m16.2 11.1c-.1-.8 0-1.7 0-2.5-.1-.8-.2-1.6-.4-2.4.5.7.6 1.6.7 2.4 0 .8 0 1.7-.3 2.5zm.6.5c0-.3.1-.7.2-1.1.1-.4.1-.9.1-1.3v-.4c0-.8-.2-1.6-.4-2.4-.4-.9-.8-1.6-1.4-2.4-.5-.6-.9-1.2-1.4-1.8l-.2-.2c.1 0 .1 0 .1.1 1 .8 1.8 1.6 2.4 2.7.5 1 .9 2 1 3.1.3 1.3.1 2.5-.4 3.7z"/>
+ </svg><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="120" height="30.3" viewBox="28.14 0 120 30.3">
+ <path fill="#2F2E71" d="M57.7 18c.9 0 1.9-.1 2.9.3.9.3 1.5.9 1.7 2 .1 1-.2 1.9-1.1 2.5-1.1.8-2.3.9-3.6 1-1.4 0-2.9 0-4.3-.6-1.6-.7-1.8-2.6-.4-3.6.3-.2.2-.3.1-.5-.7-.8-.7-2.2.3-2.8.3-.2.2-.3 0-.6-1.4-1.6-.7-4.1 1.3-4.8 1.6-.6 3.2-.6 4.8-.1.2.1.3.1.5-.1.3-.3.6-.5.9-.7.5-.3 1.1-.3 1.4 0 .3.4.3.9-.2 1.3-.7.5-.8 1-.6 1.9.4 1.5-.6 2.9-2.1 3.4-1.3.5-2.7.5-4 .2-.3-.1-.5-.1-.6.2-.1.3-.1.6.2.8.2.1.5.2.8.2h2zm-.6-2.7c.3 0 .5 0 .7-.1.8-.2 1.3-.7 1.4-1.4.1-.7-.3-1.3-.9-1.6-.8-.3-1.6-.3-2.4 0-1 .4-1.2 1.7-.6 2.4.5.6 1.2.7 1.8.7zm-.2 4.6h-1.8c-.4 0-.6.2-.7.5-.3.7.1 1.2.9 1.4 1.2.2 2.5.2 3.7-.1l.6-.3c.5-.3.4-1.1-.1-1.3-.2-.1-.5-.2-.7-.2h-1.9zm58.6-3.3h-3.2c-.3 0-.3.1-.3.4.2 1.2 1.3 2.1 2.8 2.3 1.1.1 2.1-.1 2.9-.9.2-.2.4-.3.6-.4.4-.2.8-.2 1.1.1.4.3.4.7.3 1.1-.3.9-.9 1.4-1.7 1.8-2.3 1-4.5.9-6.5-.6-1-.7-1.5-1.8-1.7-3-.3-1.7-.2-3.3.8-4.7 1.3-1.8 3.1-2.4 5.2-2.1 2 .3 3.4 1.3 4 3.2.2.6.3 1.2.2 1.8-.1.8-.4 1.1-1.2 1.1-1.1-.1-2.2-.1-3.3-.1zm-.7-1.9h2.4c.4 0 .4-.1.4-.5-.3-1.1-1.2-1.9-2.5-2-1.5-.1-2.6.6-3.1 1.9-.2.5-.1.6.4.6h2.4zm-23 6.7c-3.3 0-5.5-2.2-5.6-5.5 0-3.2 2.2-5.6 5.4-5.6 3.4 0 5.7 2.2 5.8 5.5 0 3.4-2.2 5.6-5.6 5.6zm0-2.1c1.9 0 3.2-1.3 3.3-3.3.1-2-1.3-3.4-3.2-3.4-1.8 0-3.2 1.4-3.2 3.3-.1 1.9 1.2 3.4 3.1 3.4zm-22.6 2.1c-1.1 0-2.4-.3-3.5-1.4-.3-.4-.6-.8-.7-1.3 0-.4.1-.7.4-.9.3-.2.7-.2 1 0 .3.2.6.4.8.6.9.9 2 1.1 3.2.9.6-.1 1-.5 1-1 .1-.5-.2-1-.8-1.2-.7-.3-1.4-.3-2.1-.5-.8-.2-1.6-.4-2.2-.9-1.5-1.2-1.4-3.5.2-4.6 1.2-.8 2.6-.9 4-.7 1 .1 1.9.5 2.6 1.2.3.3.4.6.5.9.1.4 0 .7-.3 1-.3.3-.7.3-1 .1-.4-.2-.7-.5-1.1-.8-.8-.6-1.7-.8-2.7-.5-.6.1-.9.5-.9 1s.3.9.8 1.1c.9.3 1.9.4 2.9.7.6.2 1.1.4 1.5.8 1.5 1.4 1.1 4.1-.9 5.1-.7.3-1.5.4-2.7.4zm-30.3 0c-1.5 0-3-.3-4.1-1.5-.3-.3-.4-.6-.5-1-.1-.4-.1-.8.3-1.1.4-.2.9-.2 1.3.1.3.2.6.5.8.7.9.8 1.9.9 3 .7.6-.1.9-.5.9-1.1 0-.5-.3-1-.8-1.2l-2.7-.6c-1.1-.3-2-.8-2.4-2-.6-1.7.4-3.4 2.2-3.9 1.7-.5 3.3-.3 4.8.5.6.3 1 .8 1.2 1.4.1.4.1.9-.3 1.1-.4.3-.8.2-1.2-.1-.4-.3-.7-.7-1.2-.9-.8-.4-1.5-.5-2.4-.3-.5.1-.8.4-.8.9s.2.8.7 1c.7.3 1.4.3 2.1.5.8.2 1.5.3 2.2.8 1.2.8 1.5 2.5.9 3.8-.7 1.4-1.9 1.8-3.3 2-.3.2-.5.2-.7.2zM78 15.8v-2.6c0-.3-.1-.4-.4-.4h-1.3c-.6-.1-.9-.5-.9-1s.4-1 .9-1h1.3c.3 0 .4-.1.4-.4V8.9c0-.7.5-1.1 1.1-1.2.6 0 1.1.4 1.2 1v.6c0 .4-.2 1 .1 1.3.3.3.9.1 1.3.1h1.6c.4 0 .7.3.8.8.1.4-.1.8-.4 1.1-.3.2-.6.2-.9.2h-2.1c-.3 0-.4 0-.4.4v4.7c0 1.1.9 1.6 1.9 1.1.3-.1.5-.3.7-.5.4-.3.8-.3 1.2 0 .4.3.4.8.2 1.2-.3.7-.9 1.1-1.5 1.3-1.3.5-2.6.5-3.8-.4-.7-.6-1-1.4-1.1-2.4.1-.7.1-1.6.1-2.4zm24.7-4.1c.8-1 1.8-1.4 3-1.3.7.1 1.4.3 1.9.9.3.4.5.9.4 1.5-.1.4-.3.7-.7.9-.5.2-.9 0-1.3-.3-.7-.7-1.3-.9-2.1-.5-.5.3-.8.7-1 1.3-.2.5-.3 1.1-.3 1.6v4.5c0 .5-.2.9-.6 1.1-.4.2-.8.2-1.2-.1-.4-.3-.5-.7-.5-1.1v-8.4c0-.7.4-1.2 1-1.3.7-.1 1.1.3 1.3 1.1.1-.1.1 0 .1.1zm-54 4.2v4.3c0 .8-.6 1.3-1.4 1.2-.5-.1-.9-.5-.9-1.1v-8.7c0-.7.5-1.1 1.2-1.1s1.1.4 1.2 1.2c-.1 1.3-.1 2.8-.1 4.2zm.3-8.2c0 .8-.6 1.4-1.4 1.4-.8 0-1.5-.6-1.5-1.4 0-.8.7-1.4 1.5-1.4s1.4.6 1.4 1.4z"/>
+ </svg>
+ </a>
+ </div>
+ <div>
+ <div class="title">
+ <span class="sigstore">sigstore </span>
+ <span>authentication successful!</span>
+ </div>
+ <div class="content">
+ <span>You may now close this page.</span>
+ </div>
+ </div>
+ <div class="anchor">
+ <div class="links">
+ <a href="https://sigstore.dev/" class="link login"><span class="sigstore">sigstore</span> home <span class="arrow">→</span></a>
+ <a href="https://docs.sigstore.dev/" class="link login"><span class="sigstore">sigstore</span> documentation <span class="arrow">→</span></a>
+ <a href="https://blog.sigstore.dev/" class="link"><span class="sigstore">sigstore</span> blog <span class="arrow">→</span></a>
+ </div>
+ </div>
+ </div>
+ <script>
+ document.getElementById("favicon").setAttribute("href", "data:image/svg+xml," + encodeURIComponent(document.getElementById("logo").outerHTML));
+ </script>
+ </body>
+</html>
+`
+)
diff --git a/pkg/oauth/internal/doc.go b/pkg/oauth/internal/doc.go
new file mode 100644
index 0000000..cd5d3c1
--- /dev/null
+++ b/pkg/oauth/internal/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package internal contains utilities for parsing OAuth2 tokens
+package internal
diff --git a/pkg/oauth/internal/token.go b/pkg/oauth/internal/token.go
new file mode 100644
index 0000000..ead4595
--- /dev/null
+++ b/pkg/oauth/internal/token.go
@@ -0,0 +1,159 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package internal includes functions to support OAuth/OIDC
+package internal
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+
+ "golang.org/x/oauth2"
+)
+
+// var for testing
+var currentTime = time.Now
+
+// tokenRespJSON is the struct representing the JSON form of a RFC6749 access token success response.
+// See: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
+type tokenRespJSON struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ RefreshToken string `json:"refresh_token"`
+ ExpiresIn int `json:"expires_in"`
+ Scope string `json:"scope"`
+}
+
+func parseFormURLEncodedAccessTokenSuccess(body []byte) (token *oauth2.Token, err error) {
+ vals, err := url.ParseQuery(string(body))
+ if err != nil {
+ return nil, err
+ }
+
+ token = &oauth2.Token{
+ AccessToken: vals.Get("access_token"),
+ TokenType: vals.Get("token_type"),
+ RefreshToken: vals.Get("refresh_token"),
+ }
+ ei := vals.Get("expires_in")
+ if ei != "" {
+ expires, err := strconv.Atoi(ei)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to parse "expires_in": %w`, err)
+ }
+ if expires != 0 {
+ token.Expiry = currentTime().Add(time.Duration(expires) * time.Second)
+ }
+ }
+ return token.WithExtra(vals), nil
+}
+
+func parseJSONAccessTokenSuccess(body []byte) (token *oauth2.Token, err error) {
+ var tj tokenRespJSON
+ if err = json.Unmarshal(body, &tj); err != nil {
+ return nil, err
+ }
+ token = &oauth2.Token{
+ AccessToken: tj.AccessToken,
+ TokenType: tj.TokenType,
+ RefreshToken: tj.RefreshToken,
+ }
+ if tj.ExpiresIn != 0 {
+ token.Expiry = currentTime().Add(time.Duration(tj.ExpiresIn) * time.Second)
+ }
+ raw := map[string]interface{}{}
+ if err = json.Unmarshal(body, &raw); err != nil {
+ return nil, err
+ }
+ return token.WithExtra(raw), nil
+}
+
+func parseAccessTokenSuccess(body []byte, contentType string) (token *oauth2.Token, err error) {
+ if contentType == "application/x-www-form-urlencoded" {
+ return parseFormURLEncodedAccessTokenSuccess(body)
+ }
+ return parseJSONAccessTokenSuccess(body)
+}
+
+// ErrorTokenResponse represents an RFC6749 access token error response.
+// See: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
+type ErrorTokenResponse struct {
+ Code string `json:"error"`
+ Description string `json:"error_description,omitempty"`
+ URI string `json:"error_uri,omitempty"`
+}
+
+// Error implements `error`
+func (e *ErrorTokenResponse) Error() string {
+ str, _ := json.Marshal(e)
+ return string(str)
+}
+
+func parseFormURLEncodedAccessTokenError(body []byte) (*ErrorTokenResponse, error) {
+ vals, parseErr := url.ParseQuery(string(body))
+ if parseErr != nil {
+ return nil, parseErr
+ }
+ return &ErrorTokenResponse{
+ Code: vals.Get("error"),
+ Description: vals.Get("error_description"),
+ URI: vals.Get("error_uri"),
+ }, nil
+}
+
+func parseJSONAccessTokenError(body []byte) (respErr *ErrorTokenResponse, parseErr error) {
+ var ej ErrorTokenResponse
+ if parseErr = json.Unmarshal(body, &ej); parseErr != nil {
+ return nil, parseErr
+ }
+ return &ej, nil
+}
+
+func parseAccessTokenError(body []byte, contentType string) (respErr *ErrorTokenResponse, parseErr error) {
+ if contentType == "application/x-www-form-urlencoded" {
+ return parseFormURLEncodedAccessTokenError(body)
+ }
+ return parseJSONAccessTokenError(body)
+}
+
+// ParseAccessTokenResponse parses an RFC6749 access token response and returns either an `*oauth2.Token` on success, an `*ErrorTokenResponse` on failure, or any other error if the response cannot be parsed.
+// See: https://datatracker.ietf.org/doc/html/rfc6749#section-5
+func ParseAccessTokenResponse(tokenResp *http.Response) (token *oauth2.Token, err error) {
+ body, err := io.ReadAll(io.LimitReader(tokenResp.Body, 1<<20))
+ if err != nil {
+ return nil, err
+ }
+ contentType, _, _ := mime.ParseMediaType(tokenResp.Header.Get("Content-Type"))
+ if tokenResp.StatusCode != http.StatusOK {
+ respErr, parseErr := parseAccessTokenError(body, contentType)
+ if parseErr != nil {
+ return nil, parseErr
+ }
+ return nil, respErr
+ }
+ token, err = parseAccessTokenSuccess(body, contentType)
+ if err != nil {
+ return nil, err
+ }
+ if token.AccessToken == "" {
+ return nil, fmt.Errorf(`response did not contain "access_token". Content-Type: %q, Body: %s`, contentType, string(body))
+ }
+ return token, nil
+}
diff --git a/pkg/oauth/internal/token_test.go b/pkg/oauth/internal/token_test.go
new file mode 100644
index 0000000..1af062b
--- /dev/null
+++ b/pkg/oauth/internal/token_test.go
@@ -0,0 +1,231 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestParseAccessTokenSuccessResponse(t *testing.T) {
+ now := currentTime()
+ realCurrentTime := currentTime
+ currentTime = func() time.Time { return now }
+ defer func() { currentTime = realCurrentTime }()
+
+ testCases := []struct {
+ desc string
+ respContentType string
+ respBody []byte
+
+ wantAccessToken string
+ wantRefreshToken string
+ wantTokenType string
+ wantExpiry time.Time
+ mustHaveExtras map[string]interface{}
+
+ wantError error
+ }{
+ {
+ desc: "json",
+
+ respContentType: "application/json",
+ respBody: []byte(`{
+ "access_token": "json access_token",
+ "token_type": "Bearer",
+ "refresh_token": "json refresh_token",
+ "expires_in": 3600,
+ "scope": "foo bar"
+ }`),
+
+ wantAccessToken: "json access_token",
+ wantRefreshToken: "json refresh_token",
+ wantTokenType: "Bearer",
+ wantExpiry: now.Add(3600 * time.Second),
+ mustHaveExtras: map[string]interface{}{
+ "scope": "foo bar",
+ },
+ },
+ {
+ desc: "json no access_token",
+
+ respContentType: "application/json",
+ respBody: []byte(`{"token_type":"Bearer","expires_in":3600,"scope":"foo bar"}`),
+
+ wantError: fmt.Errorf(`response did not contain "access_token". Content-Type: %q, Body: %s`, "application/json", `{"token_type":"Bearer","expires_in":3600,"scope":"foo bar"}`),
+ },
+ {
+ desc: "bad json",
+
+ respContentType: "application/json",
+ respBody: []byte(`"token_type":"Bearer","expires_in":3600,"scope":"foo bar"`),
+
+ wantError: errors.New("invalid character ':' after top-level value"),
+ },
+ {
+ desc: "x-www-form-urlencoded",
+
+ respContentType: "application/x-www-form-urlencoded",
+ respBody: []byte(url.Values{
+ "access_token": []string{"urlencoded access_token"},
+ "token_type": []string{"MAC"},
+ "refresh_token": []string{"urlencoded refresh_token"},
+ "expires_in": []string{"420"},
+ "scope": []string{"bar baz"},
+ }.Encode()),
+
+ wantAccessToken: "urlencoded access_token",
+ wantRefreshToken: "urlencoded refresh_token",
+ wantTokenType: "MAC",
+ wantExpiry: now.Add(420 * time.Second),
+ mustHaveExtras: map[string]interface{}{
+ "scope": "bar baz",
+ },
+ },
+ {
+ desc: "expires_in unparsable",
+
+ respContentType: "application/x-www-form-urlencoded",
+ respBody: []byte(url.Values{
+ "access_token": []string{"urlencoded access_token"},
+ "token_type": []string{"Bearer"},
+ "expires_in": []string{"unparsable"},
+ }.Encode()),
+
+ wantError: errors.New("failed to parse \"expires_in\": strconv.Atoi: parsing \"unparsable\": invalid syntax"),
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ testResp := &http.Response{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(bytes.NewReader(tc.respBody)),
+ }
+ if tc.respContentType != "" {
+ testResp.Header = make(http.Header, 1)
+ testResp.Header.Set("Content-Type", tc.respContentType)
+ }
+
+ token, err := ParseAccessTokenResponse(testResp)
+ if err == nil && tc.wantError != nil {
+ t.Fatalf("ParseAccessTokenResponse should have returned an error, got %v", token)
+ }
+ if err != nil {
+ if tc.wantError != nil {
+ gotErr := err.Error()
+ wantErr := tc.wantError.Error()
+ if gotErr != wantErr {
+ t.Fatalf("ParseAccessTokenResponse should have returned error %q, got %q", wantErr, gotErr)
+ }
+ return
+ }
+ t.Fatalf("ParseAccessTokenResponse returned error: %v", err)
+ }
+ if token.AccessToken != tc.wantAccessToken {
+ t.Errorf("Wanted AccessToken %q, got %q", tc.wantAccessToken, token.AccessToken)
+ }
+ if token.RefreshToken != tc.wantRefreshToken {
+ t.Errorf("Wanted RefreshToken %q, got %q", tc.wantRefreshToken, token.RefreshToken)
+ }
+ if token.TokenType != tc.wantTokenType {
+ t.Errorf("Wanted TokenType %q, got %q", tc.wantTokenType, token.TokenType)
+ }
+ if token.Expiry != tc.wantExpiry {
+ t.Errorf("Wanted Expiry %v, got %v", tc.wantExpiry, token.Expiry)
+ }
+ for k, v := range tc.mustHaveExtras {
+ gotVal := token.Extra(k)
+ if !reflect.DeepEqual(v, gotVal) {
+ t.Errorf("Wanted Extra(%q)=%v, got %v", k, v, gotVal)
+ }
+ }
+ })
+ }
+}
+
+func TestParseAccessTokenFailResponse(t *testing.T) {
+ testCases := []struct {
+ desc string
+ respStatusCode int
+ respContentType string
+ respBody []byte
+
+ wantError error
+ }{
+ {
+ desc: "json",
+
+ respStatusCode: http.StatusBadRequest,
+ respContentType: "application/json",
+ respBody: []byte(`{"error":"invalid_request","error_description":"description of error","error_uri":"https://test.example.com/json"}`),
+
+ wantError: &ErrorTokenResponse{
+ Code: "invalid_request",
+ Description: "description of error",
+ URI: "https://test.example.com/json",
+ },
+ },
+ {
+ desc: "x-www-form-urlencoded",
+
+ respStatusCode: http.StatusBadRequest,
+ respContentType: "application/x-www-form-urlencoded",
+ respBody: []byte(url.Values{
+ "error": []string{"invalid_request"},
+ "error_description": []string{"description of error"},
+ "error_uri": []string{"https://test.example.com/form-urlencoded"},
+ }.Encode()),
+
+ wantError: &ErrorTokenResponse{
+ Code: "invalid_request",
+ Description: "description of error",
+ URI: "https://test.example.com/form-urlencoded",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ testResp := &http.Response{
+ StatusCode: tc.respStatusCode,
+ Body: io.NopCloser(bytes.NewReader(tc.respBody)),
+ }
+ if tc.respContentType != "" {
+ testResp.Header = make(http.Header, 1)
+ testResp.Header.Set("Content-Type", tc.respContentType)
+ }
+
+ token, err := ParseAccessTokenResponse(testResp)
+ if err == nil {
+ t.Fatalf("ParseAccessTokenResponse should have failed, got: %v", token)
+ }
+
+ gotErr := err.Error()
+ wantErr := tc.wantError.Error()
+
+ if gotErr != wantErr {
+ t.Errorf("ParseAccessTokenResponse should have returned error %q, got %q", wantErr, gotErr)
+ }
+ })
+ }
+}
diff --git a/pkg/oauth/oidc/doc.go b/pkg/oauth/oidc/doc.go
new file mode 100644
index 0000000..88a3498
--- /dev/null
+++ b/pkg/oauth/oidc/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oidc contains utilities related to OIDC tokens.
+package oidc
diff --git a/pkg/oauth/oidc/interactive.go b/pkg/oauth/oidc/interactive.go
new file mode 100644
index 0000000..a3ef49d
--- /dev/null
+++ b/pkg/oauth/oidc/interactive.go
@@ -0,0 +1,209 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oidc implements support for fetching OIDC tokens
+package oidc
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+
+ coreoidc "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/pkg/browser"
+ "github.com/segmentio/ksuid"
+ "github.com/sigstore/sigstore/pkg/oauth"
+ "golang.org/x/oauth2"
+)
+
+const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
+
+type browserOpener func(url string) error
+
+func doOobFlow(cfg *oauth2.Config, stateToken string, opts []oauth2.AuthCodeOption) string {
+ cfg.RedirectURL = oobRedirectURI
+
+ authURL := cfg.AuthCodeURL(stateToken, opts...)
+ fmt.Fprintln(os.Stderr, "Go to the following link in a browser:\n\n\t", authURL)
+ fmt.Fprintf(os.Stderr, "Enter verification code: ")
+ var code string
+ fmt.Scanln(&code)
+ return code
+}
+
+func startRedirectListener(state, htmlPage, redirectURL string, codeCh chan string, errCh chan error) (*http.Server, *url.URL, error) {
+ var listener net.Listener
+ var urlListener *url.URL
+ var err error
+
+ if redirectURL == "" {
+ listener, err = net.Listen("tcp", "localhost:0") // ":0" == OS picks
+ if err != nil {
+ return nil, nil, err
+ }
+
+ addr, ok := listener.Addr().(*net.TCPAddr)
+ if !ok {
+ return nil, nil, fmt.Errorf("listener addr is not TCPAddr")
+ }
+
+ urlListener = &url.URL{
+ Scheme: "http",
+ Host: fmt.Sprintf("localhost:%d", addr.Port),
+ Path: "/auth/callback",
+ }
+ } else {
+ urlListener, err = url.Parse(redirectURL)
+ if err != nil {
+ return nil, nil, err
+ }
+ listener, err = net.Listen("tcp", urlListener.Host)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ m := http.NewServeMux()
+ s := &http.Server{
+ Addr: urlListener.Host,
+ Handler: m,
+
+ // an arbitrary reasonable value to fix gosec lint error
+ ReadHeaderTimeout: 2 * time.Second,
+ }
+
+ m.HandleFunc(urlListener.Path, func(w http.ResponseWriter, r *http.Request) {
+ // even though these are fetched from the FormValue method,
+ // these are supplied as query parameters
+ if r.FormValue("state") != state {
+ errCh <- errors.New("invalid state token")
+ return
+ }
+ codeCh <- r.FormValue("code")
+ fmt.Fprint(w, htmlPage)
+ })
+
+ go func() {
+ if err := s.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ errCh <- err
+ }
+ }()
+
+ return s, urlListener, nil
+}
+
+func getCode(codeCh chan string, errCh chan error) (string, error) {
+ select {
+ case code := <-codeCh:
+ return code, nil
+ case err := <-errCh:
+ return "", err
+ case <-time.After(120 * time.Second):
+ return "", errors.New("timeout")
+ }
+}
+
+// newKSUID returns a globally unique, base62 (URL-safe) encoded, 27 character string.
+func newKSUID() string {
+ return ksuid.New().String()
+}
+
+type interactiveIDTokenSource struct {
+ cfg oauth2.Config
+ oidp *coreoidc.Provider
+ extraAuthCodeOpts []oauth2.AuthCodeOption
+ browser browserOpener
+}
+
+var errWontOpenBrowser = errors.New("not opening that browser")
+
+func failBrowser(string) error {
+ return errWontOpenBrowser
+}
+
+func (idts *interactiveIDTokenSource) IDToken(ctx context.Context) (*IDToken, error) {
+ cfg := idts.cfg
+ p := idts.oidp
+
+ // generate random fields and save them for comparison after OAuth2 dance
+ stateToken := newKSUID()
+ nonce := newKSUID()
+
+ codeCh := make(chan string)
+ errCh := make(chan error)
+ // starts listener using the redirect_uri, otherwise starts on ephemeral port
+ redirectServer, redirectURL, err := startRedirectListener(stateToken, oauth.InteractiveSuccessHTML, cfg.RedirectURL, codeCh, errCh)
+ if err != nil {
+ close(codeCh)
+ close(errCh)
+ return nil, fmt.Errorf("starting redirect listener: %w", err)
+ }
+ defer func() {
+ go func() {
+ _ = redirectServer.Shutdown(context.Background())
+ close(codeCh)
+ close(errCh)
+ }()
+ }()
+
+ cfg.RedirectURL = redirectURL.String()
+
+ // require that OIDC provider support PKCE to provide sufficient security for the CLI
+ pkce, err := NewPKCE(p)
+ if err != nil {
+ return nil, err
+ }
+
+ opts := append(pkce.AuthURLOpts(), oauth2.AccessTypeOnline, coreoidc.Nonce(nonce))
+ if len(idts.extraAuthCodeOpts) > 0 {
+ opts = append(opts, idts.extraAuthCodeOpts...)
+ }
+ authCodeURL := cfg.AuthCodeURL(stateToken, opts...)
+ var code string
+ if err := idts.browser(authCodeURL); err != nil {
+ // Swap to the out of band flow if we can't open the browser
+ if !errors.Is(err, errWontOpenBrowser) {
+ fmt.Fprintf(os.Stderr, "error opening browser: %v\n", err)
+ }
+ code = doOobFlow(&cfg, stateToken, opts)
+ } else {
+ fmt.Fprintf(os.Stderr, "Your browser will now be opened to:\n%s\n", authCodeURL)
+ code, err = getCode(codeCh, errCh)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error getting code from local server: %v\n", err)
+ code = doOobFlow(&cfg, stateToken, opts)
+ }
+ }
+ token, err := cfg.Exchange(ctx, code, append(pkce.TokenURLOpts(), coreoidc.Nonce(nonce))...)
+ if err != nil {
+ return nil, err
+ }
+
+ verifier := p.Verifier(&coreoidc.Config{ClientID: cfg.ClientID})
+ return extractAndVerifyIDToken(ctx, token, verifier, nonce)
+}
+
+// InteractiveIDTokenSource returns an `IDTokenSource` which performs an interactive Oauth token flow in order to retrieve an `IDToken`.
+func InteractiveIDTokenSource(cfg oauth2.Config, oidp *coreoidc.Provider, extraAuthCodeOpts []oauth2.AuthCodeOption, allowBrowser bool) IDTokenSource {
+ ts := &interactiveIDTokenSource{cfg: cfg, oidp: oidp, extraAuthCodeOpts: extraAuthCodeOpts, browser: failBrowser}
+ if allowBrowser {
+ ts.browser = browser.OpenURL
+ }
+ return ts
+}
diff --git a/pkg/oauth/oidc/interactive_e2e_test.go b/pkg/oauth/oidc/interactive_e2e_test.go
new file mode 100644
index 0000000..9f8b585
--- /dev/null
+++ b/pkg/oauth/oidc/interactive_e2e_test.go
@@ -0,0 +1,110 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build e2e
+// +build e2e
+
+package oidc
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+
+ coreoidc "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/go-rod/rod"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "golang.org/x/oauth2"
+)
+
+type claims struct {
+ Email string `json:"email"`
+ Verified bool `json:"email_verified"`
+ Subject string `json:"sub"`
+}
+
+func identityFromClaims(c claims) (string, error) {
+ if c.Email != "" {
+ if !c.Verified {
+ return "", errors.New("not verified by identity provider")
+ }
+ return c.Email, nil
+ }
+
+ if c.Subject == "" {
+ return "", errors.New("no subject found in claims")
+ }
+ return c.Subject, nil
+}
+
+// identityFromIDToken extracts the email or subject claim from an `IDToken“
+func identityFromIDToken(tok *IDToken) (string, error) {
+ claims := claims{}
+ oidcTok := tok.IDToken
+ if err := oidcTok.Claims(&claims); err != nil {
+ return "", err
+ }
+ return identityFromClaims(claims)
+}
+
+type InteractiveOIDCSuite struct {
+ suite.Suite
+}
+
+func (suite *InteractiveOIDCSuite) TestInteractiveIDTokenSource() {
+ ctx := context.Background()
+
+ urlCh := make(chan string)
+ defer close(urlCh)
+
+ browserOpener := func(input string) error {
+ urlCh <- input
+ return nil
+ }
+
+ provider, err := coreoidc.NewProvider(ctx, os.Getenv("OIDC_ISSUER"))
+ require.Nil(suite.T(), err)
+ cfg := oauth2.Config{
+ ClientID: os.Getenv("OIDC_ID"),
+ ClientSecret: "",
+ Endpoint: provider.Endpoint(),
+ Scopes: []string{coreoidc.ScopeOpenID, "email"},
+ }
+
+ ts := &interactiveIDTokenSource{
+ cfg: cfg,
+ oidp: provider,
+ browser: browserOpener,
+ }
+
+ go func() {
+ authCodeURL := <-urlCh
+ page := rod.New().MustConnect().MustPage(authCodeURL)
+ page.MustElement("body > div.dex-container > div > div > div:nth-child(2) > a > button").MustClick()
+ }()
+
+ idToken, err := ts.IDToken(ctx)
+ require.Nil(suite.T(), err)
+
+ email, err := identityFromIDToken(idToken)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), email)
+ require.Equal(suite.T(), "kilgore@kilgore.trout", email)
+}
+
+func TestInteractiveOIDCFlow(t *testing.T) {
+ suite.Run(t, new(InteractiveOIDCSuite))
+}
diff --git a/pkg/oauth/oidc/pkce.go b/pkg/oauth/oidc/pkce.go
new file mode 100644
index 0000000..8054f71
--- /dev/null
+++ b/pkg/oauth/oidc/pkce.go
@@ -0,0 +1,107 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oidc
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "regexp"
+
+ coreoidc "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // PKCES256 is the SHA256 option required by the PKCE RFC
+ PKCES256 = "S256"
+)
+
+// PKCE specifies the challenge and value pair required to fulfill RFC7636
+type PKCE struct {
+ Challenge string
+ Method string
+ Value string
+}
+
+// NewPKCE creates a new PKCE challenge for the specified provider per its supported methods (obtained through OIDC discovery endpoint)
+func NewPKCE(provider *coreoidc.Provider) (*PKCE, error) {
+ var providerClaims struct {
+ CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
+ }
+
+ if err := provider.Claims(&providerClaims); err != nil {
+ // will only error out if the JSON was malformed, which shouldn't happen at this point
+ return nil, err
+ }
+
+ var chosenMethod string
+ for _, method := range providerClaims.CodeChallengeMethodsSupported {
+ // per RFC7636, any server that supports PKCE must support S256
+ if method == PKCES256 && chosenMethod != PKCES256 {
+ chosenMethod = PKCES256
+ break
+ } else if method != "plain" {
+ fmt.Printf("Unsupported code challenge method in list: '%v'", method)
+ }
+ }
+ if chosenMethod == "" {
+ if providerIsAzureBacked(provider) {
+ chosenMethod = PKCES256
+ } else {
+ return nil, fmt.Errorf("PKCE is not supported by OIDC provider '%v'", provider.Endpoint().AuthURL)
+ }
+ }
+
+ // we use two 27 character strings to meet requirements of RFC 7636:
+ // (minimum length of 43 characters and a maximum length of 128 characters)
+ value := newKSUID() + newKSUID()
+
+ h := sha256.Sum256([]byte(value))
+ challenge := base64.RawURLEncoding.EncodeToString(h[:])
+
+ return &PKCE{
+ Challenge: challenge,
+ Method: chosenMethod,
+ Value: value,
+ }, nil
+}
+
+// AuthURLOpts returns the set of request parameters required during the initial exchange of the OAuth2 flow
+func (p *PKCE) AuthURLOpts() []oauth2.AuthCodeOption {
+ return []oauth2.AuthCodeOption{
+ oauth2.SetAuthURLParam("code_challenge_method", p.Method),
+ oauth2.SetAuthURLParam("code_challenge", p.Challenge),
+ }
+}
+
+// TokenURLOpts returns the set of request parameters required during the token request exchange flow
+func (p *PKCE) TokenURLOpts() []oauth2.AuthCodeOption {
+ return []oauth2.AuthCodeOption{
+ oauth2.SetAuthURLParam("code_verifier", p.Value),
+ }
+}
+
+var azureregex = regexp.MustCompile(`^https:\/\/login\.microsoftonline\.(com|us)\/`)
+
+// providerIsAzureBacked returns a boolean indicating whether the provider is Azure-backed;
+// Azure supports PKCE but does not advertise it in their OIDC discovery endpoint
+func providerIsAzureBacked(p *coreoidc.Provider) bool {
+ // Per https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints
+ // if endpoint starts with any of these strings then we should attempt PKCE anyway as their OIDC discovery doc
+ // does not advertise supporting PKCE but they actually do
+
+ return p != nil && azureregex.MatchString(p.Endpoint().AuthURL)
+}
diff --git a/pkg/oauth/oidc/pkce_test.go b/pkg/oauth/oidc/pkce_test.go
new file mode 100644
index 0000000..9f91609
--- /dev/null
+++ b/pkg/oauth/oidc/pkce_test.go
@@ -0,0 +1,60 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oidc
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ coreoidc "github.com/coreos/go-oidc/v3/oidc"
+)
+
+func TestProviderIsAzureBacked(t *testing.T) {
+ actualAzureProviders := []string{
+ "https://login.microsoftonline.com/6babcaad-604b-40ac-a9d7-9fd97c0b779f/v2.0",
+ }
+
+ notAzureProviders := []string{
+ "https://accounts.google.com",
+ "https://login.salesforce.com",
+ }
+ for _, tc := range actualAzureProviders {
+ t.Run(fmt.Sprintf("testing azure provider %v", tc), func(t *testing.T) {
+ p, err := coreoidc.NewProvider(context.Background(), tc)
+ if err != nil {
+ t.Error(err)
+ }
+ if !providerIsAzureBacked(p) {
+ t.Errorf("valid azure provider URL %v was not detected as being azure backed", tc)
+ }
+ })
+ }
+ for _, tc := range notAzureProviders {
+ t.Run(fmt.Sprintf("testing invalid azure provider %v", tc), func(t *testing.T) {
+ p, err := coreoidc.NewProvider(context.Background(), tc)
+ if err != nil {
+ t.Error(err)
+ }
+ if providerIsAzureBacked(p) {
+ t.Errorf("invalid azure provider URL %v was detected as being azure backed", tc)
+ }
+ })
+ }
+
+ if providerIsAzureBacked(nil) != false {
+ t.Errorf("nil provider should not return true for being Azure-backed")
+ }
+}
diff --git a/pkg/oauth/oidc/token.go b/pkg/oauth/oidc/token.go
new file mode 100644
index 0000000..b250bfd
--- /dev/null
+++ b/pkg/oauth/oidc/token.go
@@ -0,0 +1,71 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oidc
+
+import (
+ "context"
+ "errors"
+
+ coreoidc "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+// IDToken is a structured representation of an OIDC IDToken.
+type IDToken struct {
+ coreoidc.IDToken
+}
+
+// IDTokenSource provides `IDTokens`.
+type IDTokenSource interface {
+ // IDToken returns an ID token or an error.
+ IDToken(context.Context) (*IDToken, error)
+}
+
+type staticIDTokenSource struct {
+ idt *IDToken
+}
+
+func (s staticIDTokenSource) IDToken(context.Context) (*IDToken, error) {
+ return s.idt, nil
+}
+
+// StaticIDTokenSource returns an `IDTokenSource` which always returns the given `IDToken`.
+func StaticIDTokenSource(idt *IDToken) IDTokenSource {
+ return staticIDTokenSource{idt: idt}
+}
+
+// extractAndVerifyIDToken extracts the ID token from the given `oauth2.Token`, then verifies it against the given verifier and nonce.
+func extractAndVerifyIDToken(ctx context.Context, t *oauth2.Token, v *coreoidc.IDTokenVerifier, nonce string) (*IDToken, error) {
+ // requesting 'openid' scope should ensure an id_token is given when exchanging the code for an access token
+ unverifiedIDToken, ok := t.Extra("id_token").(string)
+ if !ok {
+ return nil, errors.New("id_token not present")
+ }
+
+ // verify nonce, client ID, access token hash before using it
+ idToken, err := v.Verify(ctx, unverifiedIDToken)
+ if err != nil {
+ return nil, err
+ }
+ if idToken.Nonce != nonce {
+ return nil, errors.New("nonce does not match value sent")
+ }
+ if idToken.AccessTokenHash != "" {
+ if err := idToken.VerifyAccessToken(t.AccessToken); err != nil {
+ return nil, err
+ }
+ }
+ return &IDToken{*idToken}, nil
+}
diff --git a/pkg/oauthflow/device.go b/pkg/oauthflow/device.go
new file mode 100644
index 0000000..de56f8e
--- /dev/null
+++ b/pkg/oauthflow/device.go
@@ -0,0 +1,244 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oauthflow implements OAuth/OIDC support for device and token flows
+package oauthflow
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // SigstoreDeviceURL specifies the Device Code endpoint for the public good Sigstore service
+ /* #nosec */
+ // Deprecated: this constant (while correct) should not be used
+ SigstoreDeviceURL = "https://oauth2.sigstore.dev/auth/device/code"
+ // SigstoreTokenURL specifies the Token endpoint for the public good Sigstore service
+ /* #nosec */
+ // Deprecated: this constant (while correct) should not be used
+ SigstoreTokenURL = "https://oauth2.sigstore.dev/auth/device/token"
+)
+
+type deviceResp struct {
+ DeviceCode string `json:"device_code"`
+ UserCode string `json:"user_code"`
+ VerificationURI string `json:"verification_uri"`
+ VerificationURIComplete string `json:"verification_uri_complete"`
+ Interval int `json:"interval"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+type tokenResp struct {
+ IDToken string `json:"id_token"`
+ Error string `json:"error"`
+}
+
+// DeviceFlowTokenGetter fetches an OIDC Identity token using the Device Code Grant flow as specified in RFC8628
+type DeviceFlowTokenGetter struct {
+ MessagePrinter func(string)
+ Sleeper func(time.Duration)
+ Issuer string
+ codeURL string
+}
+
+// NewDeviceFlowTokenGetter creates a new DeviceFlowTokenGetter that retrieves an OIDC Identity Token using a Device Code Grant
+// Deprecated: NewDeviceFlowTokenGetter is deprecated; use NewDeviceFlowTokenGetterForIssuer() instead
+func NewDeviceFlowTokenGetter(issuer, codeURL, _ string) *DeviceFlowTokenGetter {
+ return &DeviceFlowTokenGetter{
+ MessagePrinter: func(s string) { fmt.Println(s) },
+ Sleeper: time.Sleep,
+ Issuer: issuer,
+ codeURL: codeURL,
+ }
+}
+
+// NewDeviceFlowTokenGetterForIssuer creates a new DeviceFlowTokenGetter that retrieves an OIDC Identity Token using a Device Code Grant
+func NewDeviceFlowTokenGetterForIssuer(issuer string) *DeviceFlowTokenGetter {
+ return &DeviceFlowTokenGetter{
+ MessagePrinter: func(s string) { fmt.Println(s) },
+ Sleeper: time.Sleep,
+ Issuer: issuer,
+ }
+}
+
+func (d *DeviceFlowTokenGetter) deviceFlow(p *oidc.Provider, clientID, redirectURL string) (string, error) {
+ // require that OIDC provider support PKCE to provide sufficient security for the CLI
+ pkce, err := NewPKCE(p)
+ if err != nil {
+ return "", err
+ }
+
+ data := url.Values{
+ "client_id": []string{clientID},
+ "scope": []string{"openid email"},
+ "code_challenge_method": []string{pkce.Method},
+ "code_challenge": []string{pkce.Challenge},
+ }
+ if redirectURL != "" {
+ // If a redirect uri is provided then use it
+ data["redirect_uri"] = []string{redirectURL}
+ }
+
+ codeURL, err := d.CodeURL()
+ if err != nil {
+ return "", err
+ }
+ /* #nosec */
+ resp, err := http.PostForm(codeURL, data)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("%s: %s", resp.Status, b)
+ }
+
+ parsed := deviceResp{}
+ if err := json.Unmarshal(b, &parsed); err != nil {
+ return "", err
+ }
+ uri := parsed.VerificationURIComplete
+ if uri == "" {
+ uri = parsed.VerificationURI
+ }
+ d.MessagePrinter(fmt.Sprintf("Enter the verification code %s in your browser at: %s", parsed.UserCode, uri))
+ d.MessagePrinter(fmt.Sprintf("Code will be valid for %d seconds", parsed.ExpiresIn))
+
+ for {
+ // Some providers use a secret here, we don't need for sigstore oauth one so leave it off.
+ data := url.Values{
+ "grant_type": []string{"urn:ietf:params:oauth:grant-type:device_code"},
+ "device_code": []string{parsed.DeviceCode},
+ "scope": []string{"openid", "email"},
+ "code_verifier": []string{pkce.Value},
+ }
+
+ /* #nosec */
+ resp, err := http.PostForm(p.Endpoint().TokenURL, data)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ tr := tokenResp{}
+ if err := json.Unmarshal(b, &tr); err != nil {
+ return "", err
+ }
+
+ if tr.IDToken != "" {
+ d.MessagePrinter("Token received!")
+ return tr.IDToken, nil
+ }
+ switch tr.Error {
+ case "access_denied", "expired_token":
+ return "", fmt.Errorf("error obtaining token: %s", tr.Error)
+ case "authorization_pending":
+ d.Sleeper(time.Duration(parsed.Interval) * time.Second)
+ case "slow_down":
+ // Add ten seconds if we got told to slow down
+ d.Sleeper(time.Duration(parsed.Interval)*time.Second + 10*time.Second)
+ default:
+ return "", fmt.Errorf("unexpected error in device flow: %s", tr.Error)
+ }
+ }
+}
+
+// GetIDToken gets an OIDC ID Token from the specified provider using the device code grant flow
+func (d *DeviceFlowTokenGetter) GetIDToken(p *oidc.Provider, cfg oauth2.Config) (*OIDCIDToken, error) {
+ idToken, err := d.deviceFlow(p, cfg.ClientID, cfg.RedirectURL)
+ if err != nil {
+ return nil, err
+ }
+ verifier := p.Verifier(&oidc.Config{ClientID: cfg.ClientID})
+ parsedIDToken, err := verifier.Verify(context.Background(), idToken)
+ if err != nil {
+ return nil, err
+ }
+
+ subj, err := SubjectFromToken(parsedIDToken)
+ if err != nil {
+ return nil, err
+ }
+
+ return &OIDCIDToken{
+ RawString: idToken,
+ Subject: subj,
+ }, nil
+}
+
+// CodeURL fetches the device authorization endpoint URL from the provider's well-known configuration endpoint
+func (d *DeviceFlowTokenGetter) CodeURL() (string, error) {
+ if d.codeURL != "" {
+ return d.codeURL, nil
+ }
+
+ wellKnown := strings.TrimSuffix(d.Issuer, "/") + "/.well-known/openid-configuration"
+ /* #nosec */
+ httpClient := &http.Client{
+ Timeout: 3 * time.Second,
+ }
+ resp, err := httpClient.Get(wellKnown)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("unable to read response body: %w", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("%s: %s", resp.Status, body)
+ }
+
+ providerConfig := struct {
+ Issuer string `json:"issuer"`
+ DeviceEndpoint string `json:"device_authorization_endpoint"`
+ }{}
+ if err = json.Unmarshal(body, &providerConfig); err != nil {
+ return "", fmt.Errorf("oidc: failed to decode provider discovery object: %w", err)
+ }
+
+ if d.Issuer != providerConfig.Issuer {
+ return "", fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", d.Issuer, providerConfig.Issuer)
+ }
+
+ if providerConfig.DeviceEndpoint == "" {
+ return "", fmt.Errorf("oidc: device authorization endpoint not returned by provider")
+ }
+
+ d.codeURL = providerConfig.DeviceEndpoint
+ return d.codeURL, nil
+}
diff --git a/pkg/oauthflow/device_test.go b/pkg/oauthflow/device_test.go
new file mode 100644
index 0000000..a2edd70
--- /dev/null
+++ b/pkg/oauthflow/device_test.go
@@ -0,0 +1,172 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+)
+
+const wellKnownOIDCConfig string = `
+{
+ "issuer": "ISSUER",
+ "authorization_endpoint": "ISSUER/auth",
+ "token_endpoint": "ISSUER/token",
+ "jwks_uri": "ISSUER/keys",
+ "userinfo_endpoint": "ISSUER/userinfo",
+ "device_authorization_endpoint": "ISSUER/device/code",
+ "grant_types_supported": [
+ "authorization_code",
+ "refresh_token",
+ "urn:ietf:params:oauth:grant-type:device_code"
+ ],
+ "response_types_supported": [
+ "code"
+ ],
+ "subject_types_supported": [
+ "public"
+ ],
+ "id_token_signing_alg_values_supported": [
+ "RS256"
+ ],
+ "code_challenge_methods_supported": [
+ "S256",
+ "plain"
+ ],
+ "scopes_supported": [
+ "openid",
+ "email",
+ "groups",
+ "profile",
+ "offline_access"
+ ],
+ "token_endpoint_auth_methods_supported": [
+ "client_secret_basic",
+ "client_secret_post"
+ ],
+ "claims_supported": [
+ "iss",
+ "sub",
+ "aud",
+ "iat",
+ "exp",
+ "email",
+ "email_verified",
+ "locale",
+ "name",
+ "preferred_username",
+ "at_hash"
+ ]
+ }`
+
+type testDriver struct {
+ msgs []string
+ respCh chan interface{}
+ t *testing.T
+}
+
+func (td *testDriver) writeMsg(s string) {
+ td.msgs = append(td.msgs, s)
+}
+
+func (td *testDriver) handler(w http.ResponseWriter, r *http.Request) {
+ td.t.Log("got request:", r.URL.Path)
+ if r.URL.Path == "/.well-known/openid-configuration" {
+ _, _ = w.Write([]byte(strings.ReplaceAll(wellKnownOIDCConfig, "ISSUER", fmt.Sprintf("http://%s", r.Host))))
+ return
+ }
+ nextReply := <-td.respCh
+ b, err := json.Marshal(nextReply)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ switch r.URL.Path {
+ case "/token", "/device/code":
+ _, _ = w.Write(b)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+}
+
+func TestDeviceFlowTokenGetter_deviceFlow(t *testing.T) {
+ td := testDriver{
+ respCh: make(chan interface{}, 3),
+ t: t,
+ }
+
+ ts := httptest.NewServer(http.HandlerFunc(td.handler))
+ defer ts.Close()
+
+ dtg := DeviceFlowTokenGetter{
+ MessagePrinter: td.writeMsg,
+ Sleeper: func(_ time.Duration) {},
+ Issuer: ts.URL,
+ }
+ p, pErr := oidc.NewProvider(context.Background(), ts.URL)
+ if pErr != nil {
+ t.Fatal(pErr)
+ }
+
+ tokenCh, errCh := make(chan string), make(chan error)
+ go func() {
+ token, err := dtg.deviceFlow(p, "sigstore", "")
+ tokenCh <- token
+ errCh <- err
+ }()
+
+ td.respCh <- codeResponse()
+ td.respCh <- tokenResponse("mytoken", "")
+
+ token := <-tokenCh
+ err := <-errCh
+ if err != nil {
+ t.Fatal(err)
+ }
+ if token != "mytoken" {
+ t.Fatal("expected mytoken")
+ }
+
+ if !strings.Contains(td.msgs[0], "complete-uri") {
+ t.Fatal("expected URL to be logged, got:", td.msgs[0])
+ }
+}
+
+func codeResponse() deviceResp {
+ return deviceResp{
+ UserCode: "mysecret",
+ DeviceCode: "foobar",
+ Interval: 3,
+ VerificationURIComplete: "complete-uri",
+ }
+}
+
+func tokenResponse(idToken, err string) tokenResp {
+ return tokenResp{
+ IDToken: idToken,
+ Error: err,
+ }
+}
diff --git a/pkg/oauthflow/doc.go b/pkg/oauthflow/doc.go
new file mode 100644
index 0000000..860ed23
--- /dev/null
+++ b/pkg/oauthflow/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package oauthflow contains utilities to obtain OAuth2/OIDC tokens.
+package oauthflow
diff --git a/pkg/oauthflow/e2e_test.go b/pkg/oauthflow/e2e_test.go
new file mode 100644
index 0000000..3539b60
--- /dev/null
+++ b/pkg/oauthflow/e2e_test.go
@@ -0,0 +1,67 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build e2e
+// +build e2e
+
+package oauthflow
+
+import (
+ "os"
+ "testing"
+
+ "github.com/go-rod/rod"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+)
+
+type OAuthSuite struct {
+ suite.Suite
+}
+
+func (suite *OAuthSuite) TestOauthFlow() {
+ urlCh := make(chan string)
+
+ oldOpener := browserOpener
+ browserOpener = func(input string) error {
+ urlCh <- input
+ return nil
+ }
+ defer func() { browserOpener = oldOpener }()
+
+ go func() {
+ authCodeURL := <-urlCh
+ page := rod.New().MustConnect().MustPage(authCodeURL)
+ page.MustElement("body > div.dex-container > div > div > div:nth-child(2) > a > button").MustClick()
+ }()
+
+ idToken, err := OIDConnect(
+ os.Getenv("OIDC_ISSUER"),
+ os.Getenv("OIDC_ID"),
+ "",
+ "",
+ DefaultIDTokenGetter,
+ )
+
+ require.Nil(suite.T(), err)
+
+ email := idToken.Subject
+ require.NotNil(suite.T(), email)
+ require.Equal(suite.T(), "kilgore@kilgore.trout", email)
+}
+
+func TestOAuthFlow(t *testing.T) {
+ suite.Run(t, new(OAuthSuite))
+}
diff --git a/pkg/oauthflow/flow.go b/pkg/oauthflow/flow.go
new file mode 100644
index 0000000..38df970
--- /dev/null
+++ b/pkg/oauthflow/flow.go
@@ -0,0 +1,164 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/go-jose/go-jose/v3"
+ soauth "github.com/sigstore/sigstore/pkg/oauth"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // PublicInstanceGithubAuthSubURL Default connector ids used by `oauth2.sigstore.dev` for Github
+ PublicInstanceGithubAuthSubURL = "https://github.com/login/oauth"
+ // PublicInstanceGoogleAuthSubURL Default connector ids used by `oauth2.sigstore.dev` for Google
+ PublicInstanceGoogleAuthSubURL = "https://accounts.google.com"
+ // PublicInstanceMicrosoftAuthSubURL Default connector ids used by `oauth2.sigstore.dev` for Microsoft
+ PublicInstanceMicrosoftAuthSubURL = "https://login.microsoftonline.com"
+)
+
+// TokenGetter provides a way to get an OIDC ID Token from an OIDC IdP
+type TokenGetter interface {
+ GetIDToken(provider *oidc.Provider, config oauth2.Config) (*OIDCIDToken, error)
+}
+
+// OIDCIDToken represents an OIDC Identity Token
+type OIDCIDToken struct {
+ RawString string // RawString provides the raw token (a base64-encoded JWT) value
+ Subject string // Subject is the extracted subject from the raw token
+}
+
+// ConnectorIDOpt requests the value of prov as a the connector_id (either on URL or in form body) on the initial request;
+// this is used by Dex
+func ConnectorIDOpt(prov string) oauth2.AuthCodeOption {
+ return oauth2.SetAuthURLParam("connector_id", prov)
+}
+
+// DefaultIDTokenGetter is the default implementation.
+// The HTML page and message printed to the terminal can be customized.
+var DefaultIDTokenGetter = &InteractiveIDTokenGetter{
+ HTMLPage: soauth.InteractiveSuccessHTML,
+}
+
+// PublicInstanceGithubIDTokenGetter is a `oauth2.sigstore.dev` flow selecting github as an Idp
+// Flow is based on `DefaultIDTokenGetter` fields
+var PublicInstanceGithubIDTokenGetter = &InteractiveIDTokenGetter{
+ HTMLPage: DefaultIDTokenGetter.HTMLPage,
+ ExtraAuthURLParams: []oauth2.AuthCodeOption{ConnectorIDOpt(PublicInstanceGithubAuthSubURL)},
+}
+
+// PublicInstanceGoogleIDTokenGetter is a `oauth2.sigstore.dev` flow selecting github as an Idp
+// Flow is based on `DefaultIDTokenGetter` fields
+var PublicInstanceGoogleIDTokenGetter = &InteractiveIDTokenGetter{
+ HTMLPage: DefaultIDTokenGetter.HTMLPage,
+ ExtraAuthURLParams: []oauth2.AuthCodeOption{ConnectorIDOpt(PublicInstanceGoogleAuthSubURL)},
+}
+
+// PublicInstanceMicrosoftIDTokenGetter is a `oauth2.sigstore.dev` flow selecting microsoft as an Idp
+// Flow is based on `DefaultIDTokenGetter` fields
+var PublicInstanceMicrosoftIDTokenGetter = &InteractiveIDTokenGetter{
+ HTMLPage: DefaultIDTokenGetter.HTMLPage,
+ ExtraAuthURLParams: []oauth2.AuthCodeOption{ConnectorIDOpt(PublicInstanceMicrosoftAuthSubURL)},
+}
+
+// OIDConnect requests an OIDC Identity Token from the specified issuer using the specified client credentials and TokenGetter
+// NOTE: If the redirectURL is empty a listener on localhost:0 is configured with '/auth/callback' as default path.
+func OIDConnect(issuer, id, secret, redirectURL string, tg TokenGetter) (*OIDCIDToken, error) {
+ // Check if it's a StaticTokenGetter since NewProvider below will make
+ // network calls unnecessarily and they are ignored.
+ if sg, ok := tg.(*StaticTokenGetter); ok {
+ return sg.GetIDToken(nil, oauth2.Config{})
+ }
+ provider, err := oidc.NewProvider(context.Background(), issuer)
+ if err != nil {
+ return nil, err
+ }
+ config := oauth2.Config{
+ ClientID: id,
+ ClientSecret: secret,
+ Endpoint: provider.Endpoint(),
+ Scopes: []string{oidc.ScopeOpenID, "email"},
+ RedirectURL: redirectURL,
+ }
+
+ return tg.GetIDToken(provider, config)
+}
+
+type claims struct {
+ Email string `json:"email"`
+ Verified bool `json:"email_verified"`
+ Subject string `json:"sub"`
+}
+
+// SubjectFromToken extracts the subject claim from an OIDC Identity Token
+func SubjectFromToken(tok *oidc.IDToken) (string, error) {
+ claims := claims{}
+ if err := tok.Claims(&claims); err != nil {
+ return "", err
+ }
+ return subjectFromClaims(claims)
+}
+
+func subjectFromClaims(c claims) (string, error) {
+ if c.Email != "" {
+ if !c.Verified {
+ return "", errors.New("not verified by identity provider")
+ }
+ return c.Email, nil
+ }
+
+ if c.Subject == "" {
+ return "", errors.New("no subject found in claims")
+ }
+ return c.Subject, nil
+}
+
+// StaticTokenGetter is a token getter that works on a JWT that is already known
+type StaticTokenGetter struct {
+ RawToken string
+}
+
+// GetIDToken extracts an OIDCIDToken from the raw token *without verification*
+func (stg *StaticTokenGetter) GetIDToken(_ *oidc.Provider, _ oauth2.Config) (*OIDCIDToken, error) {
+ unsafeTok, err := jose.ParseSigned(stg.RawToken)
+ if err != nil {
+ return nil, err
+ }
+ // THIS LOGIC IS GENERALLY UNSAFE BUT OK HERE
+ // We are only parsing the id-token passed directly to a command line tool by a user, so it is trusted locally.
+ // We need to extract the email address to attach an additional signed proof to the server.
+ // THE SERVER WILL DO REAL VERIFICATION HERE
+ unsafePayload := unsafeTok.UnsafePayloadWithoutVerification()
+ claims := claims{}
+ if err := json.Unmarshal(unsafePayload, &claims); err != nil {
+ return nil, err
+ }
+
+ subj, err := subjectFromClaims(claims)
+ if err != nil {
+ return nil, err
+ }
+
+ return &OIDCIDToken{
+ RawString: stg.RawToken,
+ Subject: subj,
+ }, nil
+}
diff --git a/pkg/oauthflow/flow_test.go b/pkg/oauthflow/flow_test.go
new file mode 100644
index 0000000..25bbb7a
--- /dev/null
+++ b/pkg/oauthflow/flow_test.go
@@ -0,0 +1,187 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+ "testing"
+
+ "github.com/go-jose/go-jose/v3"
+ "golang.org/x/oauth2"
+)
+
+func TestGetCodeWorking(t *testing.T) {
+ desiredState := "foo"
+ desiredCode := "code"
+ // We need to start this in the background and send our request to the server
+
+ var gotCode string
+ var gotErr error
+ doneCh := make(chan string)
+ errCh := make(chan error)
+ getCodeFinished := make(chan error)
+ _, url, _ := startRedirectListener(desiredState, "", "", doneCh, errCh)
+ go func() {
+ gotCode, gotErr = getCode(doneCh, errCh)
+ getCodeFinished <- gotErr
+ }()
+
+ sendCodeAndState(t, url, desiredCode, desiredState)
+
+ // block until we're sure getCode has returned
+ if getCodeErr := <-getCodeFinished; getCodeErr != nil {
+ t.Fatal(gotErr)
+ }
+ if gotCode != desiredCode {
+ t.Errorf("got %s, expected %s", gotCode, desiredCode)
+ }
+}
+
+func TestGetCodeWrongState(t *testing.T) {
+ desiredState := "foo"
+ desiredCode := "code"
+ // We need to start this in the background and send our request to the server
+
+ var gotErr error
+ doneCh := make(chan string)
+ errCh := make(chan error)
+ getCodeFinished := make(chan error)
+ _, u, _ := startRedirectListener(desiredState, "", "", doneCh, errCh)
+ go func() {
+ _, gotErr = getCode(doneCh, errCh)
+ getCodeFinished <- gotErr
+ }()
+
+ sendCodeAndState(t, u, desiredCode, "WRONG")
+
+ // block until we're sure getCode has returned
+ if getCodeErr := <-getCodeFinished; getCodeErr == nil {
+ t.Fatal("expected error, sent wrong state!")
+ }
+}
+
+func sendCodeAndState(t *testing.T, redirectURL *url.URL, code, state string) {
+ t.Helper()
+ testURL, _ := url.Parse(fmt.Sprintf("%v?code=%v&state=%v", redirectURL.String(), code, state))
+ if _, err := http.Get(testURL.String()); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestStaticTokenGetter_GetIDToken(t *testing.T) {
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ k := jose.SigningKey{
+ Algorithm: jose.ES256,
+ Key: priv,
+ }
+ signer, err := jose.NewSigner(k, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tests := []struct {
+ name string
+ payload interface{}
+ want *OIDCIDToken
+ wantErr bool
+ }{
+ {
+ name: "no email",
+ payload: struct {
+ A string
+ }{
+ A: "foo",
+ },
+ wantErr: true,
+ },
+ {
+ name: "no verified",
+ payload: struct {
+ Email string
+ }{
+ Email: "foobar",
+ },
+ wantErr: true,
+ },
+ {
+ name: "not verified",
+ payload: claims{
+ Email: "foobar",
+ Verified: false,
+ },
+ wantErr: true,
+ },
+ {
+ name: "verified",
+ payload: claims{
+ Email: "foobar",
+ Verified: true,
+ },
+ want: &OIDCIDToken{
+ Subject: "foobar",
+ },
+ },
+ {
+ name: "spiffeid",
+ payload: claims{
+ Subject: "spiffe://foobar",
+ },
+ want: &OIDCIDToken{
+ Subject: "spiffe://foobar",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ raw, err := json.Marshal(tt.payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ jws, err := signer.Sign(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ token, err := jws.CompactSerialize()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tt.want != nil {
+ tt.want.RawString = token
+ }
+ stg := &StaticTokenGetter{
+ RawToken: token,
+ }
+ got, err := stg.GetIDToken(nil, oauth2.Config{})
+ if (err != nil) != tt.wantErr {
+ t.Errorf("StaticTokenGetter.GetIDToken() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("StaticTokenGetter.GetIDToken() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/oauthflow/interactive.go b/pkg/oauthflow/interactive.go
new file mode 100644
index 0000000..dfc1f0c
--- /dev/null
+++ b/pkg/oauthflow/interactive.go
@@ -0,0 +1,241 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/segmentio/ksuid"
+ "github.com/skratchdot/open-golang/open"
+ "golang.org/x/oauth2"
+)
+
+const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
+
+var browserOpener = open.Run
+
+// InteractiveIDTokenGetter is a type to get ID tokens for oauth flows
+type InteractiveIDTokenGetter struct {
+ HTMLPage string
+ ExtraAuthURLParams []oauth2.AuthCodeOption
+ Input io.Reader
+ Output io.Writer
+}
+
+// GetIDToken gets an OIDC ID Token from the specified provider using an interactive browser session
+func (i *InteractiveIDTokenGetter) GetIDToken(p *oidc.Provider, cfg oauth2.Config) (*OIDCIDToken, error) {
+ // generate random fields and save them for comparison after OAuth2 dance
+ stateToken := randStr()
+ nonce := randStr()
+
+ doneCh := make(chan string)
+ errCh := make(chan error)
+ // starts listener using the redirect_uri, otherwise starts on ephemeral port
+ redirectServer, redirectURL, err := startRedirectListener(stateToken, i.HTMLPage, cfg.RedirectURL, doneCh, errCh)
+ if err != nil {
+ return nil, fmt.Errorf("starting redirect listener: %w", err)
+ }
+ defer func() {
+ go func() {
+ _ = redirectServer.Shutdown(context.Background())
+ }()
+ }()
+
+ cfg.RedirectURL = redirectURL.String()
+
+ // require that OIDC provider support PKCE to provide sufficient security for the CLI
+ pkce, err := NewPKCE(p)
+ if err != nil {
+ return nil, err
+ }
+
+ opts := append(pkce.AuthURLOpts(), oauth2.AccessTypeOnline, oidc.Nonce(nonce))
+ if len(i.ExtraAuthURLParams) > 0 {
+ opts = append(opts, i.ExtraAuthURLParams...)
+ }
+ authCodeURL := cfg.AuthCodeURL(stateToken, opts...)
+ var code string
+ if err := browserOpener(authCodeURL); err != nil {
+ // Swap to the out of band flow if we can't open the browser
+ fmt.Fprintf(i.GetOutput(), "error opening browser: %v\n", err)
+ code = i.doOobFlow(&cfg, stateToken, opts)
+ } else {
+ fmt.Fprintf(i.GetOutput(), "Your browser will now be opened to:\n%s\n", authCodeURL)
+ code, err = getCode(doneCh, errCh)
+ if err != nil {
+ fmt.Fprintf(i.GetOutput(), "error getting code from local server: %v\n", err)
+ code = i.doOobFlow(&cfg, stateToken, opts)
+ }
+ }
+ token, err := cfg.Exchange(context.Background(), code, append(pkce.TokenURLOpts(), oidc.Nonce(nonce))...)
+ if err != nil {
+ return nil, err
+ }
+
+ // requesting 'openid' scope should ensure an id_token is given when exchanging the code for an access token
+ idToken, ok := token.Extra("id_token").(string)
+ if !ok {
+ return nil, errors.New("id_token not present")
+ }
+
+ // verify nonce, client ID, access token hash before using it
+ verifier := p.Verifier(&oidc.Config{ClientID: cfg.ClientID})
+ parsedIDToken, err := verifier.Verify(context.Background(), idToken)
+ if err != nil {
+ return nil, err
+ }
+ if parsedIDToken.Nonce != nonce {
+ return nil, errors.New("nonce does not match value sent")
+ }
+ if parsedIDToken.AccessTokenHash != "" {
+ if err := parsedIDToken.VerifyAccessToken(token.AccessToken); err != nil {
+ return nil, err
+ }
+ }
+
+ email, err := SubjectFromToken(parsedIDToken)
+ if err != nil {
+ return nil, err
+ }
+
+ returnToken := OIDCIDToken{
+ RawString: idToken,
+ Subject: email,
+ }
+ return &returnToken, nil
+}
+
+func (i *InteractiveIDTokenGetter) doOobFlow(cfg *oauth2.Config, stateToken string, opts []oauth2.AuthCodeOption) string {
+ cfg.RedirectURL = oobRedirectURI
+
+ authURL := cfg.AuthCodeURL(stateToken, opts...)
+ fmt.Fprintln(i.GetOutput(), "Go to the following link in a browser:\n\n\t", authURL)
+ fmt.Fprintf(i.GetOutput(), "Enter verification code: ")
+ var code string
+ fmt.Fscanf(i.GetInput(), "%s", &code)
+ // New line in case read input doesn't move cursor to next line.
+ fmt.Fprintln(i.GetOutput())
+ return code
+}
+
+// GetInput returns the input reader for the token getter. If one is not set,
+// it defaults to stdin.
+func (i *InteractiveIDTokenGetter) GetInput() io.Reader {
+ if i.Input == nil {
+ return os.Stdin
+ }
+ return i.Input
+}
+
+// GetOutput returns the output writer for the token getter. If one is not set,
+// it defaults to stderr.
+func (i *InteractiveIDTokenGetter) GetOutput() io.Writer {
+ if i.Output == nil {
+ return os.Stderr
+ }
+ return i.Output
+}
+
+func startRedirectListener(state, htmlPage, redirectURL string, doneCh chan string, errCh chan error) (*http.Server, *url.URL, error) {
+ var listener net.Listener
+ var urlListener *url.URL
+ var err error
+
+ if redirectURL == "" {
+ listener, err = net.Listen("tcp", "localhost:0")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ addr, ok := listener.Addr().(*net.TCPAddr)
+ if !ok {
+ return nil, nil, fmt.Errorf("listener addr is not TCPAddr")
+ }
+
+ urlListener = &url.URL{
+ Scheme: "http",
+ Host: fmt.Sprintf("localhost:%d", addr.Port),
+ Path: "/auth/callback",
+ }
+ } else {
+ urlListener, err = url.Parse(redirectURL)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ listener, err = net.Listen("tcp", urlListener.Host)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ m := http.NewServeMux()
+ s := &http.Server{
+ Addr: urlListener.Host,
+ Handler: m,
+
+ // an arbitrary reasonable value to fix gosec lint error
+ ReadHeaderTimeout: 2 * time.Second,
+ }
+
+ m.HandleFunc(urlListener.Path, func(w http.ResponseWriter, r *http.Request) {
+ // even though these are fetched from the FormValue method,
+ // these are supplied as query parameters
+ if r.FormValue("state") != state {
+ errCh <- errors.New("invalid state token")
+ return
+ }
+ doneCh <- r.FormValue("code")
+ fmt.Fprint(w, htmlPage)
+ })
+
+ go func() {
+ if err := s.Serve(listener); err != nil && err != http.ErrServerClosed {
+ errCh <- err
+ }
+ }()
+
+ return s, urlListener, nil
+}
+
+func getCode(doneCh chan string, errCh chan error) (string, error) {
+ timeoutCh := time.NewTimer(120 * time.Second)
+ select {
+ case code := <-doneCh:
+ return code, nil
+ case err := <-errCh:
+ return "", err
+ case <-timeoutCh.C:
+ return "", errors.New("timeout")
+ }
+}
+
+func randStr() string {
+ // we use ksuid here to ensure we get globally unique values to mitigate
+ // risk of replay attacks
+
+ // output is a 27 character base62 string which is by default URL-safe
+ return ksuid.New().String()
+}
diff --git a/pkg/oauthflow/interactive_test.go b/pkg/oauthflow/interactive_test.go
new file mode 100644
index 0000000..78591bc
--- /dev/null
+++ b/pkg/oauthflow/interactive_test.go
@@ -0,0 +1,47 @@
+// Copyright 2022 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "bytes"
+ "os"
+ "testing"
+)
+
+func TestInteractiveFlow_IO(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ f := &InteractiveIDTokenGetter{}
+ if f.GetInput() != os.Stdin {
+ t.Error("expected stdin")
+ }
+ if f.GetOutput() != os.Stderr {
+ t.Error("expected stderr")
+ }
+ })
+
+ t.Run("buffer", func(t *testing.T) {
+ b := new(bytes.Buffer)
+ f := &InteractiveIDTokenGetter{
+ Input: b,
+ Output: b,
+ }
+ if f.GetInput() != b {
+ t.Error("expected buffer")
+ }
+ if f.GetOutput() != b {
+ t.Error("expected buffer")
+ }
+ })
+}
diff --git a/pkg/oauthflow/pkce.go b/pkg/oauthflow/pkce.go
new file mode 100644
index 0000000..be8e23b
--- /dev/null
+++ b/pkg/oauthflow/pkce.go
@@ -0,0 +1,109 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "regexp"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // PKCES256 is the SHA256 option required by the PKCE RFC
+ PKCES256 = "S256"
+)
+
+// PKCE specifies the challenge and value pair required to fulfill RFC7636
+type PKCE struct {
+ Challenge string
+ Method string
+ Value string
+}
+
+// NewPKCE creates a new PKCE challenge for the specified provider per its supported methods (obtained through OIDC discovery endpoint)
+func NewPKCE(provider *oidc.Provider) (*PKCE, error) {
+ var providerClaims struct {
+ CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
+ }
+
+ if err := provider.Claims(&providerClaims); err != nil {
+ // will only error out if the JSON was malformed, which shouldn't happen at this point
+ return nil, err
+ }
+
+ var chosenMethod string
+ for _, method := range providerClaims.CodeChallengeMethodsSupported {
+ // per RFC7636, any server that supports PKCE must support S256
+ if method == PKCES256 && chosenMethod != PKCES256 {
+ chosenMethod = PKCES256
+ break
+ } else if method != "plain" {
+ fmt.Printf("Unsupported code challenge method in list: '%v'", method)
+ }
+ }
+ if chosenMethod == "" {
+ if providerIsAzureBacked(provider) {
+ chosenMethod = PKCES256
+ } else {
+ return nil, fmt.Errorf("PKCE is not supported by OIDC provider '%v'", provider.Endpoint().AuthURL)
+ }
+ }
+
+ // we use two 27 character strings to meet requirements of RFC 7636:
+ // (minimum length of 43 characters and a maximum length of 128 characters)
+ value := randStr() + randStr()
+
+ h := sha256.New()
+ _, _ = h.Write([]byte(value))
+ challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
+
+ return &PKCE{
+ Challenge: challenge,
+ Method: chosenMethod,
+ Value: value,
+ }, nil
+}
+
+// AuthURLOpts returns the set of request parameters required during the initial exchange of the OAuth2 flow
+func (p *PKCE) AuthURLOpts() []oauth2.AuthCodeOption {
+ return []oauth2.AuthCodeOption{
+ oauth2.SetAuthURLParam("code_challenge_method", p.Method),
+ oauth2.SetAuthURLParam("code_challenge", p.Challenge),
+ }
+}
+
+// TokenURLOpts returns the set of request parameters required during the token request exchange flow
+func (p *PKCE) TokenURLOpts() []oauth2.AuthCodeOption {
+ return []oauth2.AuthCodeOption{
+ oauth2.SetAuthURLParam("code_verifier", p.Value),
+ }
+}
+
+var azureregex = regexp.MustCompile(`^https:\/\/login\.microsoftonline\.(com|us)\/`)
+
+// providerIsAzureBacked returns a boolean indicating whether the provider is Azure-backed;
+// Azure supports PKCE but does not advertise it in their OIDC discovery endpoint
+func providerIsAzureBacked(p *oidc.Provider) bool {
+ // Per https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints
+ // if endpoint starts with any of these strings then we should attempt PKCE anyway as their OIDC discovery doc
+ // does not advertise supporting PKCE but they actually do
+
+ return p != nil && azureregex.MatchString(p.Endpoint().AuthURL)
+}
diff --git a/pkg/oauthflow/pkce_test.go b/pkg/oauthflow/pkce_test.go
new file mode 100644
index 0000000..4872b78
--- /dev/null
+++ b/pkg/oauthflow/pkce_test.go
@@ -0,0 +1,61 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oauthflow
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+)
+
+func TestProviderIsAzureBacked(t *testing.T) {
+ actualAzureProviders := []string{
+ "https://login.microsoftonline.com/6babcaad-604b-40ac-a9d7-9fd97c0b779f/v2.0",
+ }
+
+ notAzureProviders := []string{
+ "https://accounts.google.com",
+ "https://login.salesforce.com",
+ }
+ for _, tc := range actualAzureProviders {
+ t.Run(fmt.Sprintf("testing azure provider %v", tc), func(t *testing.T) {
+ p, err := oidc.NewProvider(context.Background(), tc)
+ if err != nil {
+ t.Error(err)
+ }
+ if !providerIsAzureBacked(p) {
+ t.Errorf("valid azure provider URL %v was not detected as being azure backed", tc)
+ }
+ })
+ }
+ for _, tc := range notAzureProviders {
+ t.Run(fmt.Sprintf("testing invalid azure provider %v", tc), func(t *testing.T) {
+ p, err := oidc.NewProvider(context.Background(), tc)
+ if err != nil {
+ t.Error(err)
+ }
+ if providerIsAzureBacked(p) {
+ t.Errorf("invalid azure provider URL %v was detected as being azure backed", tc)
+ }
+ })
+ }
+
+ if providerIsAzureBacked(nil) != false {
+ t.Errorf("nil provider should not return true for being Azure-backed")
+ }
+}
diff --git a/pkg/signature/doc.go b/pkg/signature/doc.go
new file mode 100644
index 0000000..dbd3314
--- /dev/null
+++ b/pkg/signature/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package signature contains types and utilities related to Sigstore signatures.
+package signature
diff --git a/pkg/signature/dsse/adapters.go b/pkg/signature/dsse/adapters.go
new file mode 100644
index 0000000..5f80109
--- /dev/null
+++ b/pkg/signature/dsse/adapters.go
@@ -0,0 +1,77 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package dsse includes wrappers to support DSSE
+package dsse
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "errors"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// SignerAdapter wraps a `sigstore/signature.Signer`, making it compatible with `go-securesystemslib/dsse.Signer`.
+type SignerAdapter struct {
+ SignatureSigner signature.Signer
+ Pub crypto.PublicKey
+ Opts []signature.SignOption
+ PubKeyID string
+}
+
+// Sign implements `go-securesystemslib/dsse.Signer`
+func (a *SignerAdapter) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return a.SignatureSigner.SignMessage(bytes.NewReader(data), append(a.Opts, options.WithContext(ctx))...)
+}
+
+// Verify disabled `go-securesystemslib/dsse.Verifier`
+func (a *SignerAdapter) Verify(_ context.Context, _, _ []byte) error {
+ return errors.New("Verify disabled")
+}
+
+// Public implements `go-securesystemslib/dsse.Verifier`
+func (a *SignerAdapter) Public() crypto.PublicKey {
+ return a.Pub
+}
+
+// KeyID implements `go-securesystemslib/dsse.Verifier`
+func (a SignerAdapter) KeyID() (string, error) {
+ return a.PubKeyID, nil
+}
+
+// VerifierAdapter wraps a `sigstore/signature.Verifier`, making it compatible with `go-securesystemslib/dsse.Verifier`.
+type VerifierAdapter struct {
+ SignatureVerifier signature.Verifier
+ Pub crypto.PublicKey
+ PubKeyID string
+}
+
+// Verify implements `go-securesystemslib/dsse.Verifier`
+func (a *VerifierAdapter) Verify(ctx context.Context, data, sig []byte) error {
+ return a.SignatureVerifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithContext(ctx))
+}
+
+// Public implements `go-securesystemslib/dsse.Verifier`
+func (a *VerifierAdapter) Public() crypto.PublicKey {
+ return a.Pub
+}
+
+// KeyID implements `go-securesystemslib/dsse.Verifier`
+func (a *VerifierAdapter) KeyID() (string, error) {
+ return a.PubKeyID, nil
+}
diff --git a/pkg/signature/dsse/adapters_test.go b/pkg/signature/dsse/adapters_test.go
new file mode 100644
index 0000000..6398b61
--- /dev/null
+++ b/pkg/signature/dsse/adapters_test.go
@@ -0,0 +1,32 @@
+//
+// Copyright 2023 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dsse
+
+import (
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+)
+
+// TestImplementDSSESigner doesn't functionally test anything, but will fail to compile if the interface is not implemented
+func TestImplementsDSSESigner(_ *testing.T) {
+ var _ dsse.Signer = &SignerAdapter{}
+}
+
+// TestImplementDSSEVerifier doesn't functionally test anything, but will fail to compile if the interface is not implemented
+func TestImplementsDSSEVerifier(_ *testing.T) {
+ var _ dsse.Verifier = &VerifierAdapter{}
+}
diff --git a/pkg/signature/dsse/doc.go b/pkg/signature/dsse/doc.go
new file mode 100644
index 0000000..f58f180
--- /dev/null
+++ b/pkg/signature/dsse/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package dsse contains handlers for Dead Simple Signing Envelopes
+package dsse
diff --git a/pkg/signature/dsse/dsse.go b/pkg/signature/dsse/dsse.go
new file mode 100644
index 0000000..628e241
--- /dev/null
+++ b/pkg/signature/dsse/dsse.go
@@ -0,0 +1,152 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dsse
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "encoding/base64"
+ "encoding/json"
+ "io"
+
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/sigstore/sigstore/pkg/signature"
+)
+
+// WrapSigner returns a signature.Signer that uses the DSSE encoding format
+func WrapSigner(s signature.Signer, payloadType string) signature.Signer {
+ return &wrappedSigner{
+ s: s,
+ payloadType: payloadType,
+ }
+}
+
+type wrappedSigner struct {
+ s signature.Signer
+ payloadType string
+}
+
+// PublicKey returns the public key associated with the signer
+func (w *wrappedSigner) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return w.s.PublicKey(opts...)
+}
+
+// SignMessage signs the provided stream in the reader using the DSSE encoding format
+func (w *wrappedSigner) SignMessage(r io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ p, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ pae := dsse.PAE(w.payloadType, p)
+ sig, err := w.s.SignMessage(bytes.NewReader(pae), opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ env := dsse.Envelope{
+ PayloadType: w.payloadType,
+ Payload: base64.StdEncoding.EncodeToString(p),
+ Signatures: []dsse.Signature{
+ {
+ Sig: base64.StdEncoding.EncodeToString(sig),
+ },
+ },
+ }
+ return json.Marshal(env)
+}
+
+// WrapVerifier returns a signature.Verifier that uses the DSSE encoding format
+func WrapVerifier(v signature.Verifier) signature.Verifier {
+ return &wrappedVerifier{
+ v: v,
+ }
+}
+
+type wrappedVerifier struct {
+ v signature.Verifier
+}
+
+// PublicKey returns the public key associated with the verifier
+func (w *wrappedVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return w.v.PublicKey(opts...)
+}
+
+// VerifySignature verifies the signature specified in an DSSE envelope
+func (w *wrappedVerifier) VerifySignature(s, _ io.Reader, _ ...signature.VerifyOption) error {
+ sig, err := io.ReadAll(s)
+ if err != nil {
+ return err
+ }
+
+ env := dsse.Envelope{}
+ if err := json.Unmarshal(sig, &env); err != nil {
+ return err
+ }
+
+ pub, err := w.PublicKey()
+ if err != nil {
+ return err
+ }
+ verifier, err := dsse.NewEnvelopeVerifier(&VerifierAdapter{
+ SignatureVerifier: w.v,
+
+ Pub: pub,
+ PubKeyID: "", // We do not want to limit verification to a specific key.
+ })
+ if err != nil {
+ return err
+ }
+
+ _, err = verifier.Verify(context.Background(), &env)
+ return err
+}
+
+// WrapSignerVerifier returns a signature.SignerVerifier that uses the DSSE encoding format
+func WrapSignerVerifier(sv signature.SignerVerifier, payloadType string) signature.SignerVerifier {
+ signer := &wrappedSigner{
+ payloadType: payloadType,
+ s: sv,
+ }
+ verifier := &wrappedVerifier{
+ v: sv,
+ }
+
+ return &wrappedSignerVerifier{
+ signer: signer,
+ verifier: verifier,
+ }
+}
+
+type wrappedSignerVerifier struct {
+ signer *wrappedSigner
+ verifier *wrappedVerifier
+}
+
+// PublicKey returns the public key associated with the verifier
+func (w *wrappedSignerVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return w.signer.PublicKey(opts...)
+}
+
+// VerifySignature verifies the signature specified in an DSSE envelope
+func (w *wrappedSignerVerifier) VerifySignature(s, r io.Reader, opts ...signature.VerifyOption) error {
+ return w.verifier.VerifySignature(s, r, opts...)
+}
+
+// SignMessage signs the provided stream in the reader using the DSSE encoding format
+func (w *wrappedSignerVerifier) SignMessage(r io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ return w.signer.SignMessage(r, opts...)
+}
diff --git a/pkg/signature/dsse/dsse_test.go b/pkg/signature/dsse/dsse_test.go
new file mode 100644
index 0000000..b89edc1
--- /dev/null
+++ b/pkg/signature/dsse/dsse_test.go
@@ -0,0 +1,184 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dsse
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/sigstore/sigstore/pkg/signature"
+)
+
+func TestRoundTrip(t *testing.T) {
+ p, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := signature.LoadECDSASignerVerifier(p, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data := "sometestdata"
+ payloadType := "foo"
+
+ wsv := WrapSignerVerifier(sv, payloadType)
+
+ sig, err := wsv.SignMessage(strings.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := wsv.VerifySignature(bytes.NewReader(sig), nil); err != nil {
+ t.Fatal(err)
+ }
+
+ env := dsse.Envelope{}
+ if err := json.Unmarshal(sig, &env); err != nil {
+ t.Fatal(err)
+ }
+ if env.PayloadType != payloadType {
+ t.Errorf("Expected payloadType %s, got %s", payloadType, env.PayloadType)
+ }
+
+ got, err := base64.StdEncoding.DecodeString(env.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(got) != data {
+ t.Errorf("Expected payload %s, got %s", data, env.Payload)
+ }
+}
+
+func TestMultiRoundTrip(t *testing.T) {
+ p, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := signature.LoadECDSASignerVerifier(p, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv2, err := signature.LoadECDSASignerVerifier(p2, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data := "sometestdata"
+ payloadType := "foo"
+
+ wsv := WrapMultiSignerVerifier(payloadType, 2, sv, sv2)
+
+ sig, err := wsv.SignMessage(strings.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := wsv.VerifySignature(bytes.NewReader(sig), nil); err != nil {
+ t.Fatal(err)
+ }
+
+ env := dsse.Envelope{}
+ if err := json.Unmarshal(sig, &env); err != nil {
+ t.Fatal(err)
+ }
+ if env.PayloadType != payloadType {
+ t.Errorf("Expected payloadType %s, got %s", payloadType, env.PayloadType)
+ }
+
+ got, err := base64.StdEncoding.DecodeString(env.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(got) != data {
+ t.Errorf("Expected payload %s, got %s", data, env.Payload)
+ }
+}
+
+func TestInvalidThresholdMultiRoundTrip(t *testing.T) {
+ p, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := signature.LoadECDSASignerVerifier(p, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv2, err := signature.LoadECDSASignerVerifier(p2, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data := "sometestdata"
+ payloadType := "foo"
+
+ ws := WrapMultiSigner(payloadType, sv, sv2)
+
+ sig, err := ws.SignMessage(strings.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ wv := WrapMultiVerifier(payloadType, 2, sv)
+
+ if err := wv.VerifySignature(bytes.NewReader(sig), nil); err == nil {
+ t.Fatalf("Did not fail verification on bogus signature")
+ }
+}
+
+func TestInvalidSignature(t *testing.T) {
+ p, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v, err := signature.LoadECDSAVerifier(&p.PublicKey, crypto.SHA256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ wv := WrapVerifier(v)
+ sig := []byte("not valid JSON")
+
+ if err := wv.VerifySignature(bytes.NewReader(sig), nil); err == nil {
+ t.Fatalf("Did not fail verification on bogus signature")
+ }
+}
diff --git a/pkg/signature/dsse/multidsse.go b/pkg/signature/dsse/multidsse.go
new file mode 100644
index 0000000..34ebd5a
--- /dev/null
+++ b/pkg/signature/dsse/multidsse.go
@@ -0,0 +1,186 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dsse
+
+import (
+ "context"
+ "crypto"
+ "encoding/json"
+ "errors"
+ "io"
+
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/sigstore/sigstore/pkg/signature"
+)
+
+type wrappedMultiSigner struct {
+ sLAdapters []dsse.Signer
+ payloadType string
+}
+
+// WrapMultiSigner returns a signature.Signer that uses the DSSE encoding format
+func WrapMultiSigner(payloadType string, sL ...signature.Signer) signature.Signer {
+ signerAdapterL := make([]dsse.Signer, 0, len(sL))
+ for _, s := range sL {
+ pub, err := s.PublicKey()
+ if err != nil {
+ return nil
+ }
+
+ keyID, err := dsse.SHA256KeyID(pub)
+ if err != nil {
+ keyID = ""
+ }
+
+ signerAdapter := &SignerAdapter{
+ SignatureSigner: s,
+ Pub: s.PublicKey,
+ PubKeyID: keyID, // We do not want to limit verification to a specific key.
+ }
+
+ signerAdapterL = append(signerAdapterL, signerAdapter)
+ }
+
+ return &wrappedMultiSigner{
+ sLAdapters: signerAdapterL,
+ payloadType: payloadType,
+ }
+}
+
+// PublicKey returns the public key associated with the signer
+func (wL *wrappedMultiSigner) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return nil, errors.New("not supported for multi signatures")
+}
+
+// SignMessage signs the provided stream in the reader using the DSSE encoding format
+func (wL *wrappedMultiSigner) SignMessage(r io.Reader, _ ...signature.SignOption) ([]byte, error) {
+ p, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ envSigner, err := dsse.NewEnvelopeSigner(wL.sLAdapters...)
+ if err != nil {
+ return nil, err
+ }
+
+ env, err := envSigner.SignPayload(context.Background(), wL.payloadType, p)
+ if err != nil {
+ return nil, err
+ }
+
+ return json.Marshal(env)
+}
+
+type wrappedMultiVerifier struct {
+ vLAdapters []dsse.Verifier
+ threshold int
+ payloadType string
+}
+
+// WrapMultiVerifier returns a signature.Verifier that uses the DSSE encoding format
+func WrapMultiVerifier(payloadType string, threshold int, vL ...signature.Verifier) signature.Verifier {
+ verifierAdapterL := make([]dsse.Verifier, 0, len(vL))
+ for _, v := range vL {
+ pub, err := v.PublicKey()
+ if err != nil {
+ return nil
+ }
+
+ keyID, err := dsse.SHA256KeyID(pub)
+ if err != nil {
+ keyID = ""
+ }
+
+ verifierAdapter := &VerifierAdapter{
+ SignatureVerifier: v,
+ Pub: v.PublicKey,
+ PubKeyID: keyID, // We do not want to limit verification to a specific key.
+ }
+
+ verifierAdapterL = append(verifierAdapterL, verifierAdapter)
+ }
+
+ return &wrappedMultiVerifier{
+ vLAdapters: verifierAdapterL,
+ payloadType: payloadType,
+ threshold: threshold,
+ }
+}
+
+// PublicKey returns the public key associated with the signer
+func (wL *wrappedMultiVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return nil, errors.New("not supported for multi signatures")
+}
+
+// VerifySignature verifies the signature specified in an DSSE envelope
+func (wL *wrappedMultiVerifier) VerifySignature(s, _ io.Reader, _ ...signature.VerifyOption) error {
+ sig, err := io.ReadAll(s)
+ if err != nil {
+ return err
+ }
+
+ env := dsse.Envelope{}
+ if err := json.Unmarshal(sig, &env); err != nil {
+ return err
+ }
+
+ envVerifier, err := dsse.NewMultiEnvelopeVerifier(wL.threshold, wL.vLAdapters...)
+ if err != nil {
+ return err
+ }
+
+ _, err = envVerifier.Verify(context.Background(), &env)
+ return err
+}
+
+// WrapMultiSignerVerifier returns a signature.SignerVerifier that uses the DSSE encoding format
+func WrapMultiSignerVerifier(payloadType string, threshold int, svL ...signature.SignerVerifier) signature.SignerVerifier {
+ signerL := make([]signature.Signer, 0, len(svL))
+ verifierL := make([]signature.Verifier, 0, len(svL))
+ for _, sv := range svL {
+ signerL = append(signerL, sv)
+ verifierL = append(verifierL, sv)
+ }
+
+ sL := WrapMultiSigner(payloadType, signerL...)
+ vL := WrapMultiVerifier(payloadType, threshold, verifierL...)
+
+ return &wrappedMultiSignerVerifier{
+ signer: sL,
+ verifier: vL,
+ }
+}
+
+type wrappedMultiSignerVerifier struct {
+ signer signature.Signer
+ verifier signature.Verifier
+}
+
+// PublicKey returns the public key associated with the verifier
+func (w *wrappedMultiSignerVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return w.signer.PublicKey(opts...)
+}
+
+// VerifySignature verifies the signature specified in an DSSE envelope
+func (w *wrappedMultiSignerVerifier) VerifySignature(s, r io.Reader, opts ...signature.VerifyOption) error {
+ return w.verifier.VerifySignature(s, r, opts...)
+}
+
+// SignMessage signs the provided stream in the reader using the DSSE encoding format
+func (w *wrappedMultiSignerVerifier) SignMessage(r io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ return w.signer.SignMessage(r, opts...)
+}
diff --git a/pkg/signature/ecdsa.go b/pkg/signature/ecdsa.go
new file mode 100644
index 0000000..57199db
--- /dev/null
+++ b/pkg/signature/ecdsa.go
@@ -0,0 +1,253 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// checked on LoadSigner, LoadVerifier and SignMessage
+var ecdsaSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA512,
+ crypto.SHA384,
+ crypto.SHA224,
+}
+
+// checked on VerifySignature. Supports SHA1 verification.
+var ecdsaSupportedVerifyHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA512,
+ crypto.SHA384,
+ crypto.SHA224,
+ crypto.SHA1,
+}
+
+// ECDSASigner is a signature.Signer that uses an Elliptic Curve DSA algorithm
+type ECDSASigner struct {
+ hashFunc crypto.Hash
+ priv *ecdsa.PrivateKey
+}
+
+// LoadECDSASigner calculates signatures using the specified private key and hash algorithm.
+//
+// hf must not be crypto.Hash(0).
+func LoadECDSASigner(priv *ecdsa.PrivateKey, hf crypto.Hash) (*ECDSASigner, error) {
+ if priv == nil {
+ return nil, errors.New("invalid ECDSA private key specified")
+ }
+
+ if !isSupportedAlg(hf, ecdsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &ECDSASigner{
+ priv: priv,
+ hashFunc: hf,
+ }, nil
+}
+
+// SignMessage signs the provided message. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the ECDSASigner was created.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithRand()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (e ECDSASigner) SignMessage(message io.Reader, opts ...SignOption) ([]byte, error) {
+ digest, _, err := ComputeDigestForSigning(message, e.hashFunc, ecdsaSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ rand := selectRandFromOpts(opts...)
+
+ return ecdsa.SignASN1(rand, e.priv, digest)
+}
+
+// Public returns the public key that can be used to verify signatures created by
+// this signer.
+func (e ECDSASigner) Public() crypto.PublicKey {
+ if e.priv == nil {
+ return nil
+ }
+
+ return e.priv.Public()
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e ECDSASigner) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.Public(), nil
+}
+
+// Sign computes the signature for the specified digest. If a source of entropy is
+// given in rand, it will be used instead of the default value (rand.Reader from crypto/rand).
+//
+// If opts are specified, the hash function in opts.Hash should be the one used to compute
+// digest. If opts are not specified, the value provided when the signer was created will be used instead.
+func (e ECDSASigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ ecdsaOpts := []SignOption{options.WithDigest(digest), options.WithRand(rand)}
+ if opts != nil {
+ ecdsaOpts = append(ecdsaOpts, options.WithCryptoSignerOpts(opts))
+ }
+
+ return e.SignMessage(nil, ecdsaOpts...)
+}
+
+// ECDSAVerifier is a signature.Verifier that uses an Elliptic Curve DSA algorithm
+type ECDSAVerifier struct {
+ publicKey *ecdsa.PublicKey
+ hashFunc crypto.Hash
+}
+
+// LoadECDSAVerifier returns a Verifier that verifies signatures using the specified
+// ECDSA public key and hash algorithm.
+//
+// hf must not be crypto.Hash(0).
+func LoadECDSAVerifier(pub *ecdsa.PublicKey, hashFunc crypto.Hash) (*ECDSAVerifier, error) {
+ if pub == nil {
+ return nil, errors.New("invalid ECDSA public key specified")
+ }
+
+ if !isSupportedAlg(hashFunc, ecdsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &ECDSAVerifier{
+ publicKey: pub,
+ hashFunc: hashFunc,
+ }, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e ECDSAVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.publicKey, nil
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the ECDSAVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// All other options are ignored if specified.
+func (e ECDSAVerifier) VerifySignature(signature, message io.Reader, opts ...VerifyOption) error {
+ if e.publicKey == nil {
+ return errors.New("no public key set for ECDSAVerifier")
+ }
+
+ digest, _, err := ComputeDigestForVerifying(message, e.hashFunc, ecdsaSupportedVerifyHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+
+ if signature == nil {
+ return errors.New("nil signature passed to VerifySignature")
+ }
+
+ sigBytes, err := io.ReadAll(signature)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ // Without this check, VerifyASN1 panics on an invalid key.
+ if !e.publicKey.Curve.IsOnCurve(e.publicKey.X, e.publicKey.Y) {
+ return fmt.Errorf("invalid ECDSA public key for %s", e.publicKey.Params().Name)
+ }
+
+ if !ecdsa.VerifyASN1(e.publicKey, digest, sigBytes) {
+ return errors.New("invalid signature when validating ASN.1 encoded signature")
+ }
+
+ return nil
+}
+
+// ECDSASignerVerifier is a signature.SignerVerifier that uses an Elliptic Curve DSA algorithm
+type ECDSASignerVerifier struct {
+ *ECDSASigner
+ *ECDSAVerifier
+}
+
+// LoadECDSASignerVerifier creates a combined signer and verifier. This is a convenience object
+// that simply wraps an instance of ECDSASigner and ECDSAVerifier.
+func LoadECDSASignerVerifier(priv *ecdsa.PrivateKey, hf crypto.Hash) (*ECDSASignerVerifier, error) {
+ signer, err := LoadECDSASigner(priv, hf)
+ if err != nil {
+ return nil, fmt.Errorf("initializing signer: %w", err)
+ }
+ verifier, err := LoadECDSAVerifier(&priv.PublicKey, hf)
+ if err != nil {
+ return nil, fmt.Errorf("initializing verifier: %w", err)
+ }
+
+ return &ECDSASignerVerifier{
+ ECDSASigner: signer,
+ ECDSAVerifier: verifier,
+ }, nil
+}
+
+// NewDefaultECDSASignerVerifier creates a combined signer and verifier using ECDSA.
+//
+// This creates a new ECDSA key using the P-256 curve and uses the SHA256 hashing algorithm.
+func NewDefaultECDSASignerVerifier() (*ECDSASignerVerifier, *ecdsa.PrivateKey, error) {
+ return NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256)
+}
+
+// NewECDSASignerVerifier creates a combined signer and verifier using ECDSA.
+//
+// This creates a new ECDSA key using the specified elliptic curve, entropy source, and hashing function.
+func NewECDSASignerVerifier(curve elliptic.Curve, rand io.Reader, hashFunc crypto.Hash) (*ECDSASignerVerifier, *ecdsa.PrivateKey, error) {
+ priv, err := ecdsa.GenerateKey(curve, rand)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sv, err := LoadECDSASignerVerifier(priv, hashFunc)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return sv, priv, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e ECDSASignerVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.publicKey, nil
+}
diff --git a/pkg/signature/ecdsa_test.go b/pkg/signature/ecdsa_test.go
new file mode 100644
index 0000000..ffa521c
--- /dev/null
+++ b/pkg/signature/ecdsa_test.go
@@ -0,0 +1,136 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "math/big"
+ "strings"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+// Generated with:
+// openssl ecparam -genkey -name prime256v1 > ec_private.pem
+// openssl pkcs8 -topk8 -in ec_private.pem -nocrypt
+const ecdsaPriv = `-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmrLtCpBdXgXLUr7o
+nSUPfo3oXMjmvuwTOjpTulIBKlKhRANCAATH6KSpTFe6uXFmW1qNEFXaO7fWPfZt
+pPZrHZ1cFykidZoURKoYXfkohJ+U/USYy8Sd8b4DMd5xDRZCnlDM0h37
+-----END PRIVATE KEY-----`
+
+// Extracted from above with:
+// openssl ec -in ec_private.pem -pubout
+const ecdsaPub = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+ikqUxXurlxZltajRBV2ju31j32
+baT2ax2dXBcpInWaFESqGF35KISflP1EmMvEnfG+AzHecQ0WQp5QzNId+w==
+-----END PUBLIC KEY-----`
+
+func TestECDSASignerVerifier(t *testing.T) {
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(ecdsaPriv), cryptoutils.SkipPassword)
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling private key: %v", err)
+ }
+ sv, err := LoadECDSASignerVerifier(privateKey.(*ecdsa.PrivateKey), crypto.SHA256)
+ if err != nil {
+ t.Errorf("unexpected error creating signer/verifier: %v", err)
+ }
+
+ message := []byte("sign me")
+ // created with openssl dgst -sign privKey.pem -sha256
+ sig, _ := base64.StdEncoding.DecodeString("MEQCIGvnAsUT6P4PoJoKxP331ZFU2LfzxnuvulK14Rl3zNKIAiBJCSA7NdmAZkLNqxmWnbBp8ntJYVZmUR0Tbmv6ftS8ww==")
+ testingSigner(t, sv, "ecdsa", crypto.SHA256, message)
+ testingVerifier(t, sv, "ecdsa", crypto.SHA256, sig, message)
+
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(ecdsaPub))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key: %v", err)
+ }
+ v, err := LoadECDSAVerifier(publicKey.(*ecdsa.PublicKey), crypto.SHA256)
+ if err != nil {
+ t.Errorf("unexpected error creating verifier: %v", err)
+ }
+ testingVerifier(t, v, "ecdsa", crypto.SHA256, sig, message)
+}
+
+func TestECDSASignerVerifierUnsupportedHash(t *testing.T) {
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(ecdsaPriv), cryptoutils.SkipPassword)
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling private key: %v", err)
+ }
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(ecdsaPub))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key key: %v", err)
+ }
+
+ _, err = LoadECDSASigner(privateKey.(*ecdsa.PrivateKey), crypto.SHA1)
+ if !strings.Contains(err.Error(), "invalid hash function specified") {
+ t.Errorf("expected error 'invalid hash function specified', got: %v", err.Error())
+ }
+
+ _, err = LoadECDSAVerifier(publicKey.(*ecdsa.PublicKey), crypto.SHA1)
+ if !strings.Contains(err.Error(), "invalid hash function specified") {
+ t.Errorf("expected error 'invalid hash function specified', got: %v", err.Error())
+ }
+}
+
+func TestECDSALoadVerifierWithoutKey(t *testing.T) {
+ key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ v, err := LoadECDSAVerifier(&key.PublicKey, crypto.SHA256)
+ if err != nil {
+ t.Fatalf("error creating verifier: %v", err)
+ }
+ v.publicKey = nil
+ if err := v.VerifySignature(nil, nil); err == nil || !strings.Contains(err.Error(), "no public key") {
+ t.Fatalf("expected error without public key, got: %v", err)
+ }
+}
+
+// TestECDSALoadVerifierInvalidCurve tests gracefully handling an invalid curve.
+func TestECDSALoadVerifierInvalidCurve(t *testing.T) {
+ data := []byte{1}
+ x := ecdsa.PrivateKey{}
+ z := new(big.Int)
+ z.SetBytes(data)
+ x.X = z
+ x.Y = z
+ x.D = z
+ x.Curve = elliptic.P256()
+
+ verifier, err := LoadECDSAVerifier(&x.PublicKey, crypto.SHA256)
+ if err != nil {
+ t.Fatalf("unexpected error loading verifier: %v", err)
+ }
+
+ msg := []byte("hello")
+ digest := sha256.Sum256(msg)
+ sig, err := ecdsa.SignASN1(rand.Reader, &x, digest[:])
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ if err := verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(msg)); err == nil || !strings.Contains(err.Error(), "invalid ECDSA public key") {
+ t.Fatalf("expected error verifying signature with invalid curve, got %v", err)
+ }
+}
diff --git a/pkg/signature/ed25519.go b/pkg/signature/ed25519.go
new file mode 100644
index 0000000..23a8638
--- /dev/null
+++ b/pkg/signature/ed25519.go
@@ -0,0 +1,197 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ed25519"
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+)
+
+var ed25519SupportedHashFuncs = []crypto.Hash{
+ crypto.Hash(0),
+}
+
+// ED25519Signer is a signature.Signer that uses the Ed25519 public-key signature system
+type ED25519Signer struct {
+ priv ed25519.PrivateKey
+}
+
+// LoadED25519Signer calculates signatures using the specified private key.
+func LoadED25519Signer(priv ed25519.PrivateKey) (*ED25519Signer, error) {
+ if priv == nil {
+ return nil, errors.New("invalid ED25519 private key specified")
+ }
+
+ // check this to avoid panic and throw error gracefully
+ if len(priv) != ed25519.PrivateKeySize {
+ return nil, errors.New("invalid size for ED25519 key")
+ }
+
+ return &ED25519Signer{
+ priv: priv,
+ }, nil
+}
+
+// SignMessage signs the provided message. Passing the WithDigest option is not
+// supported as ED25519 performs a two pass hash over the message during the
+// signing process.
+//
+// All options are ignored.
+func (e ED25519Signer) SignMessage(message io.Reader, _ ...SignOption) ([]byte, error) {
+ messageBytes, _, err := ComputeDigestForSigning(message, crypto.Hash(0), ed25519SupportedHashFuncs)
+ if err != nil {
+ return nil, err
+ }
+
+ return ed25519.Sign(e.priv, messageBytes), nil
+}
+
+// Public returns the public key that can be used to verify signatures created by
+// this signer.
+func (e ED25519Signer) Public() crypto.PublicKey {
+ if e.priv == nil {
+ return nil
+ }
+
+ return e.priv.Public()
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e ED25519Signer) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.Public(), nil
+}
+
+// Sign computes the signature for the specified message; the first and third arguments to this
+// function are ignored as they are not used by the ED25519 algorithm.
+func (e ED25519Signer) Sign(_ io.Reader, message []byte, _ crypto.SignerOpts) ([]byte, error) {
+ if message == nil {
+ return nil, errors.New("message must not be nil")
+ }
+ return e.SignMessage(bytes.NewReader(message))
+}
+
+// ED25519Verifier is a signature.Verifier that uses the Ed25519 public-key signature system
+type ED25519Verifier struct {
+ publicKey ed25519.PublicKey
+}
+
+// LoadED25519Verifier returns a Verifier that verifies signatures using the specified ED25519 public key.
+func LoadED25519Verifier(pub ed25519.PublicKey) (*ED25519Verifier, error) {
+ if pub == nil {
+ return nil, errors.New("invalid ED25519 public key specified")
+ }
+
+ return &ED25519Verifier{
+ publicKey: pub,
+ }, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e *ED25519Verifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.publicKey, nil
+}
+
+// VerifySignature verifies the signature for the given message.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// All options are ignored if specified.
+func (e *ED25519Verifier) VerifySignature(signature, message io.Reader, _ ...VerifyOption) error {
+ messageBytes, _, err := ComputeDigestForVerifying(message, crypto.Hash(0), ed25519SupportedHashFuncs)
+ if err != nil {
+ return err
+ }
+
+ if signature == nil {
+ return errors.New("nil signature passed to VerifySignature")
+ }
+
+ sigBytes, err := io.ReadAll(signature)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ if !ed25519.Verify(e.publicKey, messageBytes, sigBytes) {
+ return errors.New("failed to verify signature")
+ }
+ return nil
+}
+
+// ED25519SignerVerifier is a signature.SignerVerifier that uses the Ed25519 public-key signature system
+type ED25519SignerVerifier struct {
+ *ED25519Signer
+ *ED25519Verifier
+}
+
+// LoadED25519SignerVerifier creates a combined signer and verifier. This is
+// a convenience object that simply wraps an instance of ED25519Signer and ED25519Verifier.
+func LoadED25519SignerVerifier(priv ed25519.PrivateKey) (*ED25519SignerVerifier, error) {
+ signer, err := LoadED25519Signer(priv)
+ if err != nil {
+ return nil, fmt.Errorf("initializing signer: %w", err)
+ }
+ pub, ok := priv.Public().(ed25519.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("given key is not ed25519.PublicKey")
+ }
+ verifier, err := LoadED25519Verifier(pub)
+ if err != nil {
+ return nil, fmt.Errorf("initializing verifier: %w", err)
+ }
+
+ return &ED25519SignerVerifier{
+ ED25519Signer: signer,
+ ED25519Verifier: verifier,
+ }, nil
+}
+
+// NewDefaultED25519SignerVerifier creates a combined signer and verifier using ED25519.
+// This creates a new ED25519 key using crypto/rand as an entropy source.
+func NewDefaultED25519SignerVerifier() (*ED25519SignerVerifier, ed25519.PrivateKey, error) {
+ return NewED25519SignerVerifier(rand.Reader)
+}
+
+// NewED25519SignerVerifier creates a combined signer and verifier using ED25519.
+// This creates a new ED25519 key using the specified entropy source.
+func NewED25519SignerVerifier(rand io.Reader) (*ED25519SignerVerifier, ed25519.PrivateKey, error) {
+ _, priv, err := ed25519.GenerateKey(rand)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sv, err := LoadED25519SignerVerifier(priv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return sv, priv, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (e ED25519SignerVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return e.publicKey, nil
+}
diff --git a/pkg/signature/ed25519_test.go b/pkg/signature/ed25519_test.go
new file mode 100644
index 0000000..4665990
--- /dev/null
+++ b/pkg/signature/ed25519_test.go
@@ -0,0 +1,88 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/ed25519"
+ "encoding/base64"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+// Generated with:
+// openssl genpkey -algorithm ed25519 -outform PEM -out -
+const ed25519Priv = `-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIFP9CZb6J1DiOLfdIkPfy1bwBOCjEG6KR/cIdhw90J1H
+-----END PRIVATE KEY-----`
+
+// Extracted from above with:
+// openssl ec -in ec_private.pem -pubout
+const ed25519Pub = `-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEA9wy4umF4RHQ8UQXo8fzEQNBWE4GsBMkCzQPAfHvkf/s=
+-----END PUBLIC KEY-----`
+
+func TestED25519SignerVerifier(t *testing.T) {
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(ed25519Priv), cryptoutils.SkipPassword)
+ if err != nil {
+ t.Fatalf("unexpected error unmarshalling public key: %v", err)
+ }
+ edPriv, ok := privateKey.(ed25519.PrivateKey)
+ if !ok {
+ t.Fatalf("expected ed25519.PrivateKey")
+ }
+
+ sv, err := LoadED25519SignerVerifier(edPriv)
+ if err != nil {
+ t.Fatalf("unexpected error creating signer/verifier: %v", err)
+ }
+
+ message := []byte("sign me")
+ sig, _ := base64.StdEncoding.DecodeString("cnafwd8DKq2nQ564eN66ckYV8anVFGFi5vaYiQg2aal7ej/J0/OE0PPdKHLHe9wdzWRMFy5MpurRD/2cGXGLBQ==")
+ testingSigner(t, sv, "ed25519", crypto.SHA256, message)
+ testingVerifier(t, sv, "ed25519", crypto.SHA256, sig, message)
+ pub, err := sv.PublicKey()
+ if err != nil {
+ t.Fatalf("unexpected error from PublicKey(): %v", err)
+ }
+ assertPublicKeyIsx509Marshalable(t, pub)
+}
+
+func TestED25519Verifier(t *testing.T) {
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(ed25519Pub))
+ if err != nil {
+ t.Fatalf("unexpected error unmarshalling public key: %v", err)
+ }
+ edPub, ok := publicKey.(ed25519.PublicKey)
+ if !ok {
+ t.Fatalf("public key is not ed25519")
+ }
+
+ v, err := LoadED25519Verifier(edPub)
+ if err != nil {
+ t.Fatalf("unexpected error creating verifier: %v", err)
+ }
+
+ message := []byte("sign me")
+ sig, _ := base64.StdEncoding.DecodeString("cnafwd8DKq2nQ564eN66ckYV8anVFGFi5vaYiQg2aal7ej/J0/OE0PPdKHLHe9wdzWRMFy5MpurRD/2cGXGLBQ==")
+ testingVerifier(t, v, "ed25519", crypto.SHA256, sig, message)
+ pub, err := v.PublicKey()
+ if err != nil {
+ t.Fatalf("unexpected error from PublicKey(): %v", err)
+ }
+ assertPublicKeyIsx509Marshalable(t, pub)
+}
diff --git a/pkg/signature/kms/aws/aws_test.go b/pkg/signature/kms/aws/aws_test.go
new file mode 100644
index 0000000..a3c3664
--- /dev/null
+++ b/pkg/signature/kms/aws/aws_test.go
@@ -0,0 +1,158 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aws
+
+import "testing"
+
+func TestParseReference(t *testing.T) {
+ tests := []struct {
+ in string
+ wantEndpoint string
+ wantKeyID string
+ wantAlias string
+ wantErr bool
+ }{
+ {
+ in: "awskms:///1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ // multi-region key
+ in: "awskms:///mrk-1234abcd12ab34cd56ef1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "mrk-1234abcd12ab34cd56ef1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///1234ABCD-12AB-34CD-56EF-1234567890AB",
+ wantEndpoint: "",
+ wantKeyID: "1234ABCD-12AB-34CD-56EF-1234567890AB",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms://localhost:4566/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "localhost:4566",
+ wantKeyID: "1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "localhost:4566",
+ wantKeyID: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///alias/ExampleAlias",
+ wantEndpoint: "",
+ wantKeyID: "alias/ExampleAlias",
+ wantAlias: "alias/ExampleAlias",
+ wantErr: false,
+ },
+ {
+ in: "awskms://localhost:4566/alias/ExampleAlias",
+ wantEndpoint: "localhost:4566",
+ wantKeyID: "alias/ExampleAlias",
+ wantAlias: "alias/ExampleAlias",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias",
+ wantEndpoint: "",
+ wantKeyID: "arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias",
+ wantAlias: "alias/ExampleAlias",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///arn:aws-us-gov:kms:us-gov-west-1:111122223333:alias/ExampleAlias",
+ wantEndpoint: "",
+ wantKeyID: "arn:aws-us-gov:kms:us-gov-west-1:111122223333:alias/ExampleAlias",
+ wantAlias: "alias/ExampleAlias",
+ wantErr: false,
+ },
+ {
+ in: "awskms:///arn:aws-us-gov:kms:us-gov-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "arn:aws-us-gov:kms:us-gov-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantAlias: "",
+ wantErr: false,
+ },
+ {
+ in: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias",
+ wantEndpoint: "localhost:4566",
+ wantKeyID: "arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias",
+ wantAlias: "alias/ExampleAlias",
+ wantErr: false,
+ },
+ {
+ // missing alias/ prefix
+ in: "awskms:///missingalias",
+ wantEndpoint: "",
+ wantKeyID: "",
+ wantAlias: "",
+ wantErr: true,
+ },
+ {
+ // invalid UUID
+ in: "awskms:///1234abcd-12ab-YYYY-56ef-1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "",
+ wantAlias: "",
+ wantErr: true,
+ },
+ {
+ // Currently, references without endpoints must use 3
+ // slashes. It would be nice to support this format,
+ // but that would be harder to parse.
+ in: "awskms://1234abcd-12ab-34cd-56ef-1234567890ab",
+ wantEndpoint: "",
+ wantKeyID: "",
+ wantAlias: "",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ gotEndpoint, gotKeyID, gotAlias, err := ParseReference(tt.in)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseReference() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotEndpoint != tt.wantEndpoint {
+ t.Errorf("ParseReference() gotEndpoint = %v, want %v", gotEndpoint, tt.wantEndpoint)
+ }
+ if gotKeyID != tt.wantKeyID {
+ t.Errorf("ParseReference() gotKeyID = %v, want %v", gotKeyID, tt.wantKeyID)
+ }
+ if gotAlias != tt.wantAlias {
+ t.Errorf("ParseReference() gotAlias = %v, want %v", gotAlias, tt.wantAlias)
+ }
+ })
+ }
+}
diff --git a/pkg/signature/kms/aws/client.go b/pkg/signature/kms/aws/client.go
new file mode 100644
index 0000000..3ae8ac7
--- /dev/null
+++ b/pkg/signature/kms/aws/client.go
@@ -0,0 +1,366 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package aws implement the interface with amazon aws kms service
+package aws
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "regexp"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/kms"
+ "github.com/aws/aws-sdk-go-v2/service/kms/types"
+ "github.com/jellydator/ttlcache/v3"
+ "github.com/sigstore/sigstore/pkg/signature"
+ sigkms "github.com/sigstore/sigstore/pkg/signature/kms"
+)
+
+func init() {
+ sigkms.AddProvider(ReferenceScheme, func(ctx context.Context, keyResourceID string, _ crypto.Hash, _ ...signature.RPCOption) (sigkms.SignerVerifier, error) {
+ return LoadSignerVerifier(ctx, keyResourceID)
+ })
+}
+
+const (
+ cacheKey = "signer"
+ // ReferenceScheme schemes for various KMS services are copied from https://github.com/google/go-cloud/tree/master/secrets
+ ReferenceScheme = "awskms://"
+)
+
+type awsClient struct {
+ client *kms.Client
+ endpoint string
+ keyID string
+ alias string
+ keyCache *ttlcache.Cache[string, cmk]
+}
+
+var (
+ errKMSReference = errors.New("kms specification should be in the format awskms://[ENDPOINT]/[ID/ALIAS/ARN] (endpoint optional)")
+
+ // Key ID/ALIAS/ARN conforms to KMS standard documented here: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id
+ // Key format examples:
+ // Key ID: awskms:///1234abcd-12ab-34cd-56ef-1234567890ab
+ // Key ID with endpoint: awskms://localhost:4566/1234abcd-12ab-34cd-56ef-1234567890ab
+ // Key ARN: awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
+ // Key ARN with endpoint: awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
+ // Alias name: awskms:///alias/ExampleAlias
+ // Alias name with endpoint: awskms://localhost:4566/alias/ExampleAlias
+ // Alias ARN: awskms:///arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias
+ // Alias ARN with endpoint: awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias
+ uuidRE = `m?r?k?-?[A-Fa-f0-9]{8}-?[A-Fa-f0-9]{4}-?[A-Fa-f0-9]{4}-?[A-Fa-f0-9]{4}-?[A-Fa-f0-9]{12}`
+ arnRE = `arn:(?:aws|aws-us-gov):kms:[a-z0-9-]+:\d{12}:`
+ hostRE = `([^/]*)/`
+ keyIDRE = regexp.MustCompile(`^awskms://` + hostRE + `(` + uuidRE + `)$`)
+ keyARNRE = regexp.MustCompile(`^awskms://` + hostRE + `(` + arnRE + `key/` + uuidRE + `)$`)
+ aliasNameRE = regexp.MustCompile(`^awskms://` + hostRE + `((alias/.*))$`)
+ aliasARNRE = regexp.MustCompile(`^awskms://` + hostRE + `(` + arnRE + `(alias/.*))$`)
+ allREs = []*regexp.Regexp{keyIDRE, keyARNRE, aliasNameRE, aliasARNRE}
+)
+
+// ValidReference returns a non-nil error if the reference string is invalid
+func ValidReference(ref string) error {
+ for _, re := range allREs {
+ if re.MatchString(ref) {
+ return nil
+ }
+ }
+ return errKMSReference
+}
+
+// ParseReference parses an awskms-scheme URI into its constituent parts.
+func ParseReference(resourceID string) (endpoint, keyID, alias string, err error) {
+ var v []string
+ for _, re := range allREs {
+ v = re.FindStringSubmatch(resourceID)
+ if len(v) >= 3 {
+ endpoint, keyID = v[1], v[2]
+ if len(v) == 4 {
+ alias = v[3]
+ }
+ return
+ }
+ }
+ err = fmt.Errorf("invalid awskms format %q", resourceID)
+ return
+}
+
+func newAWSClient(ctx context.Context, keyResourceID string, opts ...func(*config.LoadOptions) error) (*awsClient, error) {
+ if err := ValidReference(keyResourceID); err != nil {
+ return nil, err
+ }
+ a := &awsClient{}
+ var err error
+ a.endpoint, a.keyID, a.alias, err = ParseReference(keyResourceID)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := a.setupClient(ctx, opts...); err != nil {
+ return nil, err
+ }
+
+ a.keyCache = ttlcache.New[string, cmk](
+ ttlcache.WithDisableTouchOnHit[string, cmk](),
+ )
+
+ return a, nil
+}
+
+func (a *awsClient) setupClient(ctx context.Context, opts ...func(*config.LoadOptions) error) (err error) {
+ if a.endpoint != "" {
+ opts = append(opts, config.WithEndpointResolverWithOptions(
+ aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
+ return aws.Endpoint{
+ URL: "https://" + a.endpoint,
+ }, nil
+ }),
+ ))
+ }
+ if os.Getenv("AWS_TLS_INSECURE_SKIP_VERIFY") == "1" {
+ opts = append(opts, config.WithHTTPClient(&http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint: gosec
+ },
+ }))
+ }
+
+ cfg, err := config.LoadDefaultConfig(ctx, opts...)
+ if err != nil {
+ return fmt.Errorf("loading AWS config: %w", err)
+ }
+
+ a.client = kms.NewFromConfig(cfg)
+ return
+}
+
+type cmk struct {
+ KeyMetadata *types.KeyMetadata
+ PublicKey crypto.PublicKey
+}
+
+func (c *cmk) HashFunc() crypto.Hash {
+ switch c.KeyMetadata.SigningAlgorithms[0] {
+ case types.SigningAlgorithmSpecRsassaPssSha256, types.SigningAlgorithmSpecRsassaPkcs1V15Sha256, types.SigningAlgorithmSpecEcdsaSha256:
+ return crypto.SHA256
+ case types.SigningAlgorithmSpecRsassaPssSha384, types.SigningAlgorithmSpecRsassaPkcs1V15Sha384, types.SigningAlgorithmSpecEcdsaSha384:
+ return crypto.SHA384
+ case types.SigningAlgorithmSpecRsassaPssSha512, types.SigningAlgorithmSpecRsassaPkcs1V15Sha512, types.SigningAlgorithmSpecEcdsaSha512:
+ return crypto.SHA512
+ default:
+ return 0
+ }
+}
+
+func (c *cmk) Verifier() (signature.Verifier, error) {
+ switch c.KeyMetadata.SigningAlgorithms[0] {
+ case types.SigningAlgorithmSpecRsassaPssSha256, types.SigningAlgorithmSpecRsassaPssSha384, types.SigningAlgorithmSpecRsassaPssSha512:
+ pub, ok := c.PublicKey.(*rsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("public key is not rsa")
+ }
+ return signature.LoadRSAPSSVerifier(pub, c.HashFunc(), nil)
+ case types.SigningAlgorithmSpecRsassaPkcs1V15Sha256, types.SigningAlgorithmSpecRsassaPkcs1V15Sha384, types.SigningAlgorithmSpecRsassaPkcs1V15Sha512:
+ pub, ok := c.PublicKey.(*rsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("public key is not rsa")
+ }
+ return signature.LoadRSAPKCS1v15Verifier(pub, c.HashFunc())
+ case types.SigningAlgorithmSpecEcdsaSha256, types.SigningAlgorithmSpecEcdsaSha384, types.SigningAlgorithmSpecEcdsaSha512:
+ pub, ok := c.PublicKey.(*ecdsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("public key is not ecdsa")
+ }
+ return signature.LoadECDSAVerifier(pub, c.HashFunc())
+ default:
+ return nil, fmt.Errorf("signing algorithm unsupported")
+ }
+}
+
+func (a *awsClient) fetchCMK(ctx context.Context) (*cmk, error) {
+ var err error
+ cmk := &cmk{}
+ cmk.PublicKey, err = a.fetchPublicKey(ctx)
+ if err != nil {
+ return nil, err
+ }
+ cmk.KeyMetadata, err = a.fetchKeyMetadata(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return cmk, nil
+}
+
+func (a *awsClient) getHashFunc(ctx context.Context) (crypto.Hash, error) {
+ cmk, err := a.getCMK(ctx)
+ if err != nil {
+ return 0, err
+ }
+ return cmk.HashFunc(), nil
+}
+
+func (a *awsClient) getCMK(ctx context.Context) (*cmk, error) {
+ var lerr error
+ loader := ttlcache.LoaderFunc[string, cmk](
+ func(c *ttlcache.Cache[string, cmk], key string) *ttlcache.Item[string, cmk] {
+ var k *cmk
+ k, lerr = a.fetchCMK(ctx)
+ if lerr == nil {
+ return c.Set(cacheKey, *k, time.Second*300)
+ }
+ return nil
+ },
+ )
+
+ item := a.keyCache.Get(cacheKey, ttlcache.WithLoader[string, cmk](loader))
+ if lerr == nil {
+ cmk := item.Value()
+ return &cmk, nil
+ }
+ return nil, lerr
+}
+
+func (a *awsClient) createKey(ctx context.Context, algorithm string) (crypto.PublicKey, error) {
+ if a.alias == "" {
+ return nil, errors.New("must use alias key format")
+ }
+
+ // look for existing key first
+ cmk, err := a.getCMK(ctx)
+ if err == nil {
+ out := cmk.PublicKey
+ return out, nil
+ }
+
+ // return error if not *kms.NotFoundException
+ var errNotFound *types.NotFoundException
+ if !errors.As(err, &errNotFound) {
+ return nil, fmt.Errorf("looking up key: %w", err)
+ }
+
+ usage := types.KeyUsageTypeSignVerify
+ description := "Created by Sigstore"
+ key, err := a.client.CreateKey(ctx, &kms.CreateKeyInput{
+ CustomerMasterKeySpec: types.CustomerMasterKeySpec(algorithm),
+ KeyUsage: usage,
+ Description: &description,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("creating key: %w", err)
+ }
+
+ _, err = a.client.CreateAlias(ctx, &kms.CreateAliasInput{
+ AliasName: &a.alias,
+ TargetKeyId: key.KeyMetadata.KeyId,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("creating alias %q: %w", a.alias, err)
+ }
+
+ cmk, err = a.getCMK(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("retrieving PublicKey from cache: %w", err)
+ }
+
+ return cmk.PublicKey, err
+}
+
+func (a *awsClient) verify(ctx context.Context, sig, message io.Reader, opts ...signature.VerifyOption) error {
+ cmk, err := a.getCMK(ctx)
+ if err != nil {
+ return err
+ }
+ verifier, err := cmk.Verifier()
+ if err != nil {
+ return err
+ }
+ return verifier.VerifySignature(sig, message, opts...)
+}
+
+func (a *awsClient) verifyRemotely(ctx context.Context, sig, digest []byte) error {
+ cmk, err := a.getCMK(ctx)
+ if err != nil {
+ return err
+ }
+ alg := cmk.KeyMetadata.SigningAlgorithms[0]
+ messageType := types.MessageTypeDigest
+ if _, err := a.client.Verify(ctx, &kms.VerifyInput{
+ KeyId: &a.keyID,
+ Message: digest,
+ MessageType: messageType,
+ Signature: sig,
+ SigningAlgorithm: alg,
+ }); err != nil {
+ return fmt.Errorf("unable to verify signature: %w", err)
+ }
+ return nil
+}
+
+func (a *awsClient) sign(ctx context.Context, digest []byte, _ crypto.Hash) ([]byte, error) {
+ cmk, err := a.getCMK(ctx)
+ if err != nil {
+ return nil, err
+ }
+ alg := cmk.KeyMetadata.SigningAlgorithms[0]
+
+ messageType := types.MessageTypeDigest
+ out, err := a.client.Sign(ctx, &kms.SignInput{
+ KeyId: &a.keyID,
+ Message: digest,
+ MessageType: messageType,
+ SigningAlgorithm: alg,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("signing with kms: %w", err)
+ }
+ return out.Signature, nil
+}
+
+func (a *awsClient) fetchPublicKey(ctx context.Context) (crypto.PublicKey, error) {
+ out, err := a.client.GetPublicKey(ctx, &kms.GetPublicKeyInput{
+ KeyId: &a.keyID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("getting public key: %w", err)
+ }
+ key, err := x509.ParsePKIXPublicKey(out.PublicKey)
+ if err != nil {
+ return nil, fmt.Errorf("parsing public key: %w", err)
+ }
+ return key, nil
+}
+
+func (a *awsClient) fetchKeyMetadata(ctx context.Context) (*types.KeyMetadata, error) {
+ out, err := a.client.DescribeKey(ctx, &kms.DescribeKeyInput{
+ KeyId: &a.keyID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("getting key metadata: %w", err)
+ }
+ return out.KeyMetadata, nil
+}
diff --git a/pkg/signature/kms/aws/doc.go b/pkg/signature/kms/aws/doc.go
new file mode 100644
index 0000000..2dcd66e
--- /dev/null
+++ b/pkg/signature/kms/aws/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package aws contains utilities related to AWS KMS.
+package aws
diff --git a/pkg/signature/kms/aws/e2e_test.go b/pkg/signature/kms/aws/e2e_test.go
new file mode 100644
index 0000000..11bfc0c
--- /dev/null
+++ b/pkg/signature/kms/aws/e2e_test.go
@@ -0,0 +1,283 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build e2e
+// +build e2e
+
+package aws
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/aws/retry"
+ "github.com/aws/aws-sdk-go-v2/config"
+ awskms "github.com/aws/aws-sdk-go/service/kms"
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+type AWSSuite struct {
+ suite.Suite
+ endpoint string
+}
+
+// Address intermittent failure in issue #1110
+type Issue1110Error struct{}
+
+func (i Issue1110Error) IsErrorRetryable(err error) aws.Ternary {
+ if err != nil && err.Error() == "use of closed network connection" {
+ return aws.BoolTernary(true)
+ }
+ return aws.UnknownTernary
+}
+
+func (suite *AWSSuite) GetProvider(key string) *SignerVerifier {
+ provider, err := LoadSignerVerifier(context.Background(), fmt.Sprintf("awskms://%s/%s", suite.endpoint, key),
+ config.WithRetryer(func() aws.Retryer {
+ return retry.NewStandard(func(opts *retry.StandardOptions) {
+ opts.Retryables = append(opts.Retryables, Issue1110Error{})
+ })
+ }))
+ require.NoError(suite.T(), err)
+ require.NotNil(suite.T(), provider)
+ return provider
+}
+
+func (suite *AWSSuite) SetupSuite() {
+ suite.endpoint = os.Getenv("AWS_ENDPOINT")
+}
+
+func (suite *AWSSuite) TestGetProvider() {
+ _ = suite.GetProvider("alias/provider")
+ _ = suite.GetProvider("1234abcd-12ab-34cd-56ef-1234567890ab")
+}
+
+func (suite *AWSSuite) TestInvalidProvider() {
+ provider, err := LoadSignerVerifier(context.Background(), fmt.Sprintf("awskms://%s/nonsense", suite.endpoint))
+ require.Error(suite.T(), err)
+ require.Nil(suite.T(), provider)
+}
+
+func (suite *AWSSuite) TestCreateKey() {
+ provider := suite.GetProvider("alias/provider")
+
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ key2, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ // Subsequent call should produce same key
+ assert.Equal(suite.T(), key, key2)
+}
+
+func (suite *AWSSuite) TestCreateKeyByID() {
+ provider := suite.GetProvider("1234abcd-12ab-34cd-56ef-1234567890ab")
+
+ // CreateKey can only work with aliases, not IDs
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Error(suite.T(), err)
+ require.Nil(suite.T(), key)
+}
+
+func (suite *AWSSuite) TestSign() {
+ provider := suite.GetProvider("alias/TestSign")
+
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ verifier, _ := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *AWSSuite) TestSHA384() {
+ provider := suite.GetProvider("alias/TestSHA384")
+
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP384)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ k, err := provider.PublicKey()
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ pubKey, ok := k.(*ecdsa.PublicKey)
+ require.True(suite.T(), ok, fmt.Sprintf("expected type ecdsa, got type %T", k))
+
+ verifier, _ := signature.LoadECDSAVerifier(pubKey, crypto.SHA384)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *AWSSuite) TestPublicKey() {
+ provider := suite.GetProvider("alias/TestPubKeyVerify")
+
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.NotNil(suite.T(), sig)
+ require.Nil(suite.T(), err)
+
+ k, err := provider.PublicKey()
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ pubKey, ok := k.(*ecdsa.PublicKey)
+ require.True(suite.T(), ok, fmt.Sprintf("expected type ecdsa, got: %T", k))
+
+ verifier, _ := signature.LoadECDSAVerifier(pubKey, crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *AWSSuite) TestVerify() {
+ provider := suite.GetProvider("alias/TestVerify")
+
+ key, err := provider.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithRemoteVerification(true))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *AWSSuite) TestTwoProviders() {
+ provider1 := suite.GetProvider("alias/TestTwoProviders")
+ provider2 := suite.GetProvider("alias/TestTwoProviders")
+
+ key, err := provider1.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider1.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ err = provider2.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *AWSSuite) TestBadSignature() {
+ provider1 := suite.GetProvider("alias/TestBadSignature1")
+ provider2 := suite.GetProvider("alias/TestBadSignature2")
+
+ key1, err := provider1.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key1)
+
+ key2, err := provider2.CreateKey(context.Background(), awskms.CustomerMasterKeySpecEccNistP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key2)
+
+ data := []byte("mydata")
+ sig, err := provider1.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ err = provider2.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Contains(suite.T(), err.Error(), "invalid signature when validating ASN.1 encoded signature")
+
+ err = provider2.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithRemoteVerification(true))
+ assert.Contains(suite.T(), err.Error(), "KMSInvalidSignatureException")
+}
+
+func (suite *AWSSuite) TestKeyTypes() {
+ for _, cmkSpec := range []string{
+ awskms.CustomerMasterKeySpecRsa2048,
+ awskms.CustomerMasterKeySpecRsa3072,
+ awskms.CustomerMasterKeySpecRsa4096,
+ awskms.CustomerMasterKeySpecEccNistP256,
+ awskms.CustomerMasterKeySpecEccNistP384,
+ awskms.CustomerMasterKeySpecEccNistP521,
+ // awskms.CustomerMasterKeySpecEccSecgP256k1, // unsupported by localstack at the moment
+ } {
+ suite.T().Run(fmt.Sprintf("KeyType-%s", cmkSpec), func(t *testing.T) {
+ provider := suite.GetProvider("alias/" + cmkSpec)
+ key, err := provider.CreateKey(context.Background(), cmkSpec)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ })
+ }
+}
+
+func (suite *AWSSuite) TestCancelContext() {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ provider := suite.GetProvider("alias/TestCancelContext")
+ key, err := provider.CreateKey(ctx, awskms.CustomerMasterKeySpecEccNistP256)
+ assert.Error(suite.T(), err)
+ assert.Contains(suite.T(), err.Error(), context.Canceled.Error())
+ assert.Nil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data), options.WithContext(ctx))
+ assert.Error(suite.T(), err)
+ assert.Contains(suite.T(), err.Error(), context.Canceled.Error())
+ assert.Nil(suite.T(), sig)
+
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithContext(ctx))
+ assert.Error(suite.T(), err)
+ assert.Contains(suite.T(), err.Error(), context.Canceled.Error())
+}
+
+func TestAWS(t *testing.T) {
+ suite.Run(t, new(AWSSuite))
+}
diff --git a/pkg/signature/kms/aws/go.mod b/pkg/signature/kms/aws/go.mod
new file mode 100644
index 0000000..f0c539a
--- /dev/null
+++ b/pkg/signature/kms/aws/go.mod
@@ -0,0 +1,44 @@
+module github.com/sigstore/sigstore/pkg/signature/kms/aws
+
+replace github.com/sigstore/sigstore => ../../../../
+
+go 1.20
+
+require (
+ github.com/aws/aws-sdk-go v1.49.4
+ github.com/aws/aws-sdk-go-v2 v1.24.0
+ github.com/aws/aws-sdk-go-v2/config v1.26.1
+ github.com/aws/aws-sdk-go-v2/service/kms v1.27.6
+ github.com/jellydator/ttlcache/v3 v3.1.1
+ github.com/sigstore/sigstore v1.6.4
+ github.com/stretchr/testify v1.8.4
+)
+
+require (
+ github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect
+ github.com/aws/smithy-go v1.19.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/google/go-containerregistry v0.17.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/kr/pretty v0.2.1 // indirect
+ github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/sync v0.3.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/pkg/signature/kms/aws/go.sum b/pkg/signature/kms/aws/go.sum
new file mode 100644
index 0000000..62eee92
--- /dev/null
+++ b/pkg/signature/kms/aws/go.sum
@@ -0,0 +1,92 @@
+github.com/aws/aws-sdk-go v1.49.4 h1:qiXsqEeLLhdLgUIyfr5ot+N/dGPWALmtM1SetRmbUlY=
+github.com/aws/aws-sdk-go v1.49.4/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk=
+github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
+github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o=
+github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0=
+github.com/aws/aws-sdk-go-v2/service/kms v1.27.6 h1:zzaFokMF7UVk22/Igtb93A1ReGP50uu99ldLWaEMfHc=
+github.com/aws/aws-sdk-go-v2/service/kms v1.27.6/go.mod h1:D9FVDkZjkZnnFHymJ3fPVz0zOUlNSd0xcIIVmmrAac8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38=
+github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg=
+github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU=
+github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
+github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
+github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8=
+github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
+go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
+gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/signature/kms/aws/signer.go b/pkg/signature/kms/aws/signer.go
new file mode 100644
index 0000000..1f83561
--- /dev/null
+++ b/pkg/signature/kms/aws/signer.go
@@ -0,0 +1,247 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package aws
+
+import (
+ "context"
+ "crypto"
+ "fmt"
+ "io"
+
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/kms/types"
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+var awsSupportedAlgorithms = []types.CustomerMasterKeySpec{
+ types.CustomerMasterKeySpecRsa2048,
+ types.CustomerMasterKeySpecRsa3072,
+ types.CustomerMasterKeySpecRsa4096,
+ types.CustomerMasterKeySpecEccNistP256,
+ types.CustomerMasterKeySpecEccNistP384,
+ types.CustomerMasterKeySpecEccNistP521,
+}
+
+var awsSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+}
+
+// SignerVerifier is a signature.SignerVerifier that uses the AWS Key Management Service
+type SignerVerifier struct {
+ client *awsClient
+}
+
+// LoadSignerVerifier generates signatures using the specified key object in AWS KMS and hash algorithm.
+//
+// It also can verify signatures locally using the public key. hashFunc must not be crypto.Hash(0).
+func LoadSignerVerifier(ctx context.Context, referenceStr string, opts ...func(*config.LoadOptions) error) (*SignerVerifier, error) {
+ a := &SignerVerifier{}
+
+ var err error
+ a.client, err = newAWSClient(ctx, referenceStr, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return a, nil
+}
+
+// SignMessage signs the provided message using AWS KMS. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the Signer was created.
+//
+// SignMessage recognizes the following Options listed in order of preference:
+//
+// - WithContext()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (a *SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ var digest []byte
+ var err error
+ ctx := context.Background()
+
+ for _, opt := range opts {
+ opt.ApplyContext(&ctx)
+ opt.ApplyDigest(&digest)
+ }
+
+ var signerOpts crypto.SignerOpts
+ signerOpts, err = a.client.getHashFunc(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("getting fetching default hash function: %w", err)
+ }
+ for _, opt := range opts {
+ opt.ApplyCryptoSignerOpts(&signerOpts)
+ }
+
+ hf := signerOpts.HashFunc()
+
+ if len(digest) == 0 {
+ digest, hf, err = signature.ComputeDigestForSigning(message, hf, awsSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return a.client.sign(ctx, digest, hf)
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. If the caller wishes to specify the context to use to obtain
+// the public key, pass option.WithContext(desiredCtx).
+//
+// All other options are ignored if specified.
+func (a *SignerVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ ctx := context.Background()
+ for _, opt := range opts {
+ opt.ApplyContext(&ctx)
+ }
+
+ cmk, err := a.client.getCMK(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return cmk.PublicKey, nil
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the SignerVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithContext()
+//
+// - WithDigest()
+//
+// - WithRemoteVerification()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (a *SignerVerifier) VerifySignature(sig, message io.Reader, opts ...signature.VerifyOption) (err error) {
+ ctx := context.Background()
+ var digest []byte
+ var remoteVerification bool
+
+ for _, opt := range opts {
+ opt.ApplyContext(&ctx)
+ opt.ApplyDigest(&digest)
+ opt.ApplyRemoteVerification(&remoteVerification)
+ }
+
+ if !remoteVerification {
+ return a.client.verify(ctx, sig, message, opts...)
+ }
+
+ var signerOpts crypto.SignerOpts
+ signerOpts, err = a.client.getHashFunc(ctx)
+ if err != nil {
+ return fmt.Errorf("getting hash func: %w", err)
+ }
+ for _, opt := range opts {
+ opt.ApplyCryptoSignerOpts(&signerOpts)
+ }
+ hf := signerOpts.HashFunc()
+
+ if len(digest) == 0 {
+ digest, _, err = signature.ComputeDigestForVerifying(message, hf, awsSupportedHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+ }
+
+ sigBytes, err := io.ReadAll(sig)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+ return a.client.verifyRemotely(ctx, sigBytes, digest)
+}
+
+// CreateKey attempts to create a new key in Vault with the specified algorithm.
+func (a *SignerVerifier) CreateKey(ctx context.Context, algorithm string) (crypto.PublicKey, error) {
+ return a.client.createKey(ctx, algorithm)
+}
+
+type cryptoSignerWrapper struct {
+ ctx context.Context
+ hashFunc crypto.Hash
+ sv *SignerVerifier
+ errFunc func(error)
+}
+
+func (c cryptoSignerWrapper) Public() crypto.PublicKey {
+ pk, err := c.sv.PublicKey(options.WithContext(c.ctx))
+ if err != nil && c.errFunc != nil {
+ c.errFunc(err)
+ }
+ return pk
+}
+
+func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ hashFunc := c.hashFunc
+ if opts != nil {
+ hashFunc = opts.HashFunc()
+ }
+ awsOptions := []signature.SignOption{
+ options.WithContext(c.ctx),
+ options.WithDigest(digest),
+ options.WithCryptoSignerOpts(hashFunc),
+ }
+
+ return c.sv.SignMessage(nil, awsOptions...)
+}
+
+// CryptoSigner returns a crypto.Signer object that uses the underlying SignerVerifier, along with a crypto.SignerOpts object
+// that allows the KMS to be used in APIs that only accept the standard golang objects
+func (a *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) {
+ defaultHf, err := a.client.getHashFunc(ctx)
+ if err != nil {
+ return nil, nil, fmt.Errorf("getting fetching default hash function: %w", err)
+ }
+
+ csw := &cryptoSignerWrapper{
+ ctx: ctx,
+ sv: a,
+ hashFunc: defaultHf,
+ errFunc: errFunc,
+ }
+
+ return csw, defaultHf, nil
+}
+
+// SupportedAlgorithms returns the list of algorithms supported by the AWS KMS service
+func (*SignerVerifier) SupportedAlgorithms() []string {
+ s := make([]string, len(awsSupportedAlgorithms))
+ for i := range awsSupportedAlgorithms {
+ s[i] = string(awsSupportedAlgorithms[i])
+ }
+ return s
+}
+
+// DefaultAlgorithm returns the default algorithm for the AWS KMS service
+func (*SignerVerifier) DefaultAlgorithm() string {
+ return string(types.CustomerMasterKeySpecEccNistP256)
+}
diff --git a/pkg/signature/kms/azure/README.md b/pkg/signature/kms/azure/README.md
new file mode 100644
index 0000000..f0aacb8
--- /dev/null
+++ b/pkg/signature/kms/azure/README.md
@@ -0,0 +1,76 @@
+# Azure KMS
+
+In order to use Azure KMS ([Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/basic-concepts)) with the sigstore project you need to have a few things setup in Azure first.
+The key creation will be handled in sigstore, however the Azure Key Vault and the required permission will have to be configured before.
+
+## Azure Prerequisites
+
+- [Resource Group](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal#what-is-a-resource-group)
+- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/basic-concepts)
+- [Key Vault permissions](https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide)
+- [Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-intro) _(not required, but used in below examples)_
+
+## Permissions (Access Policies)
+
+Different commands require different Key Vault access policies. For more information check the official [Azure Docs](https://azure.microsoft.com/en-us/services/key-vault/).
+
+## Using Azure KMS with Cosign
+
+An Azure KMS key must be provided in the following format:
+`azurekms://[Key Vault Name].vault.azure.net/[Key Name]`
+
+A specific key version can optionally be provided:
+`azurekms://[Key Vault Name].vault.azure.net/[Key Name]/[Key Version]`
+
+### cosign generate-key-pair
+
+Required access policies (keys): `get`, `create`
+
+```shell
+cosign generate-key-pair --kms azurekms://[Key Vault Name].vault.azure.net/[Key Name]
+```
+
+### cosign sign
+
+Required access policies (keys): `get`, `sign`
+
+```shell
+az acr login --name [Container Registry Name]
+cosign sign --key azurekms://[Key Vault Name].vault.azure.net/[Key Name] [Container Registry Name].azurecr.io/[Image Name]
+```
+
+### cosign verify
+
+Required access policy (keys): `verify`
+
+```shell
+az acr login --name [Container Registry Name]
+cosign verify --key azurekms://[Key Vault Name].vault.azure.net/[Key Name] [Container Registry Name].azurecr.io/[Image Name]
+```
+
+## Authentication
+
+There are multiple authentication methods supported for Azure Key Vault and by default they will be evaluated in the following order:
+
+1. Client credentials (FromEnvironment)
+1. Client certificate (FromEnvironment)
+1. Username password (FromEnvironment)
+1. MSI (FromEnvironment)
+1. CLI (FromCLI)
+
+You can force either `FromEnvironment` or `FromCLI` by configuring the environment variable `AZURE_AUTH_METHOD` to either `environment` or `cli`.
+
+For backward compatibility, if you configure `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` and `AZURE_CLIENT_SECRET`, `FromEnvironment` will be used.
+
+If you would like to use a cloud other than the Azure public cloud, configure `AZURE_ENVIRONMENT`. The following values are accepted:
+- `AZUREUSGOVERNMENT`, `AZUREUSGOVERNMENTCLOUD` uses the Azure US Government Cloud
+- `AZURECHINACLOUD` uses Azure China Cloud
+- `AZURECLOUD`, `AZUREPUBLICCLOUD` uses the public cloud
+
+If `AZURE_ENVIRONMENT` is not configured, Azure public cloud is used.
+
+## Integration Testing
+
+In addition to unit tests in this module, there is `integration_test.go`, which requires you to provide either environment or CLI credentials. Because the Sigstore project does not use Azure, the tests are not run as part of any CI/CD. These tests are for Azure client developers to test that changes work as expected against their own Azure subscription.
+
+Run the integration tests with `go test -tags=integration ./...` in the root of this module.
diff --git a/pkg/signature/kms/azure/client.go b/pkg/signature/kms/azure/client.go
new file mode 100644
index 0000000..883d78d
--- /dev/null
+++ b/pkg/signature/kms/azure/client.go
@@ -0,0 +1,447 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package azure implement the interface with microsoft azure kms service
+package azure
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+
+ "github.com/go-jose/go-jose/v3"
+ "github.com/jellydator/ttlcache/v3"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
+ "github.com/sigstore/sigstore/pkg/signature"
+ sigkms "github.com/sigstore/sigstore/pkg/signature/kms"
+)
+
+func init() {
+ sigkms.AddProvider(ReferenceScheme, func(ctx context.Context, keyResourceID string, _ crypto.Hash, opts ...signature.RPCOption) (sigkms.SignerVerifier, error) {
+ return LoadSignerVerifier(ctx, keyResourceID)
+ })
+}
+
+type kvClient interface {
+ CreateKey(ctx context.Context, name string, parameters azkeys.CreateKeyParameters, options *azkeys.CreateKeyOptions) (azkeys.CreateKeyResponse, error)
+ GetKey(ctx context.Context, name, version string, options *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error)
+ Sign(ctx context.Context, name, version string, parameters azkeys.SignParameters, options *azkeys.SignOptions) (azkeys.SignResponse, error)
+ Verify(ctx context.Context, name, version string, parameters azkeys.VerifyParameters, options *azkeys.VerifyOptions) (azkeys.VerifyResponse, error)
+}
+
+type azureVaultClient struct {
+ client kvClient
+ keyCache *ttlcache.Cache[string, crypto.PublicKey]
+ vaultURL string
+ keyName string
+ keyVersion string
+}
+
+var (
+ errAzureReference = errors.New("kms specification should be in the format azurekms://[VAULT_NAME][VAULT_URL]/[KEY_NAME]/[VERSION (optional)]")
+
+ referenceRegex = regexp.MustCompile(`^azurekms://([^/]+)/([^/]+)(/[a-z0-9]*)?$`)
+)
+
+const (
+ // ReferenceScheme schemes for various KMS services are copied from https://github.com/google/go-cloud/tree/master/secrets
+ ReferenceScheme = "azurekms://"
+ cacheKey = "azure_vault_signer"
+ azureClientID = "AZURE_CLIENT_ID"
+)
+
+// ValidReference returns a non-nil error if the reference string is invalid
+func ValidReference(ref string) error {
+ if !referenceRegex.MatchString(ref) {
+ return errAzureReference
+ }
+ return nil
+}
+
+// The key version can be optionally provided
+// If provided, all key operations will specify this version.
+// If not provided, the key operations will use the latest key version by default.
+func parseReference(resourceID string) (vaultURL, keyName, keyVersion string, err error) {
+ if isIDValid := referenceRegex.MatchString(resourceID); !isIDValid {
+ err = fmt.Errorf("invalid azurekms format %q", resourceID)
+ return
+ }
+
+ fullRef := strings.Split(resourceID, "azurekms://")[1]
+ splitRef := strings.Split(fullRef, "/")
+ vaultURL = fmt.Sprintf("https://%s/", splitRef[0])
+ keyName = splitRef[1]
+
+ if len(splitRef) == 3 {
+ keyVersion = splitRef[2]
+ }
+
+ return
+}
+
+func newAzureKMS(keyResourceID string) (*azureVaultClient, error) {
+ if err := ValidReference(keyResourceID); err != nil {
+ return nil, err
+ }
+ vaultURL, keyName, keyVersion, err := parseReference(keyResourceID)
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := getKeysClient(vaultURL)
+ if err != nil {
+ return nil, fmt.Errorf("new azure kms client: %w", err)
+ }
+
+ azClient := &azureVaultClient{
+ client: client,
+ vaultURL: vaultURL,
+ keyName: keyName,
+ keyVersion: keyVersion,
+ keyCache: ttlcache.New[string, crypto.PublicKey](
+ ttlcache.WithDisableTouchOnHit[string, crypto.PublicKey](),
+ ),
+ }
+
+ return azClient, nil
+}
+
+type authenticationMethod string
+
+const (
+ unknownAuthenticationMethod = "unknown"
+ environmentAuthenticationMethod = "environment"
+ cliAuthenticationMethod = "cli"
+)
+
+// getAuthMethod returns the an authenticationMethod to use to get an Azure Authorizer.
+// If no environment variables are set, unknownAuthMethod will be used.
+// If the environment variable 'AZURE_AUTH_METHOD' is set to either environment or cli, use it.
+// If the environment variables 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID' and 'AZURE_CLIENT_SECRET' are set, use environment.
+func getAuthenticationMethod() authenticationMethod {
+ tenantID := os.Getenv("AZURE_TENANT_ID")
+ clientID := os.Getenv("AZURE_CLIENT_ID")
+ clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
+ authMethod := os.Getenv("AZURE_AUTH_METHOD")
+
+ if authMethod != "" {
+ switch strings.ToLower(authMethod) {
+ case "environment":
+ return environmentAuthenticationMethod
+ case "cli":
+ return cliAuthenticationMethod
+ }
+ }
+
+ if tenantID != "" && clientID != "" && clientSecret != "" {
+ return environmentAuthenticationMethod
+ }
+
+ return unknownAuthenticationMethod
+}
+
+type azureCredential interface {
+ GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error)
+}
+
+func getAzClientOpts() azcore.ClientOptions {
+ envName := os.Getenv("AZURE_ENVIRONMENT")
+ switch envName {
+ case "AZUREUSGOVERNMENT", "AZUREUSGOVERNMENTCLOUD":
+ return azcore.ClientOptions{Cloud: cloud.AzureGovernment}
+ case "AZURECHINACLOUD":
+ return azcore.ClientOptions{Cloud: cloud.AzureChina}
+ case "AZURECLOUD", "AZUREPUBLICCLOUD":
+ return azcore.ClientOptions{Cloud: cloud.AzurePublic}
+ default:
+ return azcore.ClientOptions{Cloud: cloud.AzurePublic}
+ }
+}
+
+// getAzureCredential takes an authenticationMethod and returns an Azure credential or an error.
+// If the method is unknown, Environment will be tested and if it returns an error CLI will be tested.
+// If the method is specified, the specified method will be used and no other will be tested.
+// This means the following default order of methods will be used if nothing else is defined:
+// 1. Client credentials (FromEnvironment)
+// 2. Client certificate (FromEnvironment)
+// 3. Username password (FromEnvironment)
+// 4. MSI (FromEnvironment)
+// 5. CLI (FromCLI)
+func getAzureCredential(method authenticationMethod) (azureCredential, error) {
+ clientOpts := getAzClientOpts()
+
+ switch method {
+ case environmentAuthenticationMethod:
+ envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOpts})
+ if err == nil {
+ return envCred, nil
+ }
+
+ o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOpts}
+ if ID, ok := os.LookupEnv(azureClientID); ok {
+ o.ID = azidentity.ClientID(ID)
+ }
+ msiCred, err := azidentity.NewManagedIdentityCredential(o)
+ if err == nil {
+ return msiCred, nil
+ }
+
+ return nil, fmt.Errorf("failed to create default azure credential from env auth method: %w", err)
+ case cliAuthenticationMethod:
+ cred, err := azidentity.NewAzureCLICredential(nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create default Azure credential from env auth method: %w", err)
+ }
+ return cred, nil
+ case unknownAuthenticationMethod:
+ break
+ default:
+ return nil, fmt.Errorf("you should never reach this")
+ }
+
+ envCreds, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOpts})
+ if err == nil {
+ return envCreds, nil
+ }
+
+ cliCreds, err := azidentity.NewAzureCLICredential(nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create default Azure credential from env auth method: %w", err)
+ }
+ return cliCreds, nil
+}
+
+func getKeysClient(vaultURL string) (*azkeys.Client, error) {
+ authMethod := getAuthenticationMethod()
+ cred, err := getAzureCredential(authMethod)
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := azkeys.NewClient(vaultURL, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return client, nil
+}
+
+func (a *azureVaultClient) fetchPublicKey(ctx context.Context) (crypto.PublicKey, error) {
+ keyBundle, err := a.getKey(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("public key: %w", err)
+ }
+
+ key := keyBundle.Key
+ keyType := key.Kty
+
+ // Azure Key Vault allows keys to be stored in either default Key Vault storage
+ // or in managed HSMs. If the key is stored in a HSM, the key type is suffixed
+ // with "-HSM". Since this suffix is specific to Azure Key Vault, it needs
+ // be stripped from the key type before attempting to represent the key
+ // with a go-jose/JSONWebKey struct.
+ switch *keyType {
+ case azkeys.KeyTypeECHSM:
+ *key.Kty = azkeys.KeyTypeEC
+ case azkeys.KeyTypeRSAHSM:
+ *key.Kty = azkeys.KeyTypeRSA
+ }
+
+ jwkJSON, err := json.Marshal(*key)
+ if err != nil {
+ return nil, fmt.Errorf("encoding the jsonWebKey: %w", err)
+ }
+
+ jwk := jose.JSONWebKey{}
+ err = jwk.UnmarshalJSON(jwkJSON)
+ if err != nil {
+ return nil, fmt.Errorf("decoding the jsonWebKey: %w", err)
+ }
+
+ return jwk.Key, nil
+}
+
+func (a *azureVaultClient) getKey(ctx context.Context) (azkeys.KeyBundle, error) {
+ resp, err := a.client.GetKey(ctx, a.keyName, a.keyVersion, nil)
+ if err != nil {
+ return azkeys.KeyBundle{}, fmt.Errorf("public key: %w", err)
+ }
+
+ return resp.KeyBundle, err
+}
+
+func (a *azureVaultClient) public(ctx context.Context) (crypto.PublicKey, error) {
+ var lerr error
+ loader := ttlcache.LoaderFunc[string, crypto.PublicKey](
+ func(c *ttlcache.Cache[string, crypto.PublicKey], key string) *ttlcache.Item[string, crypto.PublicKey] {
+ ttl := 300 * time.Second
+ var pubKey crypto.PublicKey
+ pubKey, lerr = a.fetchPublicKey(ctx)
+ if lerr == nil {
+ return c.Set(cacheKey, pubKey, ttl)
+ }
+ return nil
+ },
+ )
+ item := a.keyCache.Get(cacheKey, ttlcache.WithLoader[string, crypto.PublicKey](loader))
+ if lerr != nil {
+ return nil, lerr
+ }
+ return item.Value(), nil
+}
+
+func (a *azureVaultClient) createKey(ctx context.Context) (crypto.PublicKey, error) {
+ // check if the key already exists by attempting to fetch it
+ _, err := a.getKey(ctx)
+ // if the error is nil, this means the key already exists
+ // and we can return the public key
+ if err == nil {
+ return a.public(ctx)
+ }
+
+ // If the returned error is not nil, set the error to the
+ // custom azcore.ResponseError error implementation
+ // this custom error allows us to check the status code
+ // returned by the GetKey operation. If the operation
+ // returned a 404, we know that the key does not exist
+ // and we can create it.
+ var respErr *azcore.ResponseError
+ if ok := errors.As(err, &respErr); !ok {
+ return nil, fmt.Errorf("unexpected error returned by get key operation: %w", err)
+ }
+
+ // if a non-404 status code is returned, return the error
+ // since this is an unexpected error response
+ if respErr.StatusCode != http.StatusNotFound {
+ return nil, fmt.Errorf("unexpected status code returned by get key operation: %w", err)
+ }
+
+ // if a 404 was returned, then we can create the key
+ _, err = a.client.CreateKey(
+ ctx,
+ a.keyName,
+ azkeys.CreateKeyParameters{
+ KeyAttributes: &azkeys.KeyAttributes{
+ Enabled: to.Ptr(true),
+ },
+ KeySize: to.Ptr(int32(2048)),
+ KeyOps: []*azkeys.KeyOperation{
+ to.Ptr(azkeys.KeyOperationSign),
+ to.Ptr(azkeys.KeyOperationVerify),
+ },
+ Kty: to.Ptr(azkeys.KeyTypeEC),
+ Tags: map[string]*string{
+ "use": to.Ptr("sigstore"),
+ },
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return a.public(ctx)
+}
+
+func (a *azureVaultClient) getKeyVaultHashFunc(ctx context.Context) (crypto.Hash, azkeys.SignatureAlgorithm, error) {
+ publicKey, err := a.public(ctx)
+ if err != nil {
+ return 0, "", fmt.Errorf("failed to get public key: %w", err)
+ }
+ switch keyImpl := publicKey.(type) {
+ case *ecdsa.PublicKey:
+ switch keyImpl.Curve {
+ case elliptic.P256():
+ return crypto.SHA256, azkeys.SignatureAlgorithmES256, nil
+ case elliptic.P384():
+ return crypto.SHA384, azkeys.SignatureAlgorithmES384, nil
+ case elliptic.P521():
+ return crypto.SHA512, azkeys.SignatureAlgorithmES512, nil
+ default:
+ return 0, "", fmt.Errorf("unsupported key size: %s", keyImpl.Params().Name)
+ }
+ case *rsa.PublicKey:
+ switch keyImpl.Size() {
+ case 256:
+ return crypto.SHA256, azkeys.SignatureAlgorithmRS256, nil
+ case 384:
+ return crypto.SHA384, azkeys.SignatureAlgorithmRS384, nil
+ case 512:
+ return crypto.SHA512, azkeys.SignatureAlgorithmRS512, nil
+ default:
+ return 0, "", fmt.Errorf("unsupported key size: %d", keyImpl.Size())
+ }
+ default:
+ return 0, "", fmt.Errorf("unsupported public key type: %T", publicKey)
+ }
+}
+
+func (a *azureVaultClient) sign(ctx context.Context, hash []byte) ([]byte, error) {
+ _, keyVaultAlgo, err := a.getKeyVaultHashFunc(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get KeyVaultSignatureAlgorithm: %w", err)
+ }
+
+ params := azkeys.SignParameters{
+ Algorithm: &keyVaultAlgo,
+ Value: hash,
+ }
+
+ result, err := a.client.Sign(ctx, a.keyName, a.keyVersion, params, nil)
+ if err != nil {
+ return nil, fmt.Errorf("signing the payload: %w", err)
+ }
+
+ return result.Result, nil
+}
+
+func (a *azureVaultClient) verify(ctx context.Context, signature, hash []byte) error {
+ _, keyVaultAlgo, err := a.getKeyVaultHashFunc(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get KeyVaultSignatureAlgorithm: %w", err)
+ }
+
+ params := azkeys.VerifyParameters{
+ Algorithm: &keyVaultAlgo,
+ Digest: hash,
+ Signature: signature,
+ }
+
+ result, err := a.client.Verify(ctx, a.keyName, a.keyVersion, params, nil)
+ if err != nil {
+ return fmt.Errorf("verify: %w", err)
+ }
+
+ if !*result.Value {
+ return errors.New("failed vault verification")
+ }
+
+ return nil
+}
diff --git a/pkg/signature/kms/azure/client_test.go b/pkg/signature/kms/azure/client_test.go
new file mode 100644
index 0000000..6c02e87
--- /dev/null
+++ b/pkg/signature/kms/azure/client_test.go
@@ -0,0 +1,409 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package azure
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "testing"
+
+ "github.com/jellydator/ttlcache/v3"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
+)
+
+type testKVClient struct {
+ key azkeys.JSONWebKey
+}
+
+func (c *testKVClient) CreateKey(_ context.Context, _ string, _ azkeys.CreateKeyParameters, _ *azkeys.CreateKeyOptions) (azkeys.CreateKeyResponse, error) {
+ key, err := generatePublicKey("EC")
+ if err != nil {
+ return azkeys.CreateKeyResponse{}, err
+ }
+ c.key = key
+
+ return azkeys.CreateKeyResponse{
+ KeyBundle: azkeys.KeyBundle{
+ Key: &key,
+ },
+ }, nil
+}
+
+func (c *testKVClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error) {
+ return azkeys.GetKeyResponse{
+ KeyBundle: azkeys.KeyBundle{
+ Key: &c.key,
+ },
+ }, nil
+}
+
+func (c *testKVClient) Sign(_ context.Context, _, _ string, _ azkeys.SignParameters, _ *azkeys.SignOptions) (result azkeys.SignResponse, err error) {
+ return result, nil
+}
+
+func (c *testKVClient) Verify(_ context.Context, _, _ string, _ azkeys.VerifyParameters, _ *azkeys.VerifyOptions) (result azkeys.VerifyResponse, err error) {
+ return result, nil
+}
+
+type keyNotFoundClient struct {
+ testKVClient
+ key azkeys.JSONWebKey
+ getKeyReturnsErr bool
+ getKeyCallThreshold int
+ getKeyCallCount int
+}
+
+func (c *keyNotFoundClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error) {
+ if c.getKeyReturnsErr && c.getKeyCallCount < c.getKeyCallThreshold {
+ c.getKeyCallCount++
+ return azkeys.GetKeyResponse{}, &azcore.ResponseError{
+ StatusCode: http.StatusNotFound,
+ RawResponse: &http.Response{},
+ }
+ }
+
+ return azkeys.GetKeyResponse{
+ KeyBundle: azkeys.KeyBundle{
+ Key: &c.key,
+ },
+ }, nil
+}
+
+type nonResponseErrClient struct {
+ testKVClient
+ keyCache *ttlcache.Cache[string, crypto.PublicKey]
+}
+
+func (c *nonResponseErrClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) {
+ err = errors.New("unexpected error")
+ return result, err
+}
+
+type non404RespClient struct {
+ testKVClient
+ keyCache *ttlcache.Cache[string, crypto.PublicKey]
+}
+
+func (c *non404RespClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) {
+ err = &azcore.ResponseError{
+ StatusCode: http.StatusServiceUnavailable,
+ }
+
+ return result, err
+}
+
+func generatePublicKey(azureKeyType string) (azkeys.JSONWebKey, error) {
+ keyOps := []*azkeys.KeyOperation{to.Ptr(azkeys.KeyOperationSign), to.Ptr(azkeys.KeyOperationVerify)}
+ kid := "https://honk-vault.vault.azure.net/keys/honk-key/abc123"
+
+ key := azkeys.JSONWebKey{
+ KID: to.Ptr(azkeys.ID(kid)),
+ Kty: to.Ptr(azkeys.KeyType(azureKeyType)),
+ Crv: to.Ptr(azkeys.CurveName("P-256")),
+ KeyOps: keyOps,
+ }
+
+ keyType := azkeys.KeyType(azureKeyType)
+ switch keyType {
+ case azkeys.KeyTypeEC, azkeys.KeyTypeECHSM:
+ privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return azkeys.JSONWebKey{}, err
+ }
+
+ ecdsaPub, ok := privKey.Public().(*ecdsa.PublicKey)
+ if !ok {
+ return azkeys.JSONWebKey{}, fmt.Errorf("failed to cast public key to esdsa public key")
+ }
+
+ key.X = ecdsaPub.X.Bytes()
+ key.Y = ecdsaPub.Y.Bytes()
+
+ return key, nil
+ case azkeys.KeyTypeRSA, azkeys.KeyTypeRSAHSM:
+ privKey, err := rsa.GenerateKey(rand.Reader, 256)
+ if err != nil {
+ return azkeys.JSONWebKey{}, err
+ }
+
+ rsaPub, ok := privKey.Public().(*rsa.PublicKey)
+ if !ok {
+ return azkeys.JSONWebKey{}, fmt.Errorf("failed to cast public key to rsa public key")
+ }
+
+ key.N = rsaPub.N.Bytes()
+ key.E = []byte(fmt.Sprint(rsaPub.E))
+
+ return key, nil
+ default:
+ return azkeys.JSONWebKey{}, fmt.Errorf("invalid key type passed: %s", azureKeyType)
+ }
+}
+
+func TestAzureVaultClientFetchPublicKey(t *testing.T) {
+ type test struct {
+ azureKeyType string
+ expectSuccess bool
+ }
+
+ tests := []test{
+ {
+ azureKeyType: "EC",
+ expectSuccess: true,
+ },
+ {
+ azureKeyType: "EC-HSM",
+ expectSuccess: true,
+ },
+ {
+ azureKeyType: "RSA",
+ expectSuccess: true,
+ },
+ {
+ azureKeyType: "RSA-HSM",
+ expectSuccess: true,
+ },
+ }
+
+ for _, tc := range tests {
+ key, err := generatePublicKey(tc.azureKeyType)
+ if err != nil {
+ t.Fatalf("unexpected error while generating public key for testing: %v", err)
+ }
+
+ kvClient := testKVClient{key: key}
+ client := azureVaultClient{
+ client: &kvClient,
+ }
+
+ _, err = client.fetchPublicKey(context.Background())
+ if err != nil && tc.expectSuccess {
+ t.Fatalf("expected error to be nil, actual value: %v", err)
+ }
+ if err == nil && !tc.expectSuccess {
+ t.Fatal("expected error not to be nil")
+ }
+ }
+}
+
+func TestAzureVaultClientCreateKey(t *testing.T) {
+ type test struct {
+ name string
+ client kvClient
+ expectSuccess bool
+ }
+
+ key, err := generatePublicKey("EC")
+ if err != nil {
+ t.Fatalf("unexpected error while generating public key for testing: %v", err)
+ }
+
+ tests := []test{
+ {
+ name: "Successfully create key if it doesn't exist",
+ client: &keyNotFoundClient{
+ key: key,
+ getKeyReturnsErr: true,
+ getKeyCallThreshold: 1,
+ },
+ expectSuccess: true,
+ },
+ {
+ name: "Return public key if it already exists",
+ client: &testKVClient{
+ key: key,
+ },
+ expectSuccess: true,
+ },
+ {
+ name: "Fail to create key due to unknown error",
+ client: &nonResponseErrClient{},
+ expectSuccess: false,
+ },
+ {
+ name: "Fail to create key due to non-404 status code error",
+ client: &non404RespClient{},
+ expectSuccess: false,
+ },
+ }
+
+ for _, tc := range tests {
+ client := azureVaultClient{
+ client: tc.client,
+ keyCache: ttlcache.New[string, crypto.PublicKey](
+ ttlcache.WithDisableTouchOnHit[string, crypto.PublicKey](),
+ ),
+ }
+
+ _, err = client.createKey(context.Background())
+ if err != nil && tc.expectSuccess {
+ t.Fatalf("Test '%s' failed. Expected nil error, actual value: %v", tc.name, err)
+ }
+ if err == nil && !tc.expectSuccess {
+ t.Fatalf("Test '%s' failed. Expected non-nil error", tc.name)
+ }
+ }
+}
+
+func TestGetAuthenticationMethod(t *testing.T) {
+ clearEnv := map[string]string{
+ "AZURE_TENANT_ID": "",
+ "AZURE_CLIENT_ID": "",
+ "AZURE_CLIENT_SECRET": "",
+ "AZURE_AUTH_METHOD": "",
+ }
+ resetEnv := testSetEnv(t, clearEnv)
+ defer resetEnv()
+
+ cases := []struct {
+ testDescription string
+ environmentVariables map[string]string
+ expectedResult authenticationMethod
+ }{
+ {
+ testDescription: "No environment variables set",
+ environmentVariables: map[string]string{},
+ expectedResult: unknownAuthenticationMethod,
+ },
+ {
+ testDescription: "AZURE_AUTH_METHOD=environment",
+ environmentVariables: map[string]string{
+ "AZURE_AUTH_METHOD": "environment",
+ },
+ expectedResult: environmentAuthenticationMethod,
+ },
+ {
+ testDescription: "AZURE_AUTH_METHOD=cli",
+ environmentVariables: map[string]string{
+ "AZURE_AUTH_METHOD": "cli",
+ },
+ expectedResult: cliAuthenticationMethod,
+ },
+ {
+ testDescription: "Set environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID & AZURE_CLIENT_SECRET",
+ environmentVariables: map[string]string{
+ "AZURE_TENANT_ID": "foo",
+ "AZURE_CLIENT_ID": "bar",
+ "AZURE_CLIENT_SECRET": "baz",
+ },
+ expectedResult: environmentAuthenticationMethod,
+ },
+ }
+
+ for i, c := range cases {
+ t.Logf("Test #%d: %s", i, c.testDescription)
+ reset := testSetEnv(t, c.environmentVariables)
+
+ result := getAuthenticationMethod()
+ if result != c.expectedResult {
+ t.Logf("got: %q, want: %q", result, c.expectedResult)
+ t.Fail()
+ }
+
+ reset()
+ }
+}
+
+func testSetEnv(t *testing.T, s map[string]string) func() {
+ t.Helper()
+
+ backup := map[string]string{}
+ for k, v := range s {
+ currentEnv := os.Getenv(k)
+ backup[k] = currentEnv
+ if v == "" {
+ os.Unsetenv(k)
+ continue
+ }
+ os.Setenv(k, v)
+ }
+
+ return func() {
+ for k, v := range backup {
+ if v == "" {
+ os.Unsetenv(k)
+ continue
+ }
+ os.Setenv(k, v)
+ }
+ }
+}
+
+func TestParseReference(t *testing.T) {
+ tests := []struct {
+ in string
+ wantVaultURL string
+ wantKeyName string
+ wantKeyVersion string
+ wantErr bool
+ }{
+ {
+ in: "azurekms://honk-vault.vault.azure.net/honk-key",
+ wantVaultURL: "https://honk-vault.vault.azure.net/",
+ wantKeyName: "honk-key",
+ wantKeyVersion: "",
+ wantErr: false,
+ },
+ {
+ in: "azurekms://honk-vault.vault.azure.net/honk-key/123abc",
+ wantVaultURL: "https://honk-vault.vault.azure.net/",
+ wantKeyName: "honk-key",
+ wantKeyVersion: "123abc",
+ wantErr: false,
+ },
+ {
+ in: "foo://bar",
+ wantErr: true,
+ },
+ {
+ in: "",
+ wantErr: true,
+ },
+ {
+ in: "azurekms://wrong-test/test/1/3",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ gotVaultURL, gotKeyName, gotKeyVersion, err := parseReference(tt.in)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("parseReference() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotVaultURL != tt.wantVaultURL {
+ t.Errorf("parseReference() gotVaultURL = %v, want %v", gotVaultURL, tt.wantVaultURL)
+ }
+ if gotKeyName != tt.wantKeyName {
+ t.Errorf("parseReference() gotKeyName = %v, want %v", gotKeyName, tt.wantKeyName)
+ }
+ if gotKeyVersion != tt.wantKeyVersion {
+ t.Errorf("parseReference() gotKeyVersion = %v, want %v", gotKeyVersion, tt.wantKeyVersion)
+ }
+ })
+ }
+}
diff --git a/pkg/signature/kms/azure/doc.go b/pkg/signature/kms/azure/doc.go
new file mode 100644
index 0000000..02fcc23
--- /dev/null
+++ b/pkg/signature/kms/azure/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package azure contains utilities related to Microsoft Azure KMS.
+package azure
diff --git a/pkg/signature/kms/azure/go.mod b/pkg/signature/kms/azure/go.mod
new file mode 100644
index 0000000..6a94d00
--- /dev/null
+++ b/pkg/signature/kms/azure/go.mod
@@ -0,0 +1,39 @@
+module github.com/sigstore/sigstore/pkg/signature/kms/azure
+
+replace github.com/sigstore/sigstore => ../../../../
+
+go 1.20
+
+require (
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1
+ github.com/go-jose/go-jose/v3 v3.0.1
+ github.com/google/go-cmp v0.6.0
+ github.com/jellydator/ttlcache/v3 v3.1.1
+ github.com/sigstore/sigstore v1.6.4
+ golang.org/x/crypto v0.17.0
+)
+
+require (
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
+ github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
+ github.com/google/go-containerregistry v0.17.0 // indirect
+ github.com/google/uuid v1.3.1 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
+ github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+ github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sync v0.3.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/pkg/signature/kms/azure/go.sum b/pkg/signature/kms/azure/go.sum
new file mode 100644
index 0000000..f26f3d8
--- /dev/null
+++ b/pkg/signature/kms/azure/go.sum
@@ -0,0 +1,97 @@
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
+github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
+github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8=
+github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
+github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
+go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
+gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/signature/kms/azure/integration_test.go b/pkg/signature/kms/azure/integration_test.go
new file mode 100644
index 0000000..63f7a6f
--- /dev/null
+++ b/pkg/signature/kms/azure/integration_test.go
@@ -0,0 +1,229 @@
+//go:build integration
+
+//
+// Copyright 2023 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package azure contains utilities related to Microsoft Azure KMS.
+package azure
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/google/go-cmp/cmp"
+)
+
+/*
+The following environment variables must be set:
+AZURE_KEY_REF - full azure key reference in the format azurekms://[Key Vault Name].vault.azure.net/[Key Name](/[Key Version]Optional)
+KEY_NAME - Azure key name
+VAULT_URL - Azure Vault URL
+*/
+
+func TestMain(m *testing.M) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+ if azureKeyRef == "" {
+ panic("AZURE_KEY_REF must be set")
+ }
+ os.Exit(m.Run())
+}
+
+func TestGetAzClientOpts(t *testing.T) {
+ testCases := []struct {
+ env string
+ expectedConfig cloud.Configuration
+ }{{
+ env: "AZUREUSGOVERNMENT",
+ expectedConfig: cloud.AzureGovernment,
+ }, {
+ env: "AZUREUSGOVERNMENTCLOUD",
+ expectedConfig: cloud.AzureGovernment,
+ }, {
+ env: "AZURECHINACLOUD",
+ expectedConfig: cloud.AzureChina,
+ }, {
+ env: "AZURECLOUD",
+ expectedConfig: cloud.AzurePublic,
+ }, {
+ env: "AZUREPUBLICCLOUD",
+ expectedConfig: cloud.AzurePublic,
+ }, {
+ env: "",
+ expectedConfig: cloud.AzurePublic,
+ }}
+
+ for _, tc := range testCases {
+ t.Setenv("AZURE_ENVIRONMENT", tc.env)
+
+ opts := getAzClientOpts()
+ if !cmp.Equal(tc.expectedConfig, opts.Cloud) {
+ t.Errorf("opts.Cloud %v does not match expected config: %v", opts.Cloud, tc.expectedConfig)
+ }
+ }
+}
+
+func TestLoadSignerVerifier(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ azureKeyName := os.Getenv("KEY_NAME")
+ if azureKeyName == "" {
+ t.Fatalf("KEY_NAME must be set")
+ }
+ azureVaultURL := os.Getenv("VAULT_URL")
+ if azureVaultURL == "" {
+ t.Fatalf("VAULT_URL must be set")
+ }
+ azureKeyVersion := os.Getenv("KEY_VERSION")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Errorf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ if sv == nil {
+ t.Errorf("LoadSignerVerifier failed to create a SignerVerifier instance")
+ }
+
+ if sv.client.vaultURL != fmt.Sprintf("https://%s/", azureVaultURL) {
+ t.Errorf("expected client.vaultURL to be %s, got %s", azureVaultURL, sv.client.vaultURL)
+ }
+ if sv.client.keyName != azureKeyName {
+ t.Errorf("expected client.keyName to be %s, got %s", azureKeyName, sv.client.keyName)
+ }
+ if sv.client.keyVersion != azureKeyVersion {
+ t.Errorf("expected client.keyVersion to be %s, got %s", azureKeyVersion, sv.client.keyVersion)
+ }
+}
+
+func TestCreateKey(t *testing.T) {
+ azureVaultURL := os.Getenv("VAULT_URL")
+ if azureVaultURL == "" {
+ t.Fatalf("VAULT_URL must be set")
+ }
+
+ newKeyRef := fmt.Sprintf("azurekms://%s.vault.azure.net/%s", azureVaultURL, "new-test-key")
+
+ sv, err := LoadSignerVerifier(context.Background(), newKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ publicKey, err := sv.client.createKey(context.Background())
+ if err != nil {
+ t.Errorf("getKey failed with error: %v", err)
+ }
+ if publicKey == nil {
+ t.Errorf("public key is nil")
+ }
+
+ if _, ok := publicKey.(*ecdsa.PublicKey); !ok {
+ t.Errorf("expected public key to be of type *ecdsa.PublicKey")
+ }
+}
+
+func TestGetKey(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ keyBundle, err := sv.client.getKey(context.Background())
+ if err != nil {
+ t.Errorf("getKey failed with error: %v", err)
+ }
+
+ if keyBundle.Key == nil {
+ t.Errorf("key bundle key is nil")
+ }
+}
+
+func TestPublicKey(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ pubKey, err := sv.PublicKey()
+ if err != nil {
+ t.Errorf("PublicKey failed with error: %v", err)
+ }
+ if pubKey == nil {
+ t.Errorf("PublicKey response is nil")
+ }
+}
+
+func TestGetKeyVaultHashFunc(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ _, _, err = sv.client.getKeyVaultHashFunc(context.Background())
+ if err != nil {
+ t.Errorf("failed to get crypto hash and signature algorithm associated with key: %v", err)
+ }
+}
+
+func TestSignMessage(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ messageToSign := strings.NewReader("myblob")
+ signed, err := sv.SignMessage(messageToSign)
+ if err != nil {
+ t.Errorf("SignMessage unexpectedly returned non-nil error: %v", err)
+ }
+ if signed == nil || len(signed) == 0 {
+ t.Errorf("SignMessage unexpected returned nil or empty signature")
+ }
+}
+
+func TestVerifySignature(t *testing.T) {
+ azureKeyRef := os.Getenv("AZURE_KEY_REF")
+
+ sv, err := LoadSignerVerifier(context.Background(), azureKeyRef)
+ if err != nil {
+ t.Fatalf("LoadSignerVerifier unexpectedly returned non-nil error: %v", err)
+ }
+
+ messageToSign := "myblob"
+ signed, err := sv.SignMessage(strings.NewReader(messageToSign))
+ if err != nil {
+ t.Errorf("SignMessage unexpectedly returned non-nil error: %v", err)
+ }
+ if signed == nil || len(signed) == 0 {
+ t.Errorf("SignMessage unexpected returned nil or empty signature")
+ }
+
+ err = sv.VerifySignature(bytes.NewReader(signed), strings.NewReader(messageToSign))
+ if err != nil {
+ t.Errorf("VerifySignature unexpectedly returned non-nil error: %v", err)
+ }
+}
diff --git a/pkg/signature/kms/azure/signer.go b/pkg/signature/kms/azure/signer.go
new file mode 100644
index 0000000..8932919
--- /dev/null
+++ b/pkg/signature/kms/azure/signer.go
@@ -0,0 +1,247 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package azure
+
+import (
+ "context"
+ "crypto"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+
+ "golang.org/x/crypto/cryptobyte"
+ "golang.org/x/crypto/cryptobyte/asn1"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+var azureSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+}
+
+//nolint:revive
+const (
+ AlgorithmES256 = "ES256"
+ AlgorithmES384 = "ES384"
+ AlgorithmES512 = "ES512"
+)
+
+var azureSupportedAlgorithms = []string{
+ AlgorithmES256,
+ AlgorithmES384,
+ AlgorithmES512,
+}
+
+// SignerVerifier creates and verifies digital signatures over a message using Azure KMS service
+type SignerVerifier struct {
+ defaultCtx context.Context
+ hashFunc crypto.Hash
+ client *azureVaultClient
+}
+
+// LoadSignerVerifier generates signatures using the specified key in Azure Key Vault and hash algorithm.
+//
+// It also can verify signatures locally using the public key. hashFunc must not be crypto.Hash(0).
+func LoadSignerVerifier(defaultCtx context.Context, referenceStr string) (*SignerVerifier, error) {
+ a := &SignerVerifier{
+ defaultCtx: defaultCtx,
+ }
+
+ var err error
+ a.client, err = newAzureKMS(referenceStr)
+ if err != nil {
+ return nil, err
+ }
+
+ return a, nil
+}
+
+// SignMessage signs the provided message using Azure Key Vault. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the Signer was created.
+//
+// SignMessage recognizes the following Options listed in order of preference:
+//
+// - WithContext()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (a *SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ var digest []byte
+
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ }
+
+ hashFunc, _, err := a.client.getKeyVaultHashFunc(a.defaultCtx)
+ if err != nil {
+ return nil, err
+ }
+
+ digest, _, err = signature.ComputeDigestForSigning(message, hashFunc, azureSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ rawSig, err := a.client.sign(a.defaultCtx, digest)
+ if err != nil {
+ return nil, err
+ }
+
+ l := len(rawSig)
+ r, s := &big.Int{}, &big.Int{}
+ r.SetBytes(rawSig[0 : l/2])
+ s.SetBytes(rawSig[l/2:])
+
+ // Convert the concatenated r||s byte string to an ASN.1 sequence
+ // This logic is borrowed from https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/crypto/ecdsa/ecdsa.go;l=121
+ var b cryptobyte.Builder
+ b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
+ b.AddASN1BigInt(r)
+ b.AddASN1BigInt(s)
+ })
+
+ return b.Bytes()
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the SignerVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// All other options are ignored if specified.
+func (a *SignerVerifier) VerifySignature(sig, message io.Reader, opts ...signature.VerifyOption) error {
+ hashFunc, _, err := a.client.getKeyVaultHashFunc(a.defaultCtx)
+ if err != nil {
+ return err
+ }
+
+ var digest []byte
+ var signerOpts crypto.SignerOpts = hashFunc
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ }
+
+ digest, _, err = signature.ComputeDigestForVerifying(message, signerOpts.HashFunc(), azureSupportedHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+
+ sigBytes, err := io.ReadAll(sig)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ // Convert the ASN.1 Sequence to a concatenated r||s byte string
+ // This logic is borrowed from https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/crypto/ecdsa/ecdsa.go;l=339
+ var (
+ r, s = &big.Int{}, &big.Int{}
+ inner cryptobyte.String
+ )
+ input := cryptobyte.String(sigBytes)
+ if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
+ !input.Empty() ||
+ !inner.ReadASN1Integer(r) ||
+ !inner.ReadASN1Integer(s) ||
+ !inner.Empty() {
+ return errors.New("parsing signature")
+ }
+
+ rawSigBytes := []byte{}
+ rawSigBytes = append(rawSigBytes, r.Bytes()...)
+ rawSigBytes = append(rawSigBytes, s.Bytes()...)
+ return a.client.verify(a.defaultCtx, rawSigBytes, digest)
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. All options provided in arguments to this method are ignored.
+func (a *SignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return a.client.public(a.defaultCtx)
+}
+
+// CreateKey attempts to create a new key in Vault with the specified algorithm.
+func (a *SignerVerifier) CreateKey(ctx context.Context, _ string) (crypto.PublicKey, error) {
+ return a.client.createKey(ctx)
+}
+
+type cryptoSignerWrapper struct {
+ ctx context.Context
+ hashFunc crypto.Hash
+ sv *SignerVerifier
+ errFunc func(error)
+}
+
+func (c cryptoSignerWrapper) Public() crypto.PublicKey {
+ pk, err := c.sv.PublicKey(options.WithContext(c.ctx))
+ if err != nil && c.errFunc != nil {
+ c.errFunc(err)
+ }
+ return pk
+}
+
+func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ hashFunc := c.hashFunc
+ if opts != nil {
+ hashFunc = opts.HashFunc()
+ }
+ azOptions := []signature.SignOption{
+ options.WithContext(c.ctx),
+ options.WithDigest(digest),
+ options.WithCryptoSignerOpts(hashFunc),
+ }
+
+ return c.sv.SignMessage(nil, azOptions...)
+}
+
+// CryptoSigner returns a crypto.Signer object that uses the underlying SignerVerifier, along with a crypto.SignerOpts object
+// that allows the KMS to be used in APIs that only accept the standard golang objects
+func (a *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) {
+ hashFunc, _, err := a.client.getKeyVaultHashFunc(a.defaultCtx)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ csw := &cryptoSignerWrapper{
+ ctx: ctx,
+ sv: a,
+ hashFunc: hashFunc,
+ errFunc: errFunc,
+ }
+
+ return csw, hashFunc, nil
+}
+
+// SupportedAlgorithms returns the list of algorithms supported by the Azure KMS service
+func (*SignerVerifier) SupportedAlgorithms() []string {
+ return azureSupportedAlgorithms
+}
+
+// DefaultAlgorithm returns the default algorithm for the Azure KMS service
+func (*SignerVerifier) DefaultAlgorithm() string {
+ return AlgorithmES256
+}
diff --git a/pkg/signature/kms/doc.go b/pkg/signature/kms/doc.go
new file mode 100644
index 0000000..592c9ab
--- /dev/null
+++ b/pkg/signature/kms/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package kms contains utilities related to third-party KMS providers.
+package kms
diff --git a/pkg/signature/kms/fake/doc.go b/pkg/signature/kms/fake/doc.go
new file mode 100644
index 0000000..9601d0f
--- /dev/null
+++ b/pkg/signature/kms/fake/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package fake contains utilities to help test KMS providers.
+package fake
diff --git a/pkg/signature/kms/fake/signer.go b/pkg/signature/kms/fake/signer.go
new file mode 100644
index 0000000..2d493ee
--- /dev/null
+++ b/pkg/signature/kms/fake/signer.go
@@ -0,0 +1,155 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package fake implements fake signer to be used in tests
+package fake
+
+import (
+ "context"
+ "crypto"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ sigkms "github.com/sigstore/sigstore/pkg/signature/kms"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// KmsCtxKey is used to look up the private key in the struct.
+type KmsCtxKey struct{}
+
+// SignerVerifier creates and verifies digital signatures over a message using an in-memory signer
+type SignerVerifier struct {
+ signer signature.SignerVerifier
+}
+
+// ReferenceScheme is a scheme for fake KMS keys. Do not use in production.
+const ReferenceScheme = "fakekms://"
+
+func init() {
+ sigkms.AddProvider(ReferenceScheme, func(ctx context.Context, _ string, hf crypto.Hash, _ ...signature.RPCOption) (sigkms.SignerVerifier, error) {
+ return LoadSignerVerifier(ctx, hf)
+ })
+}
+
+// LoadSignerVerifier generates a signer/verifier using the default ECDSA signer or loads
+// a signer from a provided private key and hash. The context should contain a mapping from
+// a string "priv" to a crypto.PrivateKey (RSA, ECDSA, or ED25519).
+func LoadSignerVerifier(ctx context.Context, hf crypto.Hash) (*SignerVerifier, error) {
+ val := ctx.Value(KmsCtxKey{})
+ if val == nil {
+ signer, _, err := signature.NewDefaultECDSASignerVerifier()
+ if err != nil {
+ return nil, err
+ }
+ sv := &SignerVerifier{
+ signer: signer,
+ }
+ return sv, nil
+ }
+ signer, err := signature.LoadSignerVerifier(val.(crypto.PrivateKey), hf)
+ if err != nil {
+ return nil, err
+ }
+ sv := &SignerVerifier{
+ signer: signer,
+ }
+ return sv, nil
+}
+
+// SignMessage signs the provided message using the in-memory signer.
+func (g *SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ return g.signer.SignMessage(message, opts...)
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer.
+func (g *SignerVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return g.signer.PublicKey(opts...)
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the SignerVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// All other options are ignored if specified.
+func (g *SignerVerifier) VerifySignature(signature, message io.Reader, opts ...signature.VerifyOption) error {
+ return g.signer.VerifySignature(signature, message, opts...)
+}
+
+// CreateKey returns the signer's public key.
+func (g *SignerVerifier) CreateKey(_ context.Context, _ string) (crypto.PublicKey, error) {
+ pub, err := g.signer.PublicKey()
+ if err != nil {
+ return nil, err
+ }
+ return pub, nil
+}
+
+type cryptoSignerWrapper struct {
+ ctx context.Context
+ hashFunc crypto.Hash
+ sv *SignerVerifier
+ errFunc func(error)
+}
+
+func (c cryptoSignerWrapper) Public() crypto.PublicKey {
+ pk, err := c.sv.PublicKey(options.WithContext(c.ctx))
+ if err != nil && c.errFunc != nil {
+ c.errFunc(err)
+ }
+ return pk
+}
+
+func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ hashFunc := c.hashFunc
+ if opts != nil {
+ hashFunc = opts.HashFunc()
+ }
+ gcpOptions := []signature.SignOption{
+ options.WithContext(c.ctx),
+ options.WithDigest(digest),
+ options.WithCryptoSignerOpts(hashFunc),
+ }
+
+ return c.sv.SignMessage(nil, gcpOptions...)
+}
+
+// CryptoSigner returns a crypto.Signer object that uses the underlying SignerVerifier, along with a crypto.SignerOpts object
+// that allows the KMS to be used in APIs that only accept the standard golang objects
+func (g *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) {
+ csw := &cryptoSignerWrapper{
+ ctx: ctx,
+ sv: g,
+ hashFunc: crypto.SHA256,
+ errFunc: errFunc,
+ }
+
+ return csw, crypto.SHA256, nil
+}
+
+// SupportedAlgorithms returns a list with the default algorithm
+func (g *SignerVerifier) SupportedAlgorithms() (result []string) {
+ return []string{"ecdsa-p256-sha256"}
+}
+
+// DefaultAlgorithm returns the default algorithm for the signer
+func (g *SignerVerifier) DefaultAlgorithm() string {
+ return "ecdsa-p256-sha256"
+}
diff --git a/pkg/signature/kms/fake/signer_test.go b/pkg/signature/kms/fake/signer_test.go
new file mode 100644
index 0000000..b55ed13
--- /dev/null
+++ b/pkg/signature/kms/fake/signer_test.go
@@ -0,0 +1,128 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fake
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/sha256"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "github.com/sigstore/sigstore/pkg/signature/kms"
+)
+
+func TestFakeSigner(t *testing.T) {
+ msg := []byte{1, 2, 3, 4, 5}
+
+ signer, err := kms.Get(context.Background(), "fakekms://key", crypto.SHA256)
+ if err != nil {
+ t.Fatalf("unexpected error getting signer: %v", err)
+ }
+ pub, err := signer.PublicKey()
+ if err != nil {
+ t.Fatalf("unexpected error getting public key")
+ }
+ createdPub, err := signer.CreateKey(context.Background(), "")
+ if err != nil {
+ t.Fatalf("unexpected error creating key: %v", err)
+ }
+ if err := cryptoutils.EqualKeys(createdPub, pub); err != nil {
+ t.Fatalf("expected public keys to be equal: %v", err)
+ }
+
+ if signer.DefaultAlgorithm() != signer.SupportedAlgorithms()[0] {
+ t.Fatal("expected algorithms to match")
+ }
+
+ // Test crypto.Signer implementation
+ cryptoSigner, _, err := signer.CryptoSigner(context.Background(), func(err error) {})
+ if err != nil {
+ t.Fatalf("unexpected error fetching crypto.Signer: %v", err)
+ }
+ if err := cryptoutils.EqualKeys(cryptoSigner.Public(), pub); err != nil {
+ t.Fatalf("expected public keys to be equal: %v", err)
+ }
+
+ sha := sha256.New()
+ sha.Write(msg)
+ digest := sha.Sum(nil)
+ sig, err := cryptoSigner.Sign(rand.Reader, digest, nil)
+ if err != nil {
+ t.Fatalf("unexpected error signing with crypto.Signer: %v", err)
+ }
+ if err := signer.VerifySignature(bytes.NewReader(sig), bytes.NewReader(msg)); err != nil {
+ t.Fatalf("unexpected error verifying signature: %v", err)
+ }
+}
+
+func TestFakeSignerWithPrivateKey(t *testing.T) {
+ msg := []byte{1, 2, 3, 4, 5}
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("error generating ecdsa private key: %v", err)
+ }
+
+ signer, err := kms.Get(context.WithValue(context.TODO(), KmsCtxKey{}, priv), "fakekms://key", crypto.SHA256)
+ if err != nil {
+ t.Fatalf("unexpected error getting signer: %v", err)
+ }
+ pub, err := signer.PublicKey()
+ if err != nil {
+ t.Fatalf("unexpected error getting public key")
+ }
+
+ // Compare public key to provided key
+ if err := cryptoutils.EqualKeys(priv.Public(), pub); err != nil {
+ t.Fatalf("expected public keys to be equal: %v", err)
+ }
+
+ createdPub, err := signer.CreateKey(context.Background(), "")
+ if err != nil {
+ t.Fatalf("unexpected error creating key: %v", err)
+ }
+ if err := cryptoutils.EqualKeys(createdPub, pub); err != nil {
+ t.Fatalf("expected public keys to be equal: %v", err)
+ }
+
+ if signer.DefaultAlgorithm() != signer.SupportedAlgorithms()[0] {
+ t.Fatal("expected algorithms to match")
+ }
+
+ // Test crypto.Signer implementation
+ cryptoSigner, _, err := signer.CryptoSigner(context.Background(), func(err error) {})
+ if err != nil {
+ t.Fatalf("unexpected error fetching crypto.Signer: %v", err)
+ }
+ if err := cryptoutils.EqualKeys(cryptoSigner.Public(), pub); err != nil {
+ t.Fatalf("expected public keys to be equal: %v", err)
+ }
+
+ sha := sha256.New()
+ sha.Write(msg)
+ digest := sha.Sum(nil)
+ sig, err := cryptoSigner.Sign(rand.Reader, digest, nil)
+ if err != nil {
+ t.Fatalf("unexpected error signing with crypto.Signer: %v", err)
+ }
+ if err := signer.VerifySignature(bytes.NewReader(sig), bytes.NewReader(msg)); err != nil {
+ t.Fatalf("unexpected error verifying signature: %v", err)
+ }
+}
diff --git a/pkg/signature/kms/gcp/client.go b/pkg/signature/kms/gcp/client.go
new file mode 100644
index 0000000..32bb79a
--- /dev/null
+++ b/pkg/signature/kms/gcp/client.go
@@ -0,0 +1,420 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package gcp implement the interface with google cloud kms service
+package gcp
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "log"
+ "regexp"
+ "time"
+
+ gcpkms "cloud.google.com/go/kms/apiv1"
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "google.golang.org/api/option"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+
+ "github.com/jellydator/ttlcache/v3"
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "github.com/sigstore/sigstore/pkg/signature"
+ sigkms "github.com/sigstore/sigstore/pkg/signature/kms"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+func init() {
+ sigkms.AddProvider(ReferenceScheme, func(ctx context.Context, keyResourceID string, _ crypto.Hash, opts ...signature.RPCOption) (sigkms.SignerVerifier, error) {
+ return LoadSignerVerifier(ctx, keyResourceID)
+ })
+}
+
+//nolint:revive
+const (
+ AlgorithmECDSAP256SHA256 = "ecdsa-p256-sha256"
+ AlgorithmECDSAP384SHA384 = "ecdsa-p384-sha384"
+ AlgorithmRSAPKCS1v152048SHA256 = "rsa-pkcs1v15-2048-sha256"
+ AlgorithmRSAPKCS1v153072SHA256 = "rsa-pkcs1v15-3072-sha256"
+ AlgorithmRSAPKCS1v154096SHA256 = "rsa-pkcs1v15-4096-sha256"
+ AlgorithmRSAPKCS1v154096SHA512 = "rsa-pkcs1v15-4096-sha512"
+ AlgorithmRSAPSS2048SHA256 = "rsa-pss-2048-sha256"
+ AlgorithmRSAPSS3072SHA256 = "rsa-pss-3072-sha256"
+ AlgorithmRSAPSS4096SHA256 = "rsa-pss-4096-sha256"
+ AlgorithmRSAPSS4096SHA512 = "rsa-pss-4096-sha512"
+)
+
+var algorithmMap = map[string]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
+ AlgorithmECDSAP256SHA256: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
+ AlgorithmECDSAP384SHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384,
+ AlgorithmRSAPKCS1v152048SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256,
+ AlgorithmRSAPKCS1v153072SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
+ AlgorithmRSAPKCS1v154096SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
+ AlgorithmRSAPKCS1v154096SHA512: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512,
+ AlgorithmRSAPSS2048SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256,
+ AlgorithmRSAPSS3072SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256,
+ AlgorithmRSAPSS4096SHA256: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256,
+ AlgorithmRSAPSS4096SHA512: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512,
+}
+
+type gcpClient struct {
+ defaultCtx context.Context
+ refString string
+ projectID string
+ locationID string
+ keyRing string
+ keyName string
+ version string
+ kvCache *ttlcache.Cache[string, cryptoKeyVersion]
+ kmsClient *gcpkms.KeyManagementClient
+}
+
+func newGCPClient(ctx context.Context, refStr string, opts ...option.ClientOption) (*gcpClient, error) {
+ if err := ValidReference(refStr); err != nil {
+ return nil, err
+ }
+
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
+ g := &gcpClient{
+ defaultCtx: ctx,
+ refString: refStr,
+ kvCache: nil,
+ }
+ var err error
+ g.projectID, g.locationID, g.keyRing, g.keyName, g.version, err = parseReference(refStr)
+ if err != nil {
+ return nil, err
+ }
+
+ g.kmsClient, err = gcpkms.NewKeyManagementClient(ctx, opts...)
+ if err != nil {
+ return nil, fmt.Errorf("new gcp kms client: %w", err)
+ }
+
+ g.kvCache = ttlcache.New[string, cryptoKeyVersion](
+ ttlcache.WithDisableTouchOnHit[string, cryptoKeyVersion](),
+ )
+
+ // prime the cache
+ g.kvCache.Get(cacheKey)
+ return g, nil
+}
+
+var (
+ errKMSReference = errors.New("kms specification should be in the format gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]/cryptoKeyVersions/[VERSION]")
+
+ re = regexp.MustCompile(`^gcpkms://projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)/cryptoKeys/([^/]+)(?:/(?:cryptoKeyVersions|versions)/([^/]+))?$`)
+)
+
+// ReferenceScheme schemes for various KMS services are copied from https://github.com/google/go-cloud/tree/master/secrets
+const ReferenceScheme = "gcpkms://"
+
+// ValidReference returns a non-nil error if the reference string is invalid
+func ValidReference(ref string) error {
+ if !re.MatchString(ref) {
+ return errKMSReference
+ }
+ return nil
+}
+
+func parseReference(resourceID string) (projectID, locationID, keyRing, keyName, version string, err error) {
+ v := re.FindStringSubmatch(resourceID)
+ if len(v) != 6 {
+ err = fmt.Errorf("invalid gcpkms format %q", resourceID)
+ return
+ }
+ projectID, locationID, keyRing, keyName, version = v[1], v[2], v[3], v[4], v[5]
+ return
+}
+
+type cryptoKeyVersion struct {
+ CryptoKeyVersion *kmspb.CryptoKeyVersion
+ Verifier signature.Verifier
+ HashFunc crypto.Hash
+}
+
+// use a consistent key for cache lookups
+const cacheKey = "crypto_key_version"
+
+// keyVersionName returns the first key version found for a key in KMS
+func (g *gcpClient) keyVersionName(ctx context.Context) (*cryptoKeyVersion, error) {
+ parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", g.projectID, g.locationID, g.keyRing, g.keyName)
+
+ parentReq := &kmspb.GetCryptoKeyRequest{
+ Name: parent,
+ }
+ key, err := g.kmsClient.GetCryptoKey(ctx, parentReq)
+ if err != nil {
+ return nil, err
+ }
+ if key.Purpose != kmspb.CryptoKey_ASYMMETRIC_SIGN {
+ return nil, errors.New("specified key cannot be used to sign")
+ }
+
+ // if g.version was specified, use it explicitly
+ var kv *kmspb.CryptoKeyVersion
+ if g.version != "" {
+ req := &kmspb.GetCryptoKeyVersionRequest{
+ Name: parent + fmt.Sprintf("/cryptoKeyVersions/%s", g.version),
+ }
+ kv, err = g.kmsClient.GetCryptoKeyVersion(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ req := &kmspb.ListCryptoKeyVersionsRequest{
+ Parent: parent,
+ Filter: "state=ENABLED",
+ OrderBy: "name desc",
+ }
+ iterator := g.kmsClient.ListCryptoKeyVersions(ctx, req)
+
+ // pick the key version that is enabled with the greatest version value
+ kv, err = iterator.Next()
+ if err != nil {
+ return nil, fmt.Errorf("unable to find an enabled key version in GCP KMS: %w", err)
+ }
+ }
+ // kv is keyVersion to use
+ crv := cryptoKeyVersion{
+ CryptoKeyVersion: kv,
+ }
+
+ pubKey, err := g.fetchPublicKey(ctx, kv.Name)
+ if err != nil {
+ return nil, fmt.Errorf("unable to fetch public key while creating signer: %w", err)
+ }
+
+ // crv.Verifier is set here to enable storing the public key & hash algorithm together,
+ // as well as using the in memory Verifier to perform the verify operations.
+ switch kv.Algorithm {
+ case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256:
+ crv.Verifier, err = signature.LoadECDSAVerifier(pubKey.(*ecdsa.PublicKey), crypto.SHA256)
+ crv.HashFunc = crypto.SHA256
+ case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384:
+ crv.Verifier, err = signature.LoadECDSAVerifier(pubKey.(*ecdsa.PublicKey), crypto.SHA384)
+ crv.HashFunc = crypto.SHA384
+ case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256,
+ kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
+ kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256:
+ crv.Verifier, err = signature.LoadRSAPKCS1v15Verifier(pubKey.(*rsa.PublicKey), crypto.SHA256)
+ crv.HashFunc = crypto.SHA256
+ case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512:
+ crv.Verifier, err = signature.LoadRSAPKCS1v15Verifier(pubKey.(*rsa.PublicKey), crypto.SHA512)
+ crv.HashFunc = crypto.SHA512
+ case kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256,
+ kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256,
+ kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256:
+ crv.Verifier, err = signature.LoadRSAPSSVerifier(pubKey.(*rsa.PublicKey), crypto.SHA256, nil)
+ crv.HashFunc = crypto.SHA256
+ case kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512:
+ crv.Verifier, err = signature.LoadRSAPSSVerifier(pubKey.(*rsa.PublicKey), crypto.SHA512, nil)
+ crv.HashFunc = crypto.SHA512
+ default:
+ return nil, errors.New("unknown algorithm specified by KMS")
+ }
+ if err != nil {
+ return nil, fmt.Errorf("initializing internal verifier: %w", err)
+ }
+ return &crv, nil
+}
+
+func (g *gcpClient) fetchPublicKey(ctx context.Context, name string) (crypto.PublicKey, error) {
+ // Build the request.
+ pkreq := &kmspb.GetPublicKeyRequest{Name: name}
+ // Call the API.
+ pk, err := g.kmsClient.GetPublicKey(ctx, pkreq)
+ if err != nil {
+ return nil, fmt.Errorf("public key: %w", err)
+ }
+ return cryptoutils.UnmarshalPEMToPublicKey([]byte(pk.GetPem()))
+}
+
+func (g *gcpClient) getHashFunc() (crypto.Hash, error) {
+ ckv, err := g.getCKV()
+ if err != nil {
+ return 0, err
+ }
+ return ckv.HashFunc, nil
+}
+
+// getCKV gets the latest CryptoKeyVersion from the client's cache, which may trigger an actual
+// call to GCP if the existing entry in the cache has expired.
+func (g *gcpClient) getCKV() (*cryptoKeyVersion, error) {
+ var lerr error
+ loader := ttlcache.LoaderFunc[string, cryptoKeyVersion](
+ func(c *ttlcache.Cache[string, cryptoKeyVersion], key string) *ttlcache.Item[string, cryptoKeyVersion] {
+ var ttl time.Duration
+ var data *cryptoKeyVersion
+
+ // if we're given an explicit version, cache this value forever
+ if g.version != "" {
+ ttl = time.Second * 0
+ } else {
+ ttl = time.Second * 300
+ }
+ data, lerr = g.keyVersionName(context.Background())
+ if lerr == nil {
+ return c.Set(key, *data, ttl)
+ }
+ return nil
+ },
+ )
+
+ // we get once and use consistently to ensure the cache value doesn't change underneath us
+ item := g.kvCache.Get(cacheKey, ttlcache.WithLoader[string, cryptoKeyVersion](loader))
+ if item != nil {
+ v := item.Value()
+ return &v, nil
+ }
+ return nil, lerr
+}
+
+func (g *gcpClient) sign(ctx context.Context, digest []byte, alg crypto.Hash, crc uint32) ([]byte, error) {
+ ckv, err := g.getCKV()
+ if err != nil {
+ return nil, err
+ }
+
+ gcpSignReq := kmspb.AsymmetricSignRequest{
+ Name: ckv.CryptoKeyVersion.Name,
+ Digest: &kmspb.Digest{},
+ }
+
+ if crc != 0 {
+ gcpSignReq.DigestCrc32C = wrapperspb.Int64(int64(crc))
+ }
+
+ switch alg {
+ case crypto.SHA256:
+ gcpSignReq.Digest.Digest = &kmspb.Digest_Sha256{
+ Sha256: digest,
+ }
+ case crypto.SHA384:
+ gcpSignReq.Digest.Digest = &kmspb.Digest_Sha384{
+ Sha384: digest,
+ }
+ case crypto.SHA512:
+ gcpSignReq.Digest.Digest = &kmspb.Digest_Sha512{
+ Sha512: digest,
+ }
+ default:
+ return nil, errors.New("unsupported hash function")
+ }
+
+ resp, err := g.kmsClient.AsymmetricSign(ctx, &gcpSignReq)
+ if err != nil {
+ return nil, fmt.Errorf("calling GCP AsymmetricSign: %w", err)
+ }
+
+ // Optional, but recommended: perform integrity verification on result.
+ // For more details on ensuring E2E in-transit integrity to and from Cloud KMS visit:
+ // https://cloud.google.com/kms/docs/data-integrity-guidelines
+ if crc != 0 && !resp.VerifiedDigestCrc32C {
+ return nil, fmt.Errorf("AsymmetricSign: request corrupted in-transit")
+ }
+ if int64(crc32.Checksum(resp.Signature, crc32.MakeTable(crc32.Castagnoli))) != resp.SignatureCrc32C.Value {
+ return nil, fmt.Errorf("AsymmetricSign: response corrupted in-transit")
+ }
+
+ return resp.Signature, nil
+}
+
+func (g *gcpClient) public(ctx context.Context) (crypto.PublicKey, error) {
+ crv, err := g.getCKV()
+ if err != nil {
+ return nil, fmt.Errorf("transient error getting info from KMS: %w", err)
+ }
+ return crv.Verifier.PublicKey(options.WithContext(ctx))
+}
+
+func (g *gcpClient) verify(sig, message io.Reader, opts ...signature.VerifyOption) error {
+ crv, err := g.getCKV()
+ if err != nil {
+ return fmt.Errorf("transient error getting info from KMS: %w", err)
+ }
+ if err := crv.Verifier.VerifySignature(sig, message, opts...); err != nil {
+ // key could have been rotated, clear cache and try again if we're not pinned to a version
+ if g.version == "" {
+ g.kvCache.Delete(cacheKey)
+ crv, err = g.getCKV()
+ if err != nil {
+ return fmt.Errorf("transient error getting info from KMS: %w", err)
+ }
+ return crv.Verifier.VerifySignature(sig, message, opts...)
+ }
+ return fmt.Errorf("failed to verify for fixed version: %w", err)
+ }
+ return nil
+}
+
+func (g *gcpClient) createKey(ctx context.Context, algorithm string) (crypto.PublicKey, error) {
+ if err := g.createKeyRing(ctx); err != nil {
+ return nil, fmt.Errorf("creating key ring: %w", err)
+ }
+
+ getKeyRequest := &kmspb.GetCryptoKeyRequest{
+ Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", g.projectID, g.locationID, g.keyRing, g.keyName),
+ }
+ if _, err := g.kmsClient.GetCryptoKey(ctx, getKeyRequest); err == nil {
+ return g.public(ctx)
+ }
+
+ if _, ok := algorithmMap[algorithm]; !ok {
+ return nil, errors.New("unknown algorithm requested")
+ }
+
+ createKeyRequest := &kmspb.CreateCryptoKeyRequest{
+ Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", g.projectID, g.locationID, g.keyRing),
+ CryptoKeyId: g.keyName,
+ CryptoKey: &kmspb.CryptoKey{
+ Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN,
+ VersionTemplate: &kmspb.CryptoKeyVersionTemplate{
+ Algorithm: algorithmMap[algorithm],
+ },
+ },
+ }
+ if _, err := g.kmsClient.CreateCryptoKey(ctx, createKeyRequest); err != nil {
+ return nil, fmt.Errorf("creating crypto key: %w", err)
+ }
+ return g.public(ctx)
+}
+
+func (g *gcpClient) createKeyRing(ctx context.Context) error {
+ getKeyRingRequest := &kmspb.GetKeyRingRequest{
+ Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", g.projectID, g.locationID, g.keyRing),
+ }
+ if result, err := g.kmsClient.GetKeyRing(ctx, getKeyRingRequest); err == nil {
+ log.Printf("Key ring %s already exists in GCP KMS, moving on to creating key.\n", result.GetName())
+ // key ring already exists, no need to create
+ return nil
+ }
+ // try to create key ring
+ createKeyRingRequest := &kmspb.CreateKeyRingRequest{
+ Parent: fmt.Sprintf("projects/%s/locations/%s", g.projectID, g.locationID),
+ KeyRingId: g.keyRing,
+ }
+ result, err := g.kmsClient.CreateKeyRing(ctx, createKeyRingRequest)
+ log.Printf("Created key ring %s in GCP KMS.\n", result.GetName())
+ return err
+}
diff --git a/pkg/signature/kms/gcp/doc.go b/pkg/signature/kms/gcp/doc.go
new file mode 100644
index 0000000..3ecf441
--- /dev/null
+++ b/pkg/signature/kms/gcp/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package gcp contains utilities related to Google Cloud Platform KMS.
+package gcp
diff --git a/pkg/signature/kms/gcp/gcp_test.go b/pkg/signature/kms/gcp/gcp_test.go
new file mode 100644
index 0000000..55a4cef
--- /dev/null
+++ b/pkg/signature/kms/gcp/gcp_test.go
@@ -0,0 +1,110 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcp
+
+import (
+ "context"
+ "testing"
+
+ "golang.org/x/oauth2"
+ "google.golang.org/api/option"
+)
+
+func TestParseReference(t *testing.T) {
+ tests := []struct {
+ in string
+ wantProjectID string
+ wantLocationID string
+ wantKeyRing string
+ wantKeyName string
+ wantKeyVersion string
+ wantErr bool
+ }{
+ {
+ in: "gcpkms://projects/pp/locations/ll/keyRings/rr/cryptoKeys/kk",
+ wantProjectID: "pp",
+ wantLocationID: "ll",
+ wantKeyRing: "rr",
+ wantKeyName: "kk",
+ wantErr: false,
+ },
+ {
+ in: "gcpkms://projects/pp/locations/ll/keyRings/rr/cryptoKeys/kk/versions/1",
+ wantProjectID: "pp",
+ wantLocationID: "ll",
+ wantKeyRing: "rr",
+ wantKeyName: "kk",
+ wantKeyVersion: "1",
+ wantErr: false,
+ },
+ {
+ in: "gcpkms://projects/pp/locations/ll/keyRings/rr/cryptoKeys/kk/cryptoKeyVersions/1",
+ wantProjectID: "pp",
+ wantLocationID: "ll",
+ wantKeyRing: "rr",
+ wantKeyName: "kk",
+ wantKeyVersion: "1",
+ wantErr: false,
+ },
+ {
+ in: "gcpkms://projects/p1/p2/locations/l1/l2/keyRings/r1/r2/cryptoKeys/k1/k2",
+ wantErr: true,
+ },
+ {
+ in: "foo://bar",
+ wantErr: true,
+ },
+ {
+ in: "",
+ wantErr: true,
+ },
+ {
+ in: "gcpkms://projects/p1/p2/locations/l1/l2/keyRings/r1/r2/cryptoKeys/k1/versions",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ gotProjectID, gotLocationID, gotKeyRing, gotKeyName, gotKeyVersion, err := parseReference(tt.in)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("parseReference() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotProjectID != tt.wantProjectID {
+ t.Errorf("parseReference() gotProjectID = %v, want %v", gotProjectID, tt.wantProjectID)
+ }
+ if gotLocationID != tt.wantLocationID {
+ t.Errorf("parseReference() gotLocationID = %v, want %v", gotLocationID, tt.wantLocationID)
+ }
+ if gotKeyRing != tt.wantKeyRing {
+ t.Errorf("parseReference() gotKeyRing = %v, want %v", gotKeyRing, tt.wantKeyRing)
+ }
+ if gotKeyName != tt.wantKeyName {
+ t.Errorf("parseReference() gotKeyName = %v, want %v", gotKeyName, tt.wantKeyName)
+ }
+ if gotKeyVersion != tt.wantKeyVersion {
+ t.Errorf("parseReference() gotKeyVersion = %v, want %v", gotKeyVersion, tt.wantKeyVersion)
+ }
+ })
+ }
+}
+
+func TestOptionsWork(_ *testing.T) {
+ // Check that we can pass options into LoadSignerVerifier
+ // (this is mostly a compile-time check)
+ ts := oauth2.StaticTokenSource(&oauth2.Token{})
+ LoadSignerVerifier(context.Background(), "gcpkms://projects/a-project/locations/global/keyRings/a-keyring/cryptoKeys/key-name", option.WithTokenSource(ts))
+}
diff --git a/pkg/signature/kms/gcp/go.mod b/pkg/signature/kms/gcp/go.mod
new file mode 100644
index 0000000..4b1b2f2
--- /dev/null
+++ b/pkg/signature/kms/gcp/go.mod
@@ -0,0 +1,54 @@
+module github.com/sigstore/sigstore/pkg/signature/kms/gcp
+
+replace github.com/sigstore/sigstore => ../../../../
+
+go 1.20
+
+require (
+ cloud.google.com/go/kms v1.15.5
+ github.com/jellydator/ttlcache/v3 v3.1.1
+ github.com/sigstore/sigstore v1.6.4
+ golang.org/x/oauth2 v0.15.0
+ google.golang.org/api v0.154.0
+ google.golang.org/protobuf v1.31.0
+)
+
+require (
+ cloud.google.com/go/compute v1.23.3 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/iam v1.1.5 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-containerregistry v0.17.0 // indirect
+ github.com/google/s2a-go v0.1.7 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+ github.com/kr/pretty v0.2.1 // indirect
+ github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
+ go.opentelemetry.io/otel v1.21.0 // indirect
+ go.opentelemetry.io/otel/metric v1.21.0 // indirect
+ go.opentelemetry.io/otel/trace v1.21.0 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sync v0.5.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
+ google.golang.org/grpc v1.59.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/pkg/signature/kms/gcp/go.sum b/pkg/signature/kms/gcp/go.sum
new file mode 100644
index 0000000..0fceb6c
--- /dev/null
+++ b/pkg/signature/kms/gcp/go.sum
@@ -0,0 +1,218 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
+cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM=
+cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
+github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8=
+github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
+github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
+google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
+google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
+google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
+google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
+gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/signature/kms/gcp/signer.go b/pkg/signature/kms/gcp/signer.go
new file mode 100644
index 0000000..445f9d9
--- /dev/null
+++ b/pkg/signature/kms/gcp/signer.go
@@ -0,0 +1,195 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcp
+
+import (
+ "context"
+ "crypto"
+ "fmt"
+ "hash/crc32"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+ "google.golang.org/api/option"
+)
+
+var gcpSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA512,
+ crypto.SHA384,
+}
+
+// SignerVerifier creates and verifies digital signatures over a message using GCP KMS service
+type SignerVerifier struct {
+ defaultCtx context.Context
+ client *gcpClient
+}
+
+// LoadSignerVerifier generates signatures using the specified key object in GCP KMS and hash algorithm.
+//
+// It also can verify signatures locally using the public key. hashFunc must not be crypto.Hash(0).
+func LoadSignerVerifier(defaultCtx context.Context, referenceStr string, opts ...option.ClientOption) (*SignerVerifier, error) {
+ g := &SignerVerifier{
+ defaultCtx: defaultCtx,
+ }
+
+ var err error
+ g.client, err = newGCPClient(defaultCtx, referenceStr, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return g, nil
+}
+
+// SignMessage signs the provided message using GCP KMS. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the Signer was created.
+//
+// SignMessage recognizes the following Options listed in order of preference:
+//
+// - WithContext()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (g *SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ ctx := context.Background()
+ var digest []byte
+ var signerOpts crypto.SignerOpts
+ var err error
+
+ signerOpts, err = g.client.getHashFunc()
+ if err != nil {
+ return nil, fmt.Errorf("getting fetching default hash function: %w", err)
+ }
+
+ for _, opt := range opts {
+ opt.ApplyContext(&ctx)
+ opt.ApplyDigest(&digest)
+ opt.ApplyCryptoSignerOpts(&signerOpts)
+ }
+
+ digest, hf, err := signature.ComputeDigestForSigning(message, signerOpts.HashFunc(), gcpSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ crc32cHasher := crc32.New(crc32.MakeTable(crc32.Castagnoli))
+ _, err = crc32cHasher.Write(digest)
+ if err != nil {
+ return nil, err
+ }
+
+ return g.client.sign(ctx, digest, hf, crc32cHasher.Sum32())
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. If the caller wishes to specify the context to use to obtain
+// the public key, pass option.WithContext(desiredCtx).
+//
+// All other options are ignored if specified.
+func (g *SignerVerifier) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ ctx := context.Background()
+ for _, opt := range opts {
+ opt.ApplyContext(&ctx)
+ }
+
+ return g.client.public(ctx)
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the SignerVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// All other options are ignored if specified.
+func (g *SignerVerifier) VerifySignature(signature, message io.Reader, opts ...signature.VerifyOption) error {
+ return g.client.verify(signature, message, opts...)
+}
+
+// CreateKey attempts to create a new key in Vault with the specified algorithm.
+func (g *SignerVerifier) CreateKey(ctx context.Context, algorithm string) (crypto.PublicKey, error) {
+ return g.client.createKey(ctx, algorithm)
+}
+
+type cryptoSignerWrapper struct {
+ ctx context.Context
+ hashFunc crypto.Hash
+ sv *SignerVerifier
+ errFunc func(error)
+}
+
+func (c cryptoSignerWrapper) Public() crypto.PublicKey {
+ pk, err := c.sv.PublicKey(options.WithContext(c.ctx))
+ if err != nil && c.errFunc != nil {
+ c.errFunc(err)
+ }
+ return pk
+}
+
+func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ hashFunc := c.hashFunc
+ if opts != nil {
+ hashFunc = opts.HashFunc()
+ }
+ gcpOptions := []signature.SignOption{
+ options.WithContext(c.ctx),
+ options.WithDigest(digest),
+ options.WithCryptoSignerOpts(hashFunc),
+ }
+
+ return c.sv.SignMessage(nil, gcpOptions...)
+}
+
+// CryptoSigner returns a crypto.Signer object that uses the underlying SignerVerifier, along with a crypto.SignerOpts object
+// that allows the KMS to be used in APIs that only accept the standard golang objects
+func (g *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) {
+ defaultHf, err := g.client.getHashFunc()
+ if err != nil {
+ return nil, nil, fmt.Errorf("getting fetching default hash function: %w", err)
+ }
+
+ csw := &cryptoSignerWrapper{
+ ctx: ctx,
+ sv: g,
+ hashFunc: defaultHf,
+ errFunc: errFunc,
+ }
+
+ return csw, defaultHf, nil
+}
+
+// SupportedAlgorithms returns the list of algorithms supported by the GCP KMS service
+func (g *SignerVerifier) SupportedAlgorithms() (result []string) {
+ for k := range algorithmMap {
+ result = append(result, k)
+ }
+ return
+}
+
+// DefaultAlgorithm returns the default algorithm for the GCP KMS service
+func (g *SignerVerifier) DefaultAlgorithm() string {
+ return AlgorithmECDSAP256SHA256
+}
diff --git a/pkg/signature/kms/hashivault/client.go b/pkg/signature/kms/hashivault/client.go
new file mode 100644
index 0000000..1f56f80
--- /dev/null
+++ b/pkg/signature/kms/hashivault/client.go
@@ -0,0 +1,391 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hashivault implement the interface with hashivault kms service
+package hashivault
+
+import (
+ "context"
+ "crypto"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "time"
+
+ vault "github.com/hashicorp/vault/api"
+ "github.com/jellydator/ttlcache/v3"
+ "github.com/mitchellh/go-homedir"
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "github.com/sigstore/sigstore/pkg/signature"
+ sigkms "github.com/sigstore/sigstore/pkg/signature/kms"
+)
+
+func init() {
+ sigkms.AddProvider(ReferenceScheme, func(_ context.Context, keyResourceID string, hashFunc crypto.Hash, opts ...signature.RPCOption) (sigkms.SignerVerifier, error) {
+ return LoadSignerVerifier(keyResourceID, hashFunc, opts...)
+ })
+}
+
+type hashivaultClient struct {
+ client *vault.Client
+ keyPath string
+ transitSecretEnginePath string
+ keyCache *ttlcache.Cache[string, crypto.PublicKey]
+ keyVersion uint64
+}
+
+var (
+ errReference = errors.New("kms specification should be in the format hashivault://<key>")
+ referenceRegex = regexp.MustCompile(`^hashivault://(?P<path>\w(([\w-.]+)?\w)?)$`)
+ prefixRegex = regexp.MustCompile("^vault:v[0-9]+:")
+)
+
+const (
+ vaultV1DataPrefix = "vault:v1:"
+
+ // use a consistent key for cache lookups
+ cacheKey = "signer"
+
+ // ReferenceScheme schemes for various KMS services are copied from https://github.com/google/go-cloud/tree/master/secrets
+ ReferenceScheme = "hashivault://"
+)
+
+// ValidReference returns a non-nil error if the reference string is invalid
+func ValidReference(ref string) error {
+ if !referenceRegex.MatchString(ref) {
+ return errReference
+ }
+ return nil
+}
+
+func parseReference(resourceID string) (keyPath string, err error) {
+ i := referenceRegex.SubexpIndex("path")
+ v := referenceRegex.FindStringSubmatch(resourceID)
+ if len(v) < i+1 {
+ err = fmt.Errorf("invalid vault format %q: %w", resourceID, err)
+ return
+ }
+ keyPath = v[i]
+ return
+}
+
+func newHashivaultClient(address, token, transitSecretEnginePath, keyResourceID string, keyVersion uint64) (*hashivaultClient, error) {
+ if err := ValidReference(keyResourceID); err != nil {
+ return nil, err
+ }
+
+ keyPath, err := parseReference(keyResourceID)
+ if err != nil {
+ return nil, err
+ }
+
+ if address == "" {
+ address = os.Getenv("VAULT_ADDR")
+ }
+ if address == "" {
+ return nil, errors.New("VAULT_ADDR is not set")
+ }
+
+ client, err := vault.NewClient(&vault.Config{
+ Address: address,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("new vault client: %w", err)
+ }
+
+ if token == "" {
+ token = os.Getenv("VAULT_TOKEN")
+ }
+ if token == "" {
+ log.Printf("VAULT_TOKEN is not set, trying to read token from file at path ~/.vault-token")
+ homeDir, err := homedir.Dir()
+ if err != nil {
+ return nil, fmt.Errorf("get home directory: %w", err)
+ }
+
+ tokenFromFile, err := os.ReadFile(filepath.Join(homeDir, ".vault-token"))
+ if err != nil {
+ return nil, fmt.Errorf("read .vault-token file: %w", err)
+ }
+
+ token = string(tokenFromFile)
+ }
+ client.SetToken(token)
+
+ if transitSecretEnginePath == "" {
+ transitSecretEnginePath = os.Getenv("TRANSIT_SECRET_ENGINE_PATH")
+ }
+ if transitSecretEnginePath == "" {
+ transitSecretEnginePath = "transit"
+ }
+
+ hvClient := &hashivaultClient{
+ client: client,
+ keyPath: keyPath,
+ transitSecretEnginePath: transitSecretEnginePath,
+ keyCache: ttlcache.New[string, crypto.PublicKey](
+ ttlcache.WithDisableTouchOnHit[string, crypto.PublicKey](),
+ ),
+ keyVersion: keyVersion,
+ }
+
+ return hvClient, nil
+}
+
+func oidcLogin(_ context.Context, address, path, role, token string) (string, error) {
+ if address == "" {
+ address = os.Getenv("VAULT_ADDR")
+ }
+ if address == "" {
+ return "", errors.New("VAULT_ADDR is not set")
+ }
+ if path == "" {
+ path = "jwt"
+ }
+
+ client, err := vault.NewClient(&vault.Config{
+ Address: address,
+ })
+ if err != nil {
+ return "", fmt.Errorf("new vault client: %w", err)
+ }
+
+ loginData := map[string]interface{}{
+ "role": role,
+ "jwt": token,
+ }
+ fullpath := fmt.Sprintf("auth/%s/login", path)
+ resp, err := client.Logical().Write(fullpath, loginData)
+ if err != nil {
+ return "", fmt.Errorf("vault oidc login: %w", err)
+ }
+ return resp.TokenID()
+}
+
+func (h *hashivaultClient) fetchPublicKey(_ context.Context) (crypto.PublicKey, error) {
+ client := h.client.Logical()
+
+ path := fmt.Sprintf("/%s/keys/%s", h.transitSecretEnginePath, h.keyPath)
+
+ keyResult, err := client.Read(path)
+ if err != nil {
+ return nil, fmt.Errorf("public key: %w", err)
+ }
+
+ if keyResult == nil {
+ return nil, fmt.Errorf("could not read data from transit key path: %s", path)
+ }
+
+ keysData, hasKeys := keyResult.Data["keys"]
+ latestVersion, hasVersion := keyResult.Data["latest_version"]
+ if !hasKeys || !hasVersion {
+ return nil, errors.New("failed to read transit key keys: corrupted response")
+ }
+
+ keys, ok := keysData.(map[string]interface{})
+ if !ok {
+ return nil, errors.New("failed to read transit key keys: Invalid keys map")
+ }
+
+ keyVersion, ok := latestVersion.(json.Number)
+ if !ok {
+ return nil, fmt.Errorf("format of 'latest_version' is not json.Number")
+ }
+
+ keyData, ok := keys[string(keyVersion)]
+ if !ok {
+ return nil, errors.New("failed to read transit key keys: corrupted response")
+ }
+
+ keyMap, ok := keyData.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("could not parse transit key keys data as map[string]interface{}")
+ }
+
+ publicKeyPem, ok := keyMap["public_key"]
+ if !ok {
+ return nil, errors.New("failed to read transit key keys: corrupted response")
+ }
+
+ strPublicKeyPem, ok := publicKeyPem.(string)
+ if !ok {
+ return nil, fmt.Errorf("could not parse public key pem as string")
+ }
+
+ return cryptoutils.UnmarshalPEMToPublicKey([]byte(strPublicKeyPem))
+}
+
+func (h *hashivaultClient) public() (crypto.PublicKey, error) {
+ var lerr error
+ loader := ttlcache.LoaderFunc[string, crypto.PublicKey](
+ func(c *ttlcache.Cache[string, crypto.PublicKey], key string) *ttlcache.Item[string, crypto.PublicKey] {
+ var pubkey crypto.PublicKey
+ pubkey, lerr = h.fetchPublicKey(context.Background())
+ if lerr == nil {
+ item := c.Set(key, pubkey, 300*time.Second)
+ return item
+ }
+ return nil
+ },
+ )
+
+ item := h.keyCache.Get(cacheKey, ttlcache.WithLoader[string, crypto.PublicKey](loader))
+ if lerr != nil {
+ return nil, lerr
+ }
+
+ if item == nil {
+ return nil, fmt.Errorf("unable to retrieve an item from the cache by the provided key")
+ }
+
+ return item.Value(), nil
+}
+
+func (h hashivaultClient) sign(digest []byte, alg crypto.Hash, opts ...signature.SignOption) ([]byte, error) {
+ client := h.client.Logical()
+
+ keyVersion := fmt.Sprintf("%d", h.keyVersion)
+ var keyVersionUsedPtr *string
+ for _, opt := range opts {
+ opt.ApplyKeyVersion(&keyVersion)
+ opt.ApplyKeyVersionUsed(&keyVersionUsedPtr)
+ }
+
+ if keyVersion != "" {
+ if _, err := strconv.ParseUint(keyVersion, 10, 64); err != nil {
+ return nil, fmt.Errorf("parsing requested key version: %w", err)
+ }
+ }
+
+ signResult, err := client.Write(fmt.Sprintf("/%s/sign/%s%s", h.transitSecretEnginePath, h.keyPath, hashString(alg)), map[string]interface{}{
+ "input": base64.StdEncoding.Strict().EncodeToString(digest),
+ "prehashed": alg != crypto.Hash(0),
+ "key_version": keyVersion,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("transit: failed to sign payload: %w", err)
+ }
+
+ encodedSignature, ok := signResult.Data["signature"]
+ if !ok {
+ return nil, errors.New("transit: response corrupted in-transit")
+ }
+
+ return vaultDecode(encodedSignature, keyVersionUsedPtr)
+}
+
+func (h hashivaultClient) verify(sig, digest []byte, alg crypto.Hash, opts ...signature.VerifyOption) error {
+ client := h.client.Logical()
+ encodedSig := base64.StdEncoding.EncodeToString(sig)
+
+ keyVersion := ""
+ for _, opt := range opts {
+ opt.ApplyKeyVersion(&keyVersion)
+ }
+
+ var vaultDataPrefix string
+ if keyVersion != "" {
+ // keyVersion >= 1 on verification but can be set to 0 on signing
+ kvUint, err := strconv.ParseUint(keyVersion, 10, 64)
+ if err != nil {
+ return fmt.Errorf("parsing requested key version: %w", err)
+ } else if kvUint == 0 {
+ return errors.New("key version must be >= 1")
+ }
+
+ vaultDataPrefix = fmt.Sprintf("vault:v%d:", kvUint)
+ } else {
+ vaultDataPrefix = os.Getenv("VAULT_KEY_PREFIX")
+ if vaultDataPrefix == "" {
+ if h.keyVersion > 0 {
+ vaultDataPrefix = fmt.Sprintf("vault:v%d:", h.keyVersion)
+ } else {
+ vaultDataPrefix = vaultV1DataPrefix
+ }
+ }
+ }
+
+ result, err := client.Write(fmt.Sprintf("/%s/verify/%s/%s", h.transitSecretEnginePath, h.keyPath, hashString(alg)), map[string]interface{}{
+ "input": base64.StdEncoding.EncodeToString(digest),
+ "prehashed": alg != crypto.Hash(0),
+ "signature": fmt.Sprintf("%s%s", vaultDataPrefix, encodedSig),
+ })
+ if err != nil {
+ return fmt.Errorf("verify: %w", err)
+ }
+
+ valid, ok := result.Data["valid"]
+ if !ok {
+ return errors.New("corrupted response")
+ }
+
+ isValid, ok := valid.(bool)
+ if !ok {
+ return fmt.Errorf("received non-bool value from 'valid' key")
+ }
+
+ if !isValid {
+ return errors.New("failed vault verification")
+ }
+
+ return nil
+}
+
+// Vault likes to prefix base64 data with a version prefix
+func vaultDecode(data interface{}, keyVersionUsed *string) ([]byte, error) {
+ encoded, ok := data.(string)
+ if !ok {
+ return nil, errors.New("received non-string data")
+ }
+
+ if keyVersionUsed != nil {
+ *keyVersionUsed = prefixRegex.FindString(encoded)
+ }
+ return base64.StdEncoding.DecodeString(prefixRegex.ReplaceAllString(encoded, ""))
+}
+
+func hashString(h crypto.Hash) string {
+ var hashStr string
+ switch h {
+ case crypto.SHA224:
+ hashStr = "/sha2-224"
+ case crypto.SHA256:
+ hashStr = "/sha2-256"
+ case crypto.SHA384:
+ hashStr = "/sha2-384"
+ case crypto.SHA512:
+ hashStr = "/sha2-512"
+ default:
+ hashStr = ""
+ }
+ return hashStr
+}
+
+func (h hashivaultClient) createKey(typeStr string) (crypto.PublicKey, error) {
+ client := h.client.Logical()
+
+ if _, err := client.Write(fmt.Sprintf("/%s/keys/%s", h.transitSecretEnginePath, h.keyPath), map[string]interface{}{
+ "type": typeStr,
+ }); err != nil {
+ return nil, fmt.Errorf("failed to create transit key: %w", err)
+ }
+ return h.public()
+}
diff --git a/pkg/signature/kms/hashivault/doc.go b/pkg/signature/kms/hashivault/doc.go
new file mode 100644
index 0000000..27520ba
--- /dev/null
+++ b/pkg/signature/kms/hashivault/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hashivault contains utilities related to Hashivault KMS.
+package hashivault
diff --git a/pkg/signature/kms/hashivault/e2e_test.go b/pkg/signature/kms/hashivault/e2e_test.go
new file mode 100644
index 0000000..6471601
--- /dev/null
+++ b/pkg/signature/kms/hashivault/e2e_test.go
@@ -0,0 +1,487 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build e2e
+// +build e2e
+
+package hashivault
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rand"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+
+ vault "github.com/hashicorp/vault/api"
+)
+
+type VaultSuite struct {
+ suite.Suite
+ vaultclient *vault.Client
+}
+
+func (suite *VaultSuite) GetProvider(key string, opts ...signature.RPCOption) *SignerVerifier {
+ provider, err := LoadSignerVerifier(fmt.Sprintf("hashivault://%s", key), crypto.SHA256, opts...)
+ require.NoError(suite.T(), err)
+ require.NotNil(suite.T(), provider)
+ return provider
+}
+
+func (suite *VaultSuite) SetupSuite() {
+ var err error
+ suite.vaultclient, err = vault.NewClient(&vault.Config{
+ Address: os.Getenv("VAULT_ADDR"),
+ })
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), suite.vaultclient)
+
+ err = suite.vaultclient.Sys().Mount("transit", &vault.MountInput{
+ Type: "transit",
+ })
+ require.Nil(suite.T(), err)
+
+ err = suite.vaultclient.Sys().Mount("somerandompath", &vault.MountInput{
+ Type: "transit",
+ })
+ require.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TearDownSuite() {
+ var err error
+ if suite.vaultclient == nil {
+ suite.vaultclient, err = vault.NewClient(&vault.Config{
+ Address: os.Getenv("VAULT_ADDR"),
+ })
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), suite.vaultclient)
+ }
+
+ err = suite.vaultclient.Sys().Unmount("transit")
+ require.Nil(suite.T(), err)
+ err = suite.vaultclient.Sys().Unmount("somerandompath")
+ require.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestProvider() {
+ suite.GetProvider("provider")
+}
+
+func (suite *VaultSuite) TestCreateKey() {
+ provider := suite.GetProvider("createkey")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+}
+
+func (suite *VaultSuite) TestSign() {
+ provider := suite.GetProvider("testsign")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ verifier, _ := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestSignOpts() {
+ addr := os.Getenv("VAULT_ADDR")
+ token := os.Getenv("VAULT_TOKEN")
+ os.Setenv("VAULT_ADDR", "")
+ os.Setenv("VAULT_TOKEN", "")
+ defer os.Setenv("VAULT_ADDR", addr)
+ defer os.Setenv("VAULT_TOKEN", token)
+ provider := suite.GetProvider("testsign",
+ options.WithRPCAuthOpts(options.RPCAuth{Address: addr, Token: token}))
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ verifier, _ := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestSignSpecificKeyVersion() {
+ provider := suite.GetProvider("testsignversion")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ // test without specifying any value (aka use default)
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+ verifier, _ := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+
+ // test with specifying default value
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("0"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test with specifying explicit value
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test version that doesn't (yet) exist
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("2"))
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), sig)
+
+ // rotate key (now two valid versions)
+ client := suite.vaultclient.Logical()
+ _, err = client.Write("/transit/keys/testsignversion/rotate", nil)
+ assert.Nil(suite.T(), err)
+
+ // test default version again (implicitly)
+ sig, err = provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test default version again (explicitly)
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("0"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test explicit previous version (should still work as we haven't set min_encryption_version yet)
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test explicit new version
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("2"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test bad value
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("3"))
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), sig)
+
+ // change minimum to v2
+ _, err = client.Write("/transit/keys/testsignversion/config", map[string]interface{}{
+ "min_encryption_version": 2,
+ })
+ assert.Nil(suite.T(), err)
+
+ // test explicit previous version (should fail as min_encryption_version has been set)
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.NotNil(suite.T(), err)
+ assert.Nil(suite.T(), sig)
+
+ provider2 := suite.GetProvider("testsignversion", options.WithKeyVersion("2"))
+ sig, err = provider2.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test explicit new version
+ sig, err = provider2.SignMessage(bytes.NewReader(data), options.WithKeyVersion("2"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+}
+
+func (suite *VaultSuite) TestVerifySpecificKeyVersion() {
+ provider := suite.GetProvider("testverifyversion")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ // test using v1
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ // test without specifying key value
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+
+ // test with explicitly specifying default value
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.Nil(suite.T(), err)
+
+ // test version that doesn't (yet) exist
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("2"))
+ assert.NotNil(suite.T(), err)
+
+ // rotate key (now two valid versions)
+ client := suite.vaultclient.Logical()
+ _, err = client.Write("/transit/keys/testverifyversion/rotate", nil)
+ assert.Nil(suite.T(), err)
+
+ // test default version again (implicitly)
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+
+ // test invalid version (0 is fine for signing, but must be >= 1 for verification)
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("0"))
+ assert.NotNil(suite.T(), err)
+
+ // test explicit previous version (should still as we haven't set min_decryption_version yet)
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.Nil(suite.T(), err)
+
+ // test explicit new version (should fail since it doesn't match the v1 key)
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("2"))
+ assert.NotNil(suite.T(), err)
+
+ // test bad value
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("3"))
+ assert.NotNil(suite.T(), err)
+
+ // change minimum to v2
+ _, err = client.Write("/transit/keys/testverifyversion/config", map[string]interface{}{
+ "min_decryption_version": 2,
+ })
+ assert.Nil(suite.T(), err)
+
+ // test explicit previous version (should fail as min_decryption_version has been set)
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithKeyVersion("1"))
+ assert.NotNil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestSignAndRecordKeyVersion() {
+ provider := suite.GetProvider("testrecordsignversion")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ // test for v1
+ data := []byte("mydata")
+ var versionUsed string
+ sig, err := provider.SignMessage(bytes.NewReader(data), options.ReturnKeyVersionUsed(&versionUsed))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+ assert.Contains(suite.T(), versionUsed, "vault:v1:")
+
+ // rotate
+ client := suite.vaultclient.Logical()
+ _, err = client.Write("/transit/keys/testrecordsignversion/rotate", nil)
+ assert.Nil(suite.T(), err)
+
+ sig, err = provider.SignMessage(bytes.NewReader(data), options.WithKeyVersion("2"), options.ReturnKeyVersionUsed(&versionUsed))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+ assert.Contains(suite.T(), versionUsed, "vault:v2:")
+}
+
+func (suite *VaultSuite) TestSignWithDifferentTransitSecretEnginePath() {
+ provider := suite.GetProvider("testsign")
+ os.Setenv("TRANSIT_SECRET_ENGINE_PATH", "somerandompath")
+ defer os.Setenv("TRANSIT_SECRET_ENGINE_PATH", "transit")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data), options.WithContext(context.Background()))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ verifier, err := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ assert.Nil(suite.T(), err)
+
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithContext(context.Background()))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestInvalidPublicKey() {
+ var provider *SignerVerifier
+ var err error
+ assert.NotPanics(suite.T(), func() {
+ provider, _ = LoadSignerVerifier("hashivault://pki_int", crypto.SHA256)
+ _, err = provider.client.fetchPublicKey(context.Background())
+ })
+ assert.NotNil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestSignWithDifferentTransitSecretEnginePathOpts() {
+ provider := suite.GetProvider("testsign", options.WithRPCAuthOpts(options.RPCAuth{Path: "somerandompath"}))
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data), options.WithContext(context.Background()))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ verifier, err := signature.LoadECDSAVerifier(key.(*ecdsa.PublicKey), crypto.SHA256)
+ assert.Nil(suite.T(), err)
+
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), options.WithContext(context.Background()))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestPubKeyVerify() {
+ provider := suite.GetProvider("testsign")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ k, err := provider.PublicKey()
+ require.NotNil(suite.T(), k)
+ require.Nil(suite.T(), err)
+
+ pubKey, ok := k.(*ecdsa.PublicKey)
+ require.True(suite.T(), ok)
+
+ verifier, _ := signature.LoadECDSAVerifier(pubKey, crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestCryptoSigner() {
+ provider := suite.GetProvider("testsign")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ cs, opts, err := provider.CryptoSigner(context.Background(), func(err error) { require.Nil(suite.T(), err) })
+ hasher := opts.HashFunc().New()
+ _, _ = hasher.Write(data)
+ sig, err := cs.Sign(rand.Reader, hasher.Sum(nil), opts)
+ require.Nil(suite.T(), err)
+ require.NotNil(suite.T(), sig)
+
+ k := cs.Public()
+ require.NotNil(suite.T(), k)
+
+ pubKey, ok := k.(*ecdsa.PublicKey)
+ require.True(suite.T(), ok)
+
+ verifier, _ := signature.LoadECDSAVerifier(pubKey, crypto.SHA256)
+ err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestVerify() {
+ provider := suite.GetProvider("testverify")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+}
+
+func (suite *VaultSuite) TestVerifyBadData() {
+ provider := suite.GetProvider("testverify")
+
+ key, err := provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key)
+
+ data := []byte("mydata")
+ sig, err := provider.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig)
+
+ dataInvalid := []byte("mydata-invalid")
+ err = provider.VerifySignature(bytes.NewReader(sig), bytes.NewReader(dataInvalid))
+ assert.Contains(suite.T(), err.Error(), "failed vault verification")
+}
+
+func (suite *VaultSuite) TestBadSignature() {
+ provider1 := suite.GetProvider("testverify1")
+ provider2 := suite.GetProvider("testverify2")
+
+ key1, err := provider1.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key1)
+
+ key2, err := provider2.CreateKey(context.Background(), AlgorithmECDSAP256)
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), key2)
+
+ data := []byte("mydata")
+ sig1, err := provider1.SignMessage(bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), sig1)
+
+ err = provider1.VerifySignature(bytes.NewReader(sig1), bytes.NewReader(data))
+ assert.Nil(suite.T(), err)
+
+ err = provider2.VerifySignature(bytes.NewReader(sig1), bytes.NewReader(data))
+ assert.NotNil(suite.T(), err)
+ assert.Contains(suite.T(), err.Error(), "failed vault verification")
+}
+
+func (suite *VaultSuite) TestNoProvider() {
+ provider, err := LoadSignerVerifier("hashi://nonsense", crypto.Hash(0))
+ require.Error(suite.T(), err)
+ require.Nil(suite.T(), provider)
+}
+
+func (suite *VaultSuite) TestInvalidHost() {
+ provider, err := LoadSignerVerifier("hashivault://keyname", crypto.SHA256,
+ options.WithRPCAuthOpts(options.RPCAuth{Address: "https://unknown.example.com:8200"}))
+ assert.Nil(suite.T(), err)
+ assert.NotNil(suite.T(), provider)
+
+ _, err = provider.CreateKey(context.Background(), AlgorithmECDSAP256)
+ require.Error(suite.T(), err)
+}
+
+func TestVault(t *testing.T) {
+ suite.Run(t, new(VaultSuite))
+}
diff --git a/pkg/signature/kms/hashivault/go.mod b/pkg/signature/kms/hashivault/go.mod
new file mode 100644
index 0000000..a03d419
--- /dev/null
+++ b/pkg/signature/kms/hashivault/go.mod
@@ -0,0 +1,46 @@
+module github.com/sigstore/sigstore/pkg/signature/kms/hashivault
+
+replace github.com/sigstore/sigstore => ../../../../
+
+go 1.20
+
+require (
+ github.com/hashicorp/vault/api v1.10.0
+ github.com/jellydator/ttlcache/v3 v3.1.1
+ github.com/mitchellh/go-homedir v1.1.0
+ github.com/sigstore/sigstore v1.6.4
+ github.com/stretchr/testify v1.8.4
+)
+
+require (
+ github.com/cenkalti/backoff/v3 v3.2.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+ github.com/google/go-containerregistry v0.17.0 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
+ github.com/hashicorp/go-rootcerts v1.0.2 // indirect
+ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
+ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
+ github.com/hashicorp/go-sockaddr v1.0.2 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/kr/pretty v0.2.1 // indirect
+ github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/ryanuber/go-glob v1.0.0 // indirect
+ github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/net v0.18.0 // indirect
+ golang.org/x/sync v0.3.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.2.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/pkg/signature/kms/hashivault/go.sum b/pkg/signature/kms/hashivault/go.sum
new file mode 100644
index 0000000..e3b0bbe
--- /dev/null
+++ b/pkg/signature/kms/hashivault/go.sum
@@ -0,0 +1,124 @@
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
+github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
+github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
+github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
+github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
+github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
+github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8=
+github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
+github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
+github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
+github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
+go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
+golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
+golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
+gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/signature/kms/hashivault/hashivault_test.go b/pkg/signature/kms/hashivault/hashivault_test.go
new file mode 100644
index 0000000..4ee219b
--- /dev/null
+++ b/pkg/signature/kms/hashivault/hashivault_test.go
@@ -0,0 +1,56 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hashivault
+
+import "testing"
+
+func TestParseReference(t *testing.T) {
+ tests := []struct {
+ in string
+ wantKey string
+ wantErr bool
+ }{
+ {
+ in: "hashivault://cosign",
+ wantKey: "cosign",
+ wantErr: false,
+ },
+ {
+ in: "hashivault://cosign/nested",
+ wantErr: true,
+ },
+ {
+ in: "foo://bar",
+ wantErr: true,
+ },
+ {
+ in: "",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ gotKey, err := parseReference(tt.in)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("parseReference() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotKey != tt.wantKey {
+ t.Errorf("parseReference() gotKey = %v, want %v", gotKey, tt.wantKey)
+ }
+ })
+ }
+}
diff --git a/pkg/signature/kms/hashivault/signer.go b/pkg/signature/kms/hashivault/signer.go
new file mode 100644
index 0000000..1965f31
--- /dev/null
+++ b/pkg/signature/kms/hashivault/signer.go
@@ -0,0 +1,233 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hashivault
+
+import (
+ "context"
+ "crypto"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// Taken from https://www.vaultproject.io/api/secret/transit
+// nolint:revive
+const (
+ AlgorithmECDSAP256 = "ecdsa-p256"
+ AlgorithmECDSAP384 = "ecdsa-p384"
+ AlgorithmECDSAP521 = "ecdsa-p521"
+ AlgorithmED25519 = "ed25519"
+ AlgorithmRSA2048 = "rsa-2048"
+ AlgorithmRSA3072 = "rsa-3072"
+ AlgorithmRSA4096 = "rsa-4096"
+)
+
+var hvSupportedAlgorithms = []string{
+ AlgorithmECDSAP256,
+ AlgorithmECDSAP384,
+ AlgorithmECDSAP521,
+ AlgorithmED25519,
+ AlgorithmRSA2048,
+ AlgorithmRSA3072,
+ AlgorithmRSA4096,
+}
+
+var hvSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA224,
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+ crypto.Hash(0),
+}
+
+// SignerVerifier creates and verifies digital signatures over a message using Hashicorp Vault KMS service
+type SignerVerifier struct {
+ hashFunc crypto.Hash
+ client *hashivaultClient
+}
+
+// LoadSignerVerifier generates signatures using the specified key object in Vault and hash algorithm.
+//
+// It also can verify signatures (via a remote vall to the Vault instance). hashFunc should be
+// set to crypto.Hash(0) if the key referred to by referenceStr is an ED25519 signing key.
+func LoadSignerVerifier(referenceStr string, hashFunc crypto.Hash, opts ...signature.RPCOption) (*SignerVerifier, error) {
+ h := &SignerVerifier{}
+ ctx := context.Background()
+ rpcAuth := options.RPCAuth{}
+ var keyVersion string
+ for _, opt := range opts {
+ opt.ApplyRPCAuthOpts(&rpcAuth)
+ opt.ApplyContext(&ctx)
+ opt.ApplyKeyVersion(&keyVersion)
+ }
+
+ var keyVersionUint uint64
+ var err error
+ if keyVersion != "" {
+ keyVersionUint, err = strconv.ParseUint(keyVersion, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("parsing key version: %w", err)
+ }
+ }
+
+ if rpcAuth.OIDC.Token != "" {
+ rpcAuth.Token, err = oidcLogin(ctx, rpcAuth.Address, rpcAuth.OIDC.Path, rpcAuth.OIDC.Role, rpcAuth.OIDC.Token)
+ if err != nil {
+ return nil, err
+ }
+ }
+ h.client, err = newHashivaultClient(rpcAuth.Address, rpcAuth.Token, rpcAuth.Path, referenceStr, keyVersionUint)
+ if err != nil {
+ return nil, err
+ }
+
+ switch hashFunc {
+ case 0, crypto.SHA224, crypto.SHA256, crypto.SHA384, crypto.SHA512:
+ h.hashFunc = hashFunc
+ default:
+ return nil, errors.New("hash function not supported by Hashivault")
+ }
+
+ return h, nil
+}
+
+// SignMessage signs the provided message using HashiCorp Vault KMS. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the HashivaultSigner was created.
+//
+// SignMessage recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// All other options are ignored if specified.
+func (h SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) {
+ var digest []byte
+ var signerOpts crypto.SignerOpts = h.hashFunc
+
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ opt.ApplyCryptoSignerOpts(&signerOpts)
+ }
+
+ digest, hf, err := signature.ComputeDigestForSigning(message, signerOpts.HashFunc(), hvSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return h.client.sign(digest, hf, opts...)
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. All options provided in arguments to this method are ignored.
+func (h SignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return h.client.public()
+}
+
+// VerifySignature verifies the signature for the given message. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the SignerVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (h SignerVerifier) VerifySignature(sig, message io.Reader, opts ...signature.VerifyOption) error {
+ var digest []byte
+ var signerOpts crypto.SignerOpts = h.hashFunc
+
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ opt.ApplyCryptoSignerOpts(&signerOpts)
+ }
+
+ digest, hf, err := signature.ComputeDigestForVerifying(message, signerOpts.HashFunc(), hvSupportedHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+
+ sigBytes, err := io.ReadAll(sig)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ return h.client.verify(sigBytes, digest, hf, opts...)
+}
+
+// CreateKey attempts to create a new key in Vault with the specified algorithm.
+func (h SignerVerifier) CreateKey(_ context.Context, algorithm string) (crypto.PublicKey, error) {
+ return h.client.createKey(algorithm)
+}
+
+type cryptoSignerWrapper struct {
+ ctx context.Context
+ hashFunc crypto.Hash
+ sv *SignerVerifier
+ errFunc func(error)
+}
+
+func (c cryptoSignerWrapper) Public() crypto.PublicKey {
+ pk, err := c.sv.PublicKey(options.WithContext(c.ctx))
+ if err != nil && c.errFunc != nil {
+ c.errFunc(err)
+ }
+ return pk
+}
+
+func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ hashFunc := c.hashFunc
+ if opts != nil {
+ hashFunc = opts.HashFunc()
+ }
+ hvOptions := []signature.SignOption{
+ options.WithContext(c.ctx),
+ options.WithDigest(digest),
+ options.WithCryptoSignerOpts(hashFunc),
+ }
+
+ return c.sv.SignMessage(nil, hvOptions...)
+}
+
+// CryptoSigner returns a crypto.Signer object that uses the underlying SignerVerifier, along with a crypto.SignerOpts object
+// that allows the KMS to be used in APIs that only accept the standard golang objects
+func (h *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) {
+ csw := &cryptoSignerWrapper{
+ ctx: ctx,
+ sv: h,
+ hashFunc: h.hashFunc,
+ errFunc: errFunc,
+ }
+
+ return csw, h.hashFunc, nil
+}
+
+// SupportedAlgorithms returns the list of algorithms supported by the Hashicorp Vault service
+func (h *SignerVerifier) SupportedAlgorithms() []string {
+ return hvSupportedAlgorithms
+}
+
+// DefaultAlgorithm returns the default algorithm for the Hashicorp Vault service
+func (h *SignerVerifier) DefaultAlgorithm() string {
+ return AlgorithmECDSAP256
+}
diff --git a/pkg/signature/kms/kms.go b/pkg/signature/kms/kms.go
new file mode 100644
index 0000000..7095eb1
--- /dev/null
+++ b/pkg/signature/kms/kms.go
@@ -0,0 +1,78 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package kms implements the interface to access various ksm services
+package kms
+
+import (
+ "context"
+ "crypto"
+ "fmt"
+ "strings"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+)
+
+// ProviderNotFoundError indicates that no matching KMS provider was found
+type ProviderNotFoundError struct {
+ ref string
+}
+
+func (e *ProviderNotFoundError) Error() string {
+ return fmt.Sprintf("no kms provider found for key reference: %s", e.ref)
+}
+
+// ProviderInit is a function that initializes provider-specific SignerVerifier.
+//
+// It takes a provider-specific resource ID and hash function, and returns a
+// SignerVerifier using that resource, or any error that was encountered.
+type ProviderInit func(context.Context, string, crypto.Hash, ...signature.RPCOption) (SignerVerifier, error)
+
+// AddProvider adds the provider implementation into the local cache
+func AddProvider(keyResourceID string, init ProviderInit) {
+ providersMap[keyResourceID] = init
+}
+
+var providersMap = map[string]ProviderInit{}
+
+// Get returns a KMS SignerVerifier for the given resource string and hash function.
+// If no matching provider is found, Get returns a ProviderNotFoundError. It
+// also returns an error if initializing the SignerVerifier fails.
+func Get(ctx context.Context, keyResourceID string, hashFunc crypto.Hash, opts ...signature.RPCOption) (SignerVerifier, error) {
+ for ref, pi := range providersMap {
+ if strings.HasPrefix(keyResourceID, ref) {
+ return pi(ctx, keyResourceID, hashFunc, opts...)
+ }
+ }
+ return nil, &ProviderNotFoundError{ref: keyResourceID}
+}
+
+// SupportedProviders returns list of initialized providers
+func SupportedProviders() []string {
+ keys := make([]string, 0, len(providersMap))
+ for key := range providersMap {
+ keys = append(keys, key)
+ }
+ return keys
+}
+
+// SignerVerifier creates and verifies digital signatures over a message using a KMS service
+type SignerVerifier interface {
+ signature.SignerVerifier
+ CreateKey(ctx context.Context, algorithm string) (crypto.PublicKey, error)
+ CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error)
+ SupportedAlgorithms() []string
+ DefaultAlgorithm() string
+}
diff --git a/pkg/signature/message.go b/pkg/signature/message.go
new file mode 100644
index 0000000..6f8449e
--- /dev/null
+++ b/pkg/signature/message.go
@@ -0,0 +1,111 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ crand "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+)
+
+func isSupportedAlg(alg crypto.Hash, supportedAlgs []crypto.Hash) bool {
+ if supportedAlgs == nil {
+ return true
+ }
+ for _, supportedAlg := range supportedAlgs {
+ if alg == supportedAlg {
+ return true
+ }
+ }
+ return false
+}
+
+// ComputeDigestForSigning calculates the digest value for the specified message using a hash function selected by the following process:
+//
+// - if a digest value is already specified in a SignOption and the length of the digest matches that of the selected hash function, the
+// digest value will be returned without any further computation
+// - if a hash function is given using WithCryptoSignerOpts(opts) as a SignOption, it will be used (if it is in the supported list)
+// - otherwise defaultHashFunc will be used (if it is in the supported list)
+func ComputeDigestForSigning(rawMessage io.Reader, defaultHashFunc crypto.Hash, supportedHashFuncs []crypto.Hash, opts ...SignOption) (digest []byte, hashedWith crypto.Hash, err error) {
+ var cryptoSignerOpts crypto.SignerOpts = defaultHashFunc
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ opt.ApplyCryptoSignerOpts(&cryptoSignerOpts)
+ }
+ hashedWith = cryptoSignerOpts.HashFunc()
+ if !isSupportedAlg(hashedWith, supportedHashFuncs) {
+ return nil, crypto.Hash(0), fmt.Errorf("unsupported hash algorithm: %q not in %v", hashedWith.String(), supportedHashFuncs)
+ }
+ if len(digest) > 0 {
+ if hashedWith != crypto.Hash(0) && len(digest) != hashedWith.Size() {
+ err = errors.New("unexpected length of digest for hash function specified")
+ }
+ return
+ }
+ digest, err = hashMessage(rawMessage, hashedWith)
+ return
+}
+
+// ComputeDigestForVerifying calculates the digest value for the specified message using a hash function selected by the following process:
+//
+// - if a digest value is already specified in a SignOption and the length of the digest matches that of the selected hash function, the
+// digest value will be returned without any further computation
+// - if a hash function is given using WithCryptoSignerOpts(opts) as a SignOption, it will be used (if it is in the supported list)
+// - otherwise defaultHashFunc will be used (if it is in the supported list)
+func ComputeDigestForVerifying(rawMessage io.Reader, defaultHashFunc crypto.Hash, supportedHashFuncs []crypto.Hash, opts ...VerifyOption) (digest []byte, hashedWith crypto.Hash, err error) {
+ var cryptoSignerOpts crypto.SignerOpts = defaultHashFunc
+ for _, opt := range opts {
+ opt.ApplyDigest(&digest)
+ opt.ApplyCryptoSignerOpts(&cryptoSignerOpts)
+ }
+ hashedWith = cryptoSignerOpts.HashFunc()
+ if !isSupportedAlg(hashedWith, supportedHashFuncs) {
+ return nil, crypto.Hash(0), fmt.Errorf("unsupported hash algorithm: %q not in %v", hashedWith.String(), supportedHashFuncs)
+ }
+ if len(digest) > 0 {
+ if hashedWith != crypto.Hash(0) && len(digest) != hashedWith.Size() {
+ err = errors.New("unexpected length of digest for hash function specified")
+ }
+ return
+ }
+ digest, err = hashMessage(rawMessage, hashedWith)
+ return
+}
+
+func hashMessage(rawMessage io.Reader, hashFunc crypto.Hash) ([]byte, error) {
+ if rawMessage == nil {
+ return nil, errors.New("message cannot be nil")
+ }
+ if hashFunc == crypto.Hash(0) {
+ return io.ReadAll(rawMessage)
+ }
+ hasher := hashFunc.New()
+ // avoids reading entire message into memory
+ if _, err := io.Copy(hasher, rawMessage); err != nil {
+ return nil, fmt.Errorf("hashing message: %w", err)
+ }
+ return hasher.Sum(nil), nil
+}
+
+func selectRandFromOpts(opts ...SignOption) io.Reader {
+ rand := crand.Reader
+ for _, opt := range opts {
+ opt.ApplyRand(&rand)
+ }
+ return rand
+}
diff --git a/pkg/signature/options.go b/pkg/signature/options.go
new file mode 100644
index 0000000..0be699f
--- /dev/null
+++ b/pkg/signature/options.go
@@ -0,0 +1,57 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "context"
+ "crypto"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// RPCOption specifies options to be used when performing RPC
+type RPCOption interface {
+ ApplyContext(*context.Context)
+ ApplyRemoteVerification(*bool)
+ ApplyRPCAuthOpts(opts *options.RPCAuth)
+ ApplyKeyVersion(keyVersion *string)
+}
+
+// PublicKeyOption specifies options to be used when obtaining a public key
+type PublicKeyOption interface {
+ RPCOption
+}
+
+// MessageOption specifies options to be used when processing messages during signing or verification
+type MessageOption interface {
+ ApplyDigest(*[]byte)
+ ApplyCryptoSignerOpts(*crypto.SignerOpts)
+}
+
+// SignOption specifies options to be used when signing a message
+type SignOption interface {
+ RPCOption
+ MessageOption
+ ApplyRand(*io.Reader)
+ ApplyKeyVersionUsed(**string)
+}
+
+// VerifyOption specifies options to be used when verifying a signature
+type VerifyOption interface {
+ RPCOption
+ MessageOption
+}
diff --git a/pkg/signature/options/context.go b/pkg/signature/options/context.go
new file mode 100644
index 0000000..2282a86
--- /dev/null
+++ b/pkg/signature/options/context.go
@@ -0,0 +1,37 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package options defines options for KMS clients
+package options
+
+import (
+ "context"
+)
+
+// RequestContext implements the functional option pattern for including a context during RPC
+type RequestContext struct {
+ NoOpOptionImpl
+ ctx context.Context
+}
+
+// ApplyContext sets the specified context as the functional option
+func (r RequestContext) ApplyContext(ctx *context.Context) {
+ *ctx = r.ctx
+}
+
+// WithContext specifies that the given context should be used in RPC to external services
+func WithContext(ctx context.Context) RequestContext {
+ return RequestContext{ctx: ctx}
+}
diff --git a/pkg/signature/options/digest.go b/pkg/signature/options/digest.go
new file mode 100644
index 0000000..21875dc
--- /dev/null
+++ b/pkg/signature/options/digest.go
@@ -0,0 +1,35 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+// RequestDigest implements the functional option pattern for specifying a digest value
+type RequestDigest struct {
+ NoOpOptionImpl
+ digest []byte
+}
+
+// ApplyDigest sets the specified digest value as the functional option
+func (r RequestDigest) ApplyDigest(digest *[]byte) {
+ *digest = r.digest
+}
+
+// WithDigest specifies that the given digest can be used by underlying signature implementations
+// WARNING: When verifying a digest with ECDSA, it is trivial to craft a valid signature
+// over a random message given a public key. Do not use this unles you understand the
+// implications and do not need to protect against malleability.
+func WithDigest(digest []byte) RequestDigest {
+ return RequestDigest{digest: digest}
+}
diff --git a/pkg/signature/options/doc.go b/pkg/signature/options/doc.go
new file mode 100644
index 0000000..ebdeda2
--- /dev/null
+++ b/pkg/signature/options/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package options contains functional options for the various SignerVerifiers
+package options
diff --git a/pkg/signature/options/keyversion.go b/pkg/signature/options/keyversion.go
new file mode 100644
index 0000000..751418f
--- /dev/null
+++ b/pkg/signature/options/keyversion.go
@@ -0,0 +1,50 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+// RequestKeyVersion implements the functional option pattern for specifying the KMS key version during signing or verification
+type RequestKeyVersion struct {
+ NoOpOptionImpl
+ keyVersion string
+}
+
+// ApplyKeyVersion sets the KMS's key version as a functional option
+func (r RequestKeyVersion) ApplyKeyVersion(keyVersion *string) {
+ *keyVersion = r.keyVersion
+}
+
+// WithKeyVersion specifies that a specific KMS key version be used during signing and verification operations;
+// a value of 0 will use the latest version of the key (default)
+func WithKeyVersion(keyVersion string) RequestKeyVersion {
+ return RequestKeyVersion{keyVersion: keyVersion}
+}
+
+// RequestKeyVersionUsed implements the functional option pattern for obtaining the KMS key version used during signing
+type RequestKeyVersionUsed struct {
+ NoOpOptionImpl
+ keyVersionUsed *string
+}
+
+// ApplyKeyVersionUsed requests to store the KMS's key version that was used as a functional option
+func (r RequestKeyVersionUsed) ApplyKeyVersionUsed(keyVersionUsed **string) {
+ *keyVersionUsed = r.keyVersionUsed
+}
+
+// ReturnKeyVersionUsed specifies that the specific KMS key version that was used during signing should be stored
+// in the pointer provided
+func ReturnKeyVersionUsed(keyVersionUsed *string) RequestKeyVersionUsed {
+ return RequestKeyVersionUsed{keyVersionUsed: keyVersionUsed}
+}
diff --git a/pkg/signature/options/noop.go b/pkg/signature/options/noop.go
new file mode 100644
index 0000000..c7f1ccb
--- /dev/null
+++ b/pkg/signature/options/noop.go
@@ -0,0 +1,49 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+ "context"
+ "crypto"
+ "io"
+)
+
+// NoOpOptionImpl implements the RPCOption, SignOption, VerifyOption interfaces as no-ops.
+type NoOpOptionImpl struct{}
+
+// ApplyContext is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyContext(_ *context.Context) {}
+
+// ApplyCryptoSignerOpts is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyCryptoSignerOpts(_ *crypto.SignerOpts) {}
+
+// ApplyDigest is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyDigest(_ *[]byte) {}
+
+// ApplyRand is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyRand(_ *io.Reader) {}
+
+// ApplyRemoteVerification is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyRemoteVerification(_ *bool) {}
+
+// ApplyRPCAuthOpts is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyRPCAuthOpts(_ *RPCAuth) {}
+
+// ApplyKeyVersion is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyKeyVersion(_ *string) {}
+
+// ApplyKeyVersionUsed is a no-op required to fully implement the requisite interfaces
+func (NoOpOptionImpl) ApplyKeyVersionUsed(_ **string) {}
diff --git a/pkg/signature/options/rand.go b/pkg/signature/options/rand.go
new file mode 100644
index 0000000..fd3a17f
--- /dev/null
+++ b/pkg/signature/options/rand.go
@@ -0,0 +1,41 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+ crand "crypto/rand"
+ "io"
+)
+
+// RequestRand implements the functional option pattern for using a specific source of entropy
+type RequestRand struct {
+ NoOpOptionImpl
+ rand io.Reader
+}
+
+// ApplyRand sets the specified source of entropy as the functional option
+func (r RequestRand) ApplyRand(rand *io.Reader) {
+ *rand = r.rand
+}
+
+// WithRand specifies that the given source of entropy should be used in signing operations
+func WithRand(rand io.Reader) RequestRand {
+ r := rand
+ if r == nil {
+ r = crand.Reader
+ }
+ return RequestRand{rand: r}
+}
diff --git a/pkg/signature/options/remoteverification.go b/pkg/signature/options/remoteverification.go
new file mode 100644
index 0000000..26144ad
--- /dev/null
+++ b/pkg/signature/options/remoteverification.go
@@ -0,0 +1,32 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+// RequestRemoteVerification implements the functional option pattern for remotely verifiying signatures when possible
+type RequestRemoteVerification struct {
+ NoOpOptionImpl
+ remoteVerification bool
+}
+
+// ApplyRemoteVerification sets remote verification as a functional option
+func (r RequestRemoteVerification) ApplyRemoteVerification(remoteVerification *bool) {
+ *remoteVerification = r.remoteVerification
+}
+
+// WithRemoteVerification specifies that the verification operation should be performed remotely (vs in the process of the caller)
+func WithRemoteVerification(remoteVerification bool) RequestRemoteVerification {
+ return RequestRemoteVerification{remoteVerification: remoteVerification}
+}
diff --git a/pkg/signature/options/rpcauth.go b/pkg/signature/options/rpcauth.go
new file mode 100644
index 0000000..188de92
--- /dev/null
+++ b/pkg/signature/options/rpcauth.go
@@ -0,0 +1,58 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+// RPCAuthOpts includes authentication settings for RPC calls
+type RPCAuthOpts struct {
+ NoOpOptionImpl
+ opts RPCAuth
+}
+
+// RPCAuth provides credentials for RPC calls, empty fields are ignored
+type RPCAuth struct {
+ Address string // address is the remote server address, e.g. https://vault:8200
+ Path string // path for the RPC, in vault this is the transit path which default to "transit"
+ Token string // token used for RPC, in vault this is the VAULT_TOKEN value
+ OIDC RPCAuthOIDC
+}
+
+// RPCAuthOIDC is used to perform the RPC login using OIDC instead of a fixed token
+type RPCAuthOIDC struct {
+ Path string // path defaults to "jwt" for vault
+ Role string // role is required for jwt logins
+ Token string // token is a jwt with vault
+}
+
+// ApplyRPCAuthOpts sets the RPCAuth as a function option
+func (r RPCAuthOpts) ApplyRPCAuthOpts(opts *RPCAuth) {
+ if r.opts.Address != "" {
+ opts.Address = r.opts.Address
+ }
+ if r.opts.Path != "" {
+ opts.Path = r.opts.Path
+ }
+ if r.opts.Token != "" {
+ opts.Token = r.opts.Token
+ }
+ if r.opts.OIDC.Token != "" {
+ opts.OIDC = r.opts.OIDC
+ }
+}
+
+// WithRPCAuthOpts specifies RPCAuth settings to be used with RPC logins
+func WithRPCAuthOpts(opts RPCAuth) RPCAuthOpts {
+ return RPCAuthOpts{opts: opts}
+}
diff --git a/pkg/signature/options/signeropts.go b/pkg/signature/options/signeropts.go
new file mode 100644
index 0000000..1a3ac73
--- /dev/null
+++ b/pkg/signature/options/signeropts.go
@@ -0,0 +1,40 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+ "crypto"
+)
+
+// RequestCryptoSignerOpts implements the functional option pattern for supplying crypto.SignerOpts when signing or verifying
+type RequestCryptoSignerOpts struct {
+ NoOpOptionImpl
+ opts crypto.SignerOpts
+}
+
+// ApplyCryptoSignerOpts sets crypto.SignerOpts as a functional option
+func (r RequestCryptoSignerOpts) ApplyCryptoSignerOpts(opts *crypto.SignerOpts) {
+ *opts = r.opts
+}
+
+// WithCryptoSignerOpts specifies that provided crypto.SignerOpts be used during signing and verification operations
+func WithCryptoSignerOpts(opts crypto.SignerOpts) RequestCryptoSignerOpts {
+ var optsToUse crypto.SignerOpts = crypto.SHA256
+ if opts != nil {
+ optsToUse = opts
+ }
+ return RequestCryptoSignerOpts{opts: optsToUse}
+}
diff --git a/pkg/signature/payload/doc.go b/pkg/signature/payload/doc.go
new file mode 100644
index 0000000..3664185
--- /dev/null
+++ b/pkg/signature/payload/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package payload contains types and utilities related to the Cosign signature format.
+package payload
diff --git a/pkg/signature/payload/payload.go b/pkg/signature/payload/payload.go
new file mode 100644
index 0000000..cab6f5b
--- /dev/null
+++ b/pkg/signature/payload/payload.go
@@ -0,0 +1,122 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package payload defines a container image
+package payload
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+// CosignSignatureType is the value of `critical.type` in a SimpleContainerImage payload.
+const CosignSignatureType = "cosign container image signature"
+
+// SimpleContainerImage describes the structure of a basic container image signature payload, as defined at:
+// https://github.com/containers/image/blob/main/docs/containers-signature.5.md#json-data-format
+type SimpleContainerImage struct {
+ Critical Critical `json:"critical"` // Critical data critical to correctly evaluating the validity of the signature
+ Optional map[string]interface{} `json:"optional"` // Optional optional metadata about the image
+}
+
+// Critical data critical to correctly evaluating the validity of a signature
+type Critical struct {
+ Identity Identity `json:"identity"` // Identity claimed identity of the image
+ Image Image `json:"image"` // Image identifies the container that the signature applies to
+ Type string `json:"type"` // Type must be 'atomic container signature'
+}
+
+// Identity is the claimed identity of the image
+type Identity struct {
+ DockerReference string `json:"docker-reference"` // DockerReference is a reference used to refer to or download the image
+}
+
+// Image identifies the container image that the signature applies to
+type Image struct {
+ DockerManifestDigest string `json:"docker-manifest-digest"` // DockerManifestDigest the manifest digest of the signed container image
+}
+
+// Cosign describes a container image signed using Cosign
+type Cosign struct {
+ Image name.Digest
+ // ClaimedIdentity is what the signer claims the image to be; usually a registry.com/…/repo:tag, but can also use a digest instead.
+ // ALMOST ALL consumers MUST verify that ClaimedIdentity in the signature is correct given how user refers to the image;
+ // e.g. if the user asks to access a signed image example.com/repo/mysql:3.14,
+ // it is ALMOST ALWAYS necessary to validate that ClaimedIdentity = example.com/repo/mysql:3.14
+ //
+ // Considerations:
+ // - The user might refer to an image using a digest (example.com/repo/mysql@sha256:…); in that case the registry/…/repo should still match
+ // - If the image is multi-arch, ClaimedIdentity usually refers to the top-level multi-arch image index also on the per-arch images
+ // (possibly even if ClaimedIdentity contains a digest!)
+ // - Older versions of cosign generate signatures where ClaimedIdentity only contains a registry/…/repo ; signature consumers should allow users
+ // to determine whether such images should be accepted (and, long-term, the default SHOULD be to reject them)
+ ClaimedIdentity string
+ Annotations map[string]interface{}
+}
+
+// SimpleContainerImage returns information about a container image in the github.com/containers/image/signature format
+func (p Cosign) SimpleContainerImage() SimpleContainerImage {
+ dockerReference := p.Image.Repository.Name()
+ if p.ClaimedIdentity != "" {
+ dockerReference = p.ClaimedIdentity
+ }
+ return SimpleContainerImage{
+ Critical: Critical{
+ Identity: Identity{
+ DockerReference: dockerReference,
+ },
+ Image: Image{
+ DockerManifestDigest: p.Image.DigestStr(),
+ },
+ Type: CosignSignatureType,
+ },
+ Optional: p.Annotations,
+ }
+}
+
+// MarshalJSON marshals the container signature into a []byte of JSON data
+func (p Cosign) MarshalJSON() ([]byte, error) {
+ return json.Marshal(p.SimpleContainerImage())
+}
+
+var _ json.Marshaler = Cosign{}
+
+// UnmarshalJSON unmarshals []byte of JSON data into a container signature object
+func (p *Cosign) UnmarshalJSON(data []byte) error {
+ if string(data) == "null" {
+ // JSON "null" is a no-op by convention
+ return nil
+ }
+ var simple SimpleContainerImage
+ if err := json.Unmarshal(data, &simple); err != nil {
+ return err
+ }
+ if simple.Critical.Type != CosignSignatureType {
+ return fmt.Errorf("Cosign signature payload was of an unknown type: %q", simple.Critical.Type)
+ }
+ digestStr := simple.Critical.Identity.DockerReference + "@" + simple.Critical.Image.DockerManifestDigest
+ digest, err := name.NewDigest(digestStr)
+ if err != nil {
+ return fmt.Errorf("could not parse image digest string %q: %w", digestStr, err)
+ }
+ p.Image = digest
+ p.ClaimedIdentity = simple.Critical.Identity.DockerReference
+ p.Annotations = simple.Optional
+ return nil
+}
+
+var _ json.Unmarshaler = (*Cosign)(nil)
diff --git a/pkg/signature/payload/payload_test.go b/pkg/signature/payload/payload_test.go
new file mode 100644
index 0000000..bf3c635
--- /dev/null
+++ b/pkg/signature/payload/payload_test.go
@@ -0,0 +1,165 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package payload
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/go-test/deep"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+const validDigest = "sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"
+
+func mustParseDigest(t *testing.T, digestStr string) name.Digest {
+ t.Helper()
+ digest, err := name.NewDigest(digestStr)
+ if err != nil {
+ t.Fatalf("could not parse digest %q: %v", digestStr, err)
+ }
+ return digest
+}
+
+func TestMarshalCosign(t *testing.T) {
+ t.Parallel()
+ testCases := []struct {
+ desc string
+ imgPayload Cosign
+ expected string
+ }{
+ {
+ desc: "no claims",
+ imgPayload: Cosign{
+ Image: mustParseDigest(t, "example.com/test/image@"+validDigest),
+ },
+ expected: `{"critical":{"identity":{"docker-reference":"example.com/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":null}`,
+ },
+ {
+ desc: "standard atomic signature",
+ imgPayload: Cosign{
+ Image: mustParseDigest(t, "example.com/atomic/test/image@"+validDigest),
+ Annotations: map[string]interface{}{
+ "creator": "atomic",
+ "timestamp": 1458239713,
+ },
+ },
+ expected: `{"critical":{"identity":{"docker-reference":"example.com/atomic/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":{"creator":"atomic","timestamp":1458239713}}`,
+ },
+ {
+ desc: "arbitrary claims",
+ imgPayload: Cosign{
+ Image: mustParseDigest(t, "example.com/cosign/test/image@"+validDigest),
+ Annotations: map[string]interface{}{
+ "creator": "anyone",
+ "some_struct": map[string]interface{}{
+ "foo": "bar",
+ "false": true,
+ "nothing": nil,
+ },
+ "CamelCase WithSpace": 8.314,
+ },
+ },
+ expected: `{"critical":{"identity":{"docker-reference":"example.com/cosign/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":{"CamelCase WithSpace":8.314,"creator":"anyone","some_struct":{"false":true,"foo":"bar","nothing":null}}}`,
+ },
+ {
+ desc: "custom identity",
+ imgPayload: Cosign{
+ Image: mustParseDigest(t, "example.com/test/image@"+validDigest),
+ ClaimedIdentity: "docker.io/library/test:1.2.3",
+ },
+ expected: `{"critical":{"identity":{"docker-reference":"docker.io/library/test:1.2.3"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":null}`,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ tc := tc
+ t.Parallel()
+ payload, err := json.Marshal(tc.imgPayload)
+ if err != nil {
+ t.Fatalf("json.Marshal returned error: %v", err)
+ }
+
+ if tc.expected != string(payload) {
+ t.Errorf("marshaled payload was %q, wanted %q", string(payload), tc.expected)
+ }
+ })
+ }
+}
+
+func TestUnmarshalCosign(t *testing.T) {
+ t.Parallel()
+ testCases := []struct {
+ desc string
+ payload string
+
+ expected Cosign
+ expectErr bool
+ }{
+ {
+ desc: "no claims",
+ payload: `{"critical":{"identity":{"docker-reference":"example.com/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":null}`,
+ expected: Cosign{
+ Image: mustParseDigest(t, "example.com/test/image@"+validDigest),
+ ClaimedIdentity: "example.com/test/image",
+ },
+ },
+ {
+ desc: "arbitrary claims",
+ payload: `{"critical":{"identity":{"docker-reference":"example.com/cosign/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"cosign container image signature"},"optional":{"CamelCase WithSpace":8.314,"creator":"anyone","some_struct":{"false":true,"foo":"bar","nothing":null}}}`,
+ expected: Cosign{
+ Image: mustParseDigest(t, "example.com/cosign/test/image@"+validDigest),
+ ClaimedIdentity: "example.com/cosign/test/image",
+ Annotations: map[string]interface{}{
+ "creator": "anyone",
+ "some_struct": map[string]interface{}{
+ "foo": "bar",
+ "false": true,
+ "nothing": nil,
+ },
+ "CamelCase WithSpace": 8.314,
+ },
+ },
+ },
+ {
+ desc: "unknown type",
+ payload: `{"critical":{"identity":{"docker-reference":"example.com/atomic/test/image"},"image":{"docker-manifest-digest":"sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"},"type":"atomic container signature"},"optional":{}}`,
+ expectErr: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ tc := tc
+ t.Parallel()
+ var imgPayload Cosign
+ err := json.Unmarshal([]byte(tc.payload), &imgPayload)
+ if err != nil {
+ if !tc.expectErr {
+ t.Fatalf("json.Unmarshal unexpectedly returned an error: %v", err)
+ }
+ return // operation failed successfully
+ }
+ if tc.expectErr {
+ t.Fatalf("json.Unmarshal returned %v, expected an error", imgPayload)
+ }
+ if diff := deep.Equal(tc.expected, imgPayload); diff != nil {
+ t.Errorf("Cosign unmarshalled incorrectly: %v", diff)
+ }
+ })
+ }
+}
diff --git a/pkg/signature/publickey.go b/pkg/signature/publickey.go
new file mode 100644
index 0000000..6f6a47a
--- /dev/null
+++ b/pkg/signature/publickey.go
@@ -0,0 +1,25 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+)
+
+// PublicKeyProvider returns a PublicKey associated with a digital signature
+type PublicKeyProvider interface {
+ PublicKey(opts ...PublicKeyOption) (crypto.PublicKey, error)
+}
diff --git a/pkg/signature/rsapkcs1v15.go b/pkg/signature/rsapkcs1v15.go
new file mode 100644
index 0000000..1cac68a
--- /dev/null
+++ b/pkg/signature/rsapkcs1v15.go
@@ -0,0 +1,225 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// RSAPKCS1v15Signer is a signature.Signer that uses the RSA PKCS1v15 algorithm
+type RSAPKCS1v15Signer struct {
+ hashFunc crypto.Hash
+ priv *rsa.PrivateKey
+}
+
+// LoadRSAPKCS1v15Signer calculates signatures using the specified private key and hash algorithm.
+//
+// hf must be either SHA256, SHA388, or SHA512.
+func LoadRSAPKCS1v15Signer(priv *rsa.PrivateKey, hf crypto.Hash) (*RSAPKCS1v15Signer, error) {
+ if priv == nil {
+ return nil, errors.New("invalid RSA private key specified")
+ }
+
+ if !isSupportedAlg(hf, rsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &RSAPKCS1v15Signer{
+ priv: priv,
+ hashFunc: hf,
+ }, nil
+}
+
+// SignMessage signs the provided message using PKCS1v15. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the RSAPKCS1v15Signer was created.
+//
+// SignMessage recognizes the following Options listed in order of preference:
+//
+// - WithRand()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (r RSAPKCS1v15Signer) SignMessage(message io.Reader, opts ...SignOption) ([]byte, error) {
+ digest, hf, err := ComputeDigestForSigning(message, r.hashFunc, rsaSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ rand := selectRandFromOpts(opts...)
+
+ return rsa.SignPKCS1v15(rand, r.priv, hf, digest)
+}
+
+// Public returns the public key that can be used to verify signatures created by
+// this signer.
+func (r RSAPKCS1v15Signer) Public() crypto.PublicKey {
+ if r.priv == nil {
+ return nil
+ }
+
+ return r.priv.Public()
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPKCS1v15Signer) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.Public(), nil
+}
+
+// Sign computes the signature for the specified digest using PKCS1v15.
+//
+// If a source of entropy is given in rand, it will be used instead of the default value (rand.Reader
+// from crypto/rand).
+//
+// If opts are specified, they should specify the hash function used to compute digest. If opts are
+// not specified, this function assumes the hash function provided when the signer was created was
+// used to create the value specified in digest.
+func (r RSAPKCS1v15Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ rsaOpts := []SignOption{options.WithDigest(digest), options.WithRand(rand)}
+ if opts != nil {
+ rsaOpts = append(rsaOpts, options.WithCryptoSignerOpts(opts))
+ }
+
+ return r.SignMessage(nil, rsaOpts...)
+}
+
+// RSAPKCS1v15Verifier is a signature.Verifier that uses the RSA PKCS1v15 algorithm
+type RSAPKCS1v15Verifier struct {
+ publicKey *rsa.PublicKey
+ hashFunc crypto.Hash
+}
+
+// LoadRSAPKCS1v15Verifier returns a Verifier that verifies signatures using the specified
+// RSA public key and hash algorithm.
+//
+// hf must be either SHA256, SHA388, or SHA512.
+func LoadRSAPKCS1v15Verifier(pub *rsa.PublicKey, hashFunc crypto.Hash) (*RSAPKCS1v15Verifier, error) {
+ if pub == nil {
+ return nil, errors.New("invalid RSA public key specified")
+ }
+
+ if !isSupportedAlg(hashFunc, rsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &RSAPKCS1v15Verifier{
+ publicKey: pub,
+ hashFunc: hashFunc,
+ }, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPKCS1v15Verifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.publicKey, nil
+}
+
+// VerifySignature verifies the signature for the given message using PKCS1v15. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the RSAPKCS1v15Verifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (r RSAPKCS1v15Verifier) VerifySignature(signature, message io.Reader, opts ...VerifyOption) error {
+ digest, hf, err := ComputeDigestForVerifying(message, r.hashFunc, rsaSupportedVerifyHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+
+ if signature == nil {
+ return errors.New("nil signature passed to VerifySignature")
+ }
+
+ sigBytes, err := io.ReadAll(signature)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ return rsa.VerifyPKCS1v15(r.publicKey, hf, digest, sigBytes)
+}
+
+// RSAPKCS1v15SignerVerifier is a signature.SignerVerifier that uses the RSA PKCS1v15 algorithm
+type RSAPKCS1v15SignerVerifier struct {
+ *RSAPKCS1v15Signer
+ *RSAPKCS1v15Verifier
+}
+
+// LoadRSAPKCS1v15SignerVerifier creates a combined signer and verifier. This is a convenience object
+// that simply wraps an instance of RSAPKCS1v15Signer and RSAPKCS1v15Verifier.
+func LoadRSAPKCS1v15SignerVerifier(priv *rsa.PrivateKey, hf crypto.Hash) (*RSAPKCS1v15SignerVerifier, error) {
+ signer, err := LoadRSAPKCS1v15Signer(priv, hf)
+ if err != nil {
+ return nil, fmt.Errorf("initializing signer: %w", err)
+ }
+ verifier, err := LoadRSAPKCS1v15Verifier(&priv.PublicKey, hf)
+ if err != nil {
+ return nil, fmt.Errorf("initializing verifier: %w", err)
+ }
+
+ return &RSAPKCS1v15SignerVerifier{
+ RSAPKCS1v15Signer: signer,
+ RSAPKCS1v15Verifier: verifier,
+ }, nil
+}
+
+// NewDefaultRSAPKCS1v15SignerVerifier creates a combined signer and verifier using RSA PKCS1v15.
+// This creates a new RSA key of 2048 bits and uses the SHA256 hashing algorithm.
+func NewDefaultRSAPKCS1v15SignerVerifier() (*RSAPKCS1v15SignerVerifier, *rsa.PrivateKey, error) {
+ return NewRSAPKCS1v15SignerVerifier(rand.Reader, 2048, crypto.SHA256)
+}
+
+// NewRSAPKCS1v15SignerVerifier creates a combined signer and verifier using RSA PKCS1v15.
+// This creates a new RSA key of the specified length of bits, entropy source, and hash function.
+func NewRSAPKCS1v15SignerVerifier(rand io.Reader, bits int, hashFunc crypto.Hash) (*RSAPKCS1v15SignerVerifier, *rsa.PrivateKey, error) {
+ priv, err := rsa.GenerateKey(rand, bits)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sv, err := LoadRSAPKCS1v15SignerVerifier(priv, hashFunc)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return sv, priv, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPKCS1v15SignerVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.publicKey, nil
+}
diff --git a/pkg/signature/rsapkcs1v15_test.go b/pkg/signature/rsapkcs1v15_test.go
new file mode 100644
index 0000000..a4f3c97
--- /dev/null
+++ b/pkg/signature/rsapkcs1v15_test.go
@@ -0,0 +1,66 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/rsa"
+ "encoding/base64"
+ "strings"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+// keys defined in rsapss_test.go
+
+func TestRSAPKCS1v15SignerVerifier(t *testing.T) {
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(rsaKey), cryptoutils.SkipPassword)
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling private key: %v", err)
+ }
+ sv, err := LoadRSAPKCS1v15SignerVerifier(privateKey.(*rsa.PrivateKey), crypto.SHA256)
+ if err != nil {
+ t.Errorf("unexpected error creating signer/verifier: %v", err)
+ }
+
+ message := []byte("sign me")
+ // created with openssl dgst -sign privKey.pem -sha256
+ sig, _ := base64.StdEncoding.DecodeString("AMpSInspjqXdigO0vACd7KMilwLMnrHqnSitnyY0dNiIQ912I2wEme3sMqAMeWnsJ26BxObqV2iMZiggnmeMwd92+6dWpfc2is7m3IbdrUmwKG8y4WDegXEq+EWOy6qsPoqXFPgn1500MFkwrMASP035Gu6wTPmc92zimKozT91j2MNBSONWlcrP89DYBpSVnX+AUs4CKJUppRH/AeyKtftm8GC2TOGrG83U5JqDNegbp5Sji3ViAbUtbiHfob4o1VDGqlyCLgaB0sthekI0XFucWHJj9xRBFazcSBA7Bw1I+T08SqsjfP9Gz43VkItnZbwXMWdSRV81vEK0UuX/rA==")
+ testingSigner(t, sv, "rsa", crypto.SHA256, message)
+ testingVerifier(t, sv, "rsa", crypto.SHA256, sig, message)
+
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(pubKey))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key: %v", err)
+ }
+ v, err := LoadRSAPKCS1v15Verifier(publicKey.(*rsa.PublicKey), crypto.SHA256)
+ if err != nil {
+ t.Errorf("unexpected error creating verifier: %v", err)
+ }
+ testingVerifier(t, v, "rsa", crypto.SHA256, sig, message)
+}
+
+func TestRSAPKCS1v15SignerVerifierUnsupportedHash(t *testing.T) {
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(pubKey))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key: %v", err)
+ }
+ _, err = LoadRSAPKCS1v15Verifier(publicKey.(*rsa.PublicKey), crypto.SHA1)
+ if !strings.Contains(err.Error(), "invalid hash function specified") {
+ t.Errorf("expected error 'invalid hash function specified', got: %v", err.Error())
+ }
+}
diff --git a/pkg/signature/rsapss.go b/pkg/signature/rsapss.go
new file mode 100644
index 0000000..6e52bed
--- /dev/null
+++ b/pkg/signature/rsapss.go
@@ -0,0 +1,260 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// checked on LoadSigner, LoadVerifier, and SignMessage
+var rsaSupportedHashFuncs = []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+}
+
+// checked on VerifySignature. Supports SHA1 verification.
+var rsaSupportedVerifyHashFuncs = []crypto.Hash{
+ crypto.SHA1,
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+}
+
+// RSAPSSSigner is a signature.Signer that uses the RSA PSS algorithm
+type RSAPSSSigner struct {
+ hashFunc crypto.Hash
+ priv *rsa.PrivateKey
+ pssOpts *rsa.PSSOptions
+}
+
+// LoadRSAPSSSigner calculates signatures using the specified private key and hash algorithm.
+//
+// If opts are specified, then they will be stored and used as a default if not overridden
+// by the value passed to Sign().
+//
+// hf must be either SHA256, SHA388, or SHA512. opts.Hash is ignored.
+func LoadRSAPSSSigner(priv *rsa.PrivateKey, hf crypto.Hash, opts *rsa.PSSOptions) (*RSAPSSSigner, error) {
+ if priv == nil {
+ return nil, errors.New("invalid RSA private key specified")
+ }
+
+ if !isSupportedAlg(hf, rsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &RSAPSSSigner{
+ priv: priv,
+ pssOpts: opts,
+ hashFunc: hf,
+ }, nil
+}
+
+// SignMessage signs the provided message using PSS. If the message is provided,
+// this method will compute the digest according to the hash function specified
+// when the RSAPSSSigner was created.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithRand()
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (r RSAPSSSigner) SignMessage(message io.Reader, opts ...SignOption) ([]byte, error) {
+ digest, hf, err := ComputeDigestForSigning(message, r.hashFunc, rsaSupportedHashFuncs, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ rand := selectRandFromOpts(opts...)
+ pssOpts := r.pssOpts
+ if pssOpts == nil {
+ pssOpts = &rsa.PSSOptions{
+ SaltLength: rsa.PSSSaltLengthAuto,
+ }
+ }
+ pssOpts.Hash = hf
+
+ return rsa.SignPSS(rand, r.priv, hf, digest, pssOpts)
+}
+
+// Public returns the public key that can be used to verify signatures created by
+// this signer.
+func (r RSAPSSSigner) Public() crypto.PublicKey {
+ if r.priv == nil {
+ return nil
+ }
+
+ return r.priv.Public()
+}
+
+// PublicKey returns the public key that can be used to verify signatures created by
+// this signer. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPSSSigner) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.Public(), nil
+}
+
+// Sign computes the signature for the specified digest using PSS.
+//
+// If a source of entropy is given in rand, it will be used instead of the default value (rand.Reader
+// from crypto/rand).
+//
+// If opts are specified, they must be *rsa.PSSOptions. If opts are not specified, the hash function
+// provided when the signer was created will be assumed.
+func (r RSAPSSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ rsaOpts := []SignOption{options.WithDigest(digest), options.WithRand(rand)}
+ if opts != nil {
+ rsaOpts = append(rsaOpts, options.WithCryptoSignerOpts(opts))
+ }
+
+ return r.SignMessage(nil, rsaOpts...)
+}
+
+// RSAPSSVerifier is a signature.Verifier that uses the RSA PSS algorithm
+type RSAPSSVerifier struct {
+ publicKey *rsa.PublicKey
+ hashFunc crypto.Hash
+ pssOpts *rsa.PSSOptions
+}
+
+// LoadRSAPSSVerifier verifies signatures using the specified public key and hash algorithm.
+//
+// hf must be either SHA256, SHA388, or SHA512. opts.Hash is ignored.
+func LoadRSAPSSVerifier(pub *rsa.PublicKey, hashFunc crypto.Hash, opts *rsa.PSSOptions) (*RSAPSSVerifier, error) {
+ if pub == nil {
+ return nil, errors.New("invalid RSA public key specified")
+ }
+
+ if !isSupportedAlg(hashFunc, rsaSupportedHashFuncs) {
+ return nil, errors.New("invalid hash function specified")
+ }
+
+ return &RSAPSSVerifier{
+ publicKey: pub,
+ hashFunc: hashFunc,
+ pssOpts: opts,
+ }, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPSSVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.publicKey, nil
+}
+
+// VerifySignature verifies the signature for the given message using PSS. Unless provided
+// in an option, the digest of the message will be computed using the hash function specified
+// when the RSAPSSVerifier was created.
+//
+// This function returns nil if the verification succeeded, and an error message otherwise.
+//
+// This function recognizes the following Options listed in order of preference:
+//
+// - WithDigest()
+//
+// - WithCryptoSignerOpts()
+//
+// All other options are ignored if specified.
+func (r RSAPSSVerifier) VerifySignature(signature, message io.Reader, opts ...VerifyOption) error {
+ digest, hf, err := ComputeDigestForVerifying(message, r.hashFunc, rsaSupportedVerifyHashFuncs, opts...)
+ if err != nil {
+ return err
+ }
+
+ if signature == nil {
+ return errors.New("nil signature passed to VerifySignature")
+ }
+
+ sigBytes, err := io.ReadAll(signature)
+ if err != nil {
+ return fmt.Errorf("reading signature: %w", err)
+ }
+
+ // rsa.VerifyPSS ignores pssOpts.Hash, so we don't set it
+ pssOpts := r.pssOpts
+ if pssOpts == nil {
+ pssOpts = &rsa.PSSOptions{
+ SaltLength: rsa.PSSSaltLengthAuto,
+ }
+ }
+
+ return rsa.VerifyPSS(r.publicKey, hf, digest, sigBytes, pssOpts)
+}
+
+// RSAPSSSignerVerifier is a signature.SignerVerifier that uses the RSA PSS algorithm
+type RSAPSSSignerVerifier struct {
+ *RSAPSSSigner
+ *RSAPSSVerifier
+}
+
+// LoadRSAPSSSignerVerifier creates a combined signer and verifier using RSA PSS. This is
+// a convenience object that simply wraps an instance of RSAPSSSigner and RSAPSSVerifier.
+func LoadRSAPSSSignerVerifier(priv *rsa.PrivateKey, hf crypto.Hash, opts *rsa.PSSOptions) (*RSAPSSSignerVerifier, error) {
+ signer, err := LoadRSAPSSSigner(priv, hf, opts)
+ if err != nil {
+ return nil, fmt.Errorf("initializing signer: %w", err)
+ }
+ verifier, err := LoadRSAPSSVerifier(&priv.PublicKey, hf, opts)
+ if err != nil {
+ return nil, fmt.Errorf("initializing verifier: %w", err)
+ }
+
+ return &RSAPSSSignerVerifier{
+ RSAPSSSigner: signer,
+ RSAPSSVerifier: verifier,
+ }, nil
+}
+
+// NewDefaultRSAPSSSignerVerifier creates a combined signer and verifier using RSA PSS.
+// This creates a new RSA key of 2048 bits and uses the SHA256 hashing algorithm.
+func NewDefaultRSAPSSSignerVerifier() (*RSAPSSSignerVerifier, *rsa.PrivateKey, error) {
+ return NewRSAPSSSignerVerifier(rand.Reader, 2048, crypto.SHA256)
+}
+
+// NewRSAPSSSignerVerifier creates a combined signer and verifier using RSA PSS.
+// This creates a new RSA key of the specified length of bits, entropy source, and hash function.
+func NewRSAPSSSignerVerifier(rand io.Reader, bits int, hashFunc crypto.Hash) (*RSAPSSSignerVerifier, *rsa.PrivateKey, error) {
+ priv, err := rsa.GenerateKey(rand, bits)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sv, err := LoadRSAPSSSignerVerifier(priv, hashFunc, &rsa.PSSOptions{Hash: hashFunc})
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return sv, priv, nil
+}
+
+// PublicKey returns the public key that is used to verify signatures by
+// this verifier. As this value is held in memory, all options provided in arguments
+// to this method are ignored.
+func (r RSAPSSSignerVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+ return r.publicKey, nil
+}
diff --git a/pkg/signature/rsapss_test.go b/pkg/signature/rsapss_test.go
new file mode 100644
index 0000000..f42a584
--- /dev/null
+++ b/pkg/signature/rsapss_test.go
@@ -0,0 +1,123 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/rsa"
+ "encoding/base64"
+ "strings"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+const rsaKey = `-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfCoj9PKxSIpOB
+jVvP7B0l8Q6KXgwSxEBIobMl11nrH2Fv6ufZRWgma7E3rZcjRMygyfia6SB8KBjq
+OBMHnxX78tp5IDxbPWniA7GGTWZyBsXgfLFH7GVGBh8fiJJtfL4TP/xmMzY47rx8
+qvglkQDktdmSEmvfYmof5SIXD/CBI9YDxpXQB9EBcd16QnjwHUKHElOs4lZI9OeP
+8TSV8tWyskq1cO4LxPS8WZVTvbq0jp84OwQTpWtJqG/DUQ1QfMjfixt+uauCDA87
+iIwBC+rC7aCfaXHpqNayHzToUi2Jc34O6LMyfHgowEjQgnKehClY4Vuy0aJXQvKB
+mRDqyjO/AgMBAAECggEBAIHOAs3Gis8+WjRSjXVjh882DG1QsJwXZQYgPT+vpiAl
+YjKdNpOHRkbd9ARgXY5kEuccxDd7p7E6MM3XFpQf7M51ltpZfWboRgAIgD+WOiHw
+eSbdytr95C6tj11twTJBH+naGk1sTokxv7aaVdKfIjL49oeBexBFmVe4pW9gkmrE
+1z1y1a0RohqbZ0kprYPWjz5UhsNqbCzgkdDqS7IrcOwVg6zvKYFjHnqIHqaJXVif
+FgIfoNt7tz+12FTHI+6OkKoN3YCJueaxneBhITXm6RLOpQWa9qhdUPbkJ9vQNfph
+Qqke4faaxKY9UDma+GpEHR016AWufZp92pd9wQkDn0kCgYEA7w/ZizAkefHoZhZ8
+Isn/fYu4fdtUaVgrnGUVZobiGxWrHRU9ikbAwR7UwbgRSfppGiJdAMq1lyH2irmb
+4OHU64rjuYSlIqUWHLQHWmqUbLUvlDojH/vdmH/Zn0AbrLZaimC5UCjK3Eb7sAMq
+G0tGeDX2JraQvx7KrbC6peTaaaMCgYEA7tgZBiRCQJ7+mNu+gX9x6OXtjsDCh516
+vToRLkxWc7LAbC9LKsuEHl4e3vy1PY/nyuv12Ng2dBq4WDXozAmVgz0ok7rRlIFp
+w8Yj8o/9KuGZkD/7tw/pLsVc9Q3Wf0ACrnAAh7+3dAvn3yg+WHwXzqWIbrseDPt9
+ILCfUoNDpzUCgYAKFCX8y0PObFd67lm/cbq2xUw66iNN6ay1BEH5t5gSwkAbksis
+ar03pyAbJrJ75vXFZ0t6fBFZ1NG7GYYr3fmHEKz3JlN7+W/MN/7TXgjx6FWgLy9J
+6ul1w3YeU6qXBn0ctmU5ru6WiNuVmRyOWAcZjFTbXvkNRbQPzJKh6dsXdwKBgA1D
+FIihxMf/zBVCxl48bF/JPJqbm3GaTfFp4wBWHsrH1yVqrtrOeCSTh1VMZOfpMK60
+0W7b+pIR1cCYJbgGpDWoVLN3QSHk2bGUM/TJB/60jilTVC/DA2ikbtfwj8N7E2sK
+Lw1amN4ptxNOEcAqC8xepqe3XiDMahNBm2cigMQtAoGBAKwrXvss2BKz+/6poJQU
+A0c7jhMN8M9Y5S2Ockw07lrQeAgfu4q+/8ztm0NeHJbk01IJvJY5Nt7bSgwgNVlo
+j7vR2BMAc9U73Ju9aeTl/L6GqmZyA+Ojhl5gA5DPZYqNiqi93ydgRaI6n4+o3dI7
+5wnr40AmbuKCDvMOvN7nMybL
+-----END PRIVATE KEY-----`
+
+// Extracted from the certificate using:
+// openssl x509 -pubkey -noout -in test.crt
+const pubKey = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wqI/TysUiKTgY1bz+wd
+JfEOil4MEsRASKGzJddZ6x9hb+rn2UVoJmuxN62XI0TMoMn4mukgfCgY6jgTB58V
++/LaeSA8Wz1p4gOxhk1mcgbF4HyxR+xlRgYfH4iSbXy+Ez/8ZjM2OO68fKr4JZEA
+5LXZkhJr32JqH+UiFw/wgSPWA8aV0AfRAXHdekJ48B1ChxJTrOJWSPTnj/E0lfLV
+srJKtXDuC8T0vFmVU726tI6fODsEE6VrSahvw1ENUHzI34sbfrmrggwPO4iMAQvq
+wu2gn2lx6ajWsh806FItiXN+DuizMnx4KMBI0IJynoQpWOFbstGiV0LygZkQ6soz
+vwIDAQAB
+-----END PUBLIC KEY-----`
+
+func TestRSAPSSSignerVerifier(t *testing.T) {
+ opts := &rsa.PSSOptions{
+ Hash: crypto.SHA256,
+ }
+
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(rsaKey), cryptoutils.SkipPassword)
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling private key: %v", err)
+ }
+ sv, err := LoadRSAPSSSignerVerifier(privateKey.(*rsa.PrivateKey), crypto.SHA256, opts)
+ if err != nil {
+ t.Errorf("unexpected error creating signer/verifier: %v", err)
+ }
+
+ message := []byte("sign me")
+ // created with openssl dgst -sign privKey.pem -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 -sha256
+ sig, _ := base64.StdEncoding.DecodeString("UyouJxmgAKdm/Qfi9YA7aK71/eqyLcytmDN8CQqSCgcbGSln7S5fgIAmrwUfGp1tcxKjuNjLScn11+fqawiG9y66740VEC6GfS1hgElC2k3i/v8ly2mlt+4JYs3euzYxtWnxwQr4csc7Jy2V2cjoeQm6GTxkR4E6TRJM8/UxXvjKtp3rxRD8OuyfuGFkI0lU48vjKLgbuZKQqQdWuNUOnsPvtrHxvGRY/F1C0Ig3b7SoTyAjWSXQG42faKsFT+W1L/UdRK+m73TYdxMleI4uIGtl0k0Weui1/gK7Uh2FUP5+/F1ZoQRYk/DMz0M4QPmPsYLGwc8oduoF6JvNMGKymg==")
+ testingSigner(t, sv, "rsa", crypto.SHA256, message)
+ testingVerifier(t, sv, "rsa", crypto.SHA256, sig, message)
+
+ // test with nil opts (sane defaults)
+ sv, err = LoadRSAPSSSignerVerifier(privateKey.(*rsa.PrivateKey), crypto.SHA256, nil)
+ if err != nil {
+ t.Errorf("unexpected error creating signer/verifier: %v", err)
+ }
+ testingSigner(t, sv, "rsa", crypto.SHA256, message)
+ testingVerifier(t, sv, "rsa", crypto.SHA256, sig, message)
+
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(pubKey))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key: %v", err)
+ }
+ v, err := LoadRSAPSSVerifier(publicKey.(*rsa.PublicKey), crypto.SHA256, opts)
+ if err != nil {
+ t.Errorf("unexpected error creating verifier: %v", err)
+ }
+ testingVerifier(t, v, "rsa", crypto.SHA256, sig, message)
+
+ v, err = LoadRSAPSSVerifier(publicKey.(*rsa.PublicKey), crypto.SHA256, nil)
+ if err != nil {
+ t.Errorf("unexpected error creating verifier with nil opts: %v", err)
+ }
+ testingVerifier(t, v, "rsa", crypto.SHA256, sig, message)
+}
+
+func TestRSAPSSSignerVerifierUnsupportedHash(t *testing.T) {
+ publicKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(pubKey))
+ if err != nil {
+ t.Errorf("unexpected error unmarshalling public key: %v", err)
+ }
+ _, err = LoadRSAPSSVerifier(publicKey.(*rsa.PublicKey), crypto.SHA1, nil)
+ if !strings.Contains(err.Error(), "invalid hash function specified") {
+ t.Errorf("expected error 'invalid hash function specified', got: %v", err.Error())
+ }
+}
diff --git a/pkg/signature/signer.go b/pkg/signature/signer.go
new file mode 100644
index 0000000..3bd3823
--- /dev/null
+++ b/pkg/signature/signer.go
@@ -0,0 +1,89 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+
+ // these ensure we have the implementations loaded
+ _ "crypto/sha256"
+ _ "crypto/sha512"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+
+ // these ensure we have the implementations loaded
+ _ "golang.org/x/crypto/sha3"
+)
+
+// Signer creates digital signatures over a message using a specified key pair
+type Signer interface {
+ PublicKeyProvider
+ SignMessage(message io.Reader, opts ...SignOption) ([]byte, error)
+}
+
+// SignerOpts implements crypto.SignerOpts but also allows callers to specify
+// additional options that may be utilized in signing the digest provided.
+type SignerOpts struct {
+ Hash crypto.Hash
+ Opts []SignOption
+}
+
+// HashFunc returns the hash function for this object
+func (s SignerOpts) HashFunc() crypto.Hash {
+ return s.Hash
+}
+
+// LoadSigner returns a signature.Signer based on the algorithm of the private key
+// provided.
+//
+// If privateKey is an RSA key, a RSAPKCS1v15Signer will be returned. If a
+// RSAPSSSigner is desired instead, use the LoadRSAPSSSigner() method directly.
+func LoadSigner(privateKey crypto.PrivateKey, hashFunc crypto.Hash) (Signer, error) {
+ switch pk := privateKey.(type) {
+ case *rsa.PrivateKey:
+ return LoadRSAPKCS1v15Signer(pk, hashFunc)
+ case *ecdsa.PrivateKey:
+ return LoadECDSASigner(pk, hashFunc)
+ case ed25519.PrivateKey:
+ return LoadED25519Signer(pk)
+ }
+ return nil, errors.New("unsupported public key type")
+}
+
+// LoadSignerFromPEMFile returns a signature.Signer based on the algorithm of the private key
+// in the file. The Signer will use the hash function specified when computing digests.
+//
+// If key is an RSA key, a RSAPKCS1v15Signer will be returned. If a
+// RSAPSSSigner is desired instead, use the LoadRSAPSSSigner() and
+// cryptoutils.UnmarshalPEMToPrivateKey() methods directly.
+func LoadSignerFromPEMFile(path string, hashFunc crypto.Hash, pf cryptoutils.PassFunc) (Signer, error) {
+ fileBytes, err := os.ReadFile(filepath.Clean(path))
+ if err != nil {
+ return nil, err
+ }
+ priv, err := cryptoutils.UnmarshalPEMToPrivateKey(fileBytes, pf)
+ if err != nil {
+ return nil, err
+ }
+ return LoadSigner(priv, hashFunc)
+}
diff --git a/pkg/signature/signerverifier.go b/pkg/signature/signerverifier.go
new file mode 100644
index 0000000..90667f2
--- /dev/null
+++ b/pkg/signature/signerverifier.go
@@ -0,0 +1,69 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "errors"
+ "os"
+ "path/filepath"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+// SignerVerifier creates and verifies digital signatures over a message using a specified key pair
+type SignerVerifier interface {
+ Signer
+ Verifier
+}
+
+// LoadSignerVerifier returns a signature.SignerVerifier based on the algorithm of the private key
+// provided.
+//
+// If privateKey is an RSA key, a RSAPKCS1v15SignerVerifier will be returned. If a
+// RSAPSSSignerVerifier is desired instead, use the LoadRSAPSSSignerVerifier() method directly.
+func LoadSignerVerifier(privateKey crypto.PrivateKey, hashFunc crypto.Hash) (SignerVerifier, error) {
+ switch pk := privateKey.(type) {
+ case *rsa.PrivateKey:
+ return LoadRSAPKCS1v15SignerVerifier(pk, hashFunc)
+ case *ecdsa.PrivateKey:
+ return LoadECDSASignerVerifier(pk, hashFunc)
+ case ed25519.PrivateKey:
+ return LoadED25519SignerVerifier(pk)
+ }
+ return nil, errors.New("unsupported public key type")
+}
+
+// LoadSignerVerifierFromPEMFile returns a signature.SignerVerifier based on the algorithm of the private key
+// in the file. The SignerVerifier will use the hash function specified when computing digests.
+//
+// If publicKey is an RSA key, a RSAPKCS1v15SignerVerifier will be returned. If a
+// RSAPSSSignerVerifier is desired instead, use the LoadRSAPSSSignerVerifier() and
+// cryptoutils.UnmarshalPEMToPrivateKey() methods directly.
+func LoadSignerVerifierFromPEMFile(path string, hashFunc crypto.Hash, pf cryptoutils.PassFunc) (SignerVerifier, error) {
+ fileBytes, err := os.ReadFile(filepath.Clean(path))
+ if err != nil {
+ return nil, err
+ }
+ priv, err := cryptoutils.UnmarshalPEMToPrivateKey(fileBytes, pf)
+ if err != nil {
+ return nil, err
+ }
+ return LoadSignerVerifier(priv, hashFunc)
+}
diff --git a/pkg/signature/ssh/README.md b/pkg/signature/ssh/README.md
new file mode 100644
index 0000000..7d306f3
--- /dev/null
+++ b/pkg/signature/ssh/README.md
@@ -0,0 +1,118 @@
+# SSH File Signatures
+
+SSH keys can be used to sign files!
+Unfortunately this is a pretty recent change to the openssh tooling, so it is not
+supported by golang.org/x/crypto/ssh yet.
+
+This document explains how it works at a high level.
+
+## Keys
+
+SSH keys are usually split into public and private files, named `id_rsa.pub` and
+`id_rsa`, respectively.
+These files are encoded and formatted a little differently than other signing keys.
+
+### Public Keys
+
+These are typically in the "known hosts" format.
+This looks something like:
+
+```
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDw0ZWP4zZLELSJVenQTQsrFJVBnoP64KTg/UWRU6qOb8HEOdtHJDOyTmo9dvN/yJoTFtWAfQEjaTsMVJzTD0gOk6ncTsp0BUtgXawSCfEUiv7v+2VgSVbUfAv/NL+HEGSCdcORnansIyrZaHwAjR3ei3O+pRWvgjRj3pOH1rWGrxaC5IbsELYzS/HvwAG/uwcxgBv4POvaq6eCEHVbqRjIYjjoYsC+c24sgSQxOyXvDS7j2z9TPHPvepDhVr9y6xnnqhLqZEWmidRrbb35aYkVLJxmGTFy/JW1cewyU2Jb3+sKQOiOwL7DAB39tRyec2ed+EHh6QLW4pcMnoXsWuPyi+G595HiUYmIlqXJ5JPo0Cv/rOJrmWSFceWiDjC/SeODp/AcK0EsN/p3wOp6ac7EzAz9Npri0vwSQX4MUYlya/olKiKCx5GIhTZtXioREPd8v4osx2VrVyDxKX99PVVbxw1FXSe4u+PuOawJzUA4vW41mxUY9zoAsb/fvoNPtrrT9HfC+7Pg6ryBdz+445M8Atc8YjjLeYXkTXWD6KMielRzBFFoIwIgi0bMotq3iQ9IwjQSXPMDQLb+UPg8xqsgRsX3wvyZzdBhxO4Bdomv7JYmySysaGgliHktU8qRse1lpDIXMovPtowywcKL4U3seDKrq7saVO0qdsLavy1o0w== lorenc.d@gmail.com
+```
+
+These can be parsed with [ParseKnownHosts](https://pkg.go.dev/golang.org/x/crypto/ssh#ParseKnownHosts)
+, NOT `ParsePublicKey`.
+
+In addition to the key material itself, this can contain the algorithm (`ssh-rsa` here) and a comment
+(lorenc.d@gmail.com) here.
+
+### Private Keys
+
+These are stored in an "armored" PEM format, resembling PGP or x509 keys:
+
+```
+-----BEGIN SSH PRIVATE KEY-----
+<base64 encoded key here>
+-----END SSH PRIVATE KEY-----
+```
+
+These can be parsed correctly with [ParsePrivateKey](https://pkg.go.dev/golang.org/x/crypto/ssh#ParsePrivateKey).
+
+## Wire Format
+
+The wire format is relatively standard.
+
+* Bytes are laid out in order.
+* Fixed-length fields are laid out at the proper offset with the specified length.
+* Strings are stored with the size as a prefix.
+
+## Signature
+
+These can be generated and validated from the command line with the `ssh-keygen -Y` set of commands:
+`sign`, `verify`, and `check-novalidate`.
+
+To work with them in Go is a little tricker.
+The signature is stored using a struct packed using the `openssh` wire format.
+The data that is used in the signing function is also packed in another struct before it is signed.
+
+### Signature Format
+
+Signatures are formatted on disk in a PEM-encoded format.
+The header is `-----BEGIN SSH SIGNATURE-----`, and the end is `-----BEGIN SSH SIGNATURE-----`.
+The signature contents are base64-encoded.
+
+The signature contents are wrapped with extra metadata, then encoded as a struct using the
+`openssh` wire format.
+That struct is defined [here](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L34).
+
+In Go:
+
+```
+type wrappedSig struct {
+ MagicHeader [6]byte
+ Version uint32
+ PublicKey string
+ Namespace string
+ Reserved string
+ HashAlgorithm string
+ Signature string
+}
+```
+
+The `PublicKey` and `Signature` fields are also stored as openssh-wire-formatted structs.
+The `MagicHeader` is `SSHSIG`.
+The `Version` is 1.
+The `Namespace` is `file` (for this use-case).
+`Reserved` must be empty.
+
+Go can already parse the `PublicKey` and `Signature` fields,
+and the `Signature` struct contains a `Blob` with the signature data.
+
+### Signed Message
+
+In addition to these wrappers, the message to be signed is wrapped with some metadata before
+it is passed to the signing function.
+
+That wrapper is defined [here](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L81).
+
+And in Go:
+
+```
+type messageWrapper struct {
+ Namespace string
+ Reserved string
+ HashAlgorithm string
+ Hash string
+}
+```
+
+So, the data must first be hashed, then packed in this struct and encoded in the
+openssh wire format.
+Then, this resulting data is signed using the desired signature function.
+
+The `Namespace` field must be `file` (for this usecase).
+The `Reserved` field must be empty.
+
+The output of this signature function (and the hash) becomes the `Signature.Blob`
+value, which gets wire-encoded, wrapped, wire-encoded and finally pem-encoded.
diff --git a/pkg/signature/ssh/armor.go b/pkg/signature/ssh/armor.go
new file mode 100644
index 0000000..1212f1d
--- /dev/null
+++ b/pkg/signature/ssh/armor.go
@@ -0,0 +1,104 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package ssh implements signing with SSH keys
+package ssh
+
+import (
+ "encoding/pem"
+ "errors"
+ "fmt"
+
+ "golang.org/x/crypto/ssh"
+)
+
+const (
+ namespace = "file"
+ pemType = "SSH SIGNATURE"
+)
+
+// Signature encapsulates an SSH Signature object for verification.
+type Signature struct {
+ signature *ssh.Signature
+ pk ssh.PublicKey
+ hashAlg string
+}
+
+// Armor generates a PEM armored signature block.
+func Armor(s *ssh.Signature, p ssh.PublicKey) []byte {
+ sig := wrappedSig{
+ Version: 1,
+ PublicKey: string(p.Marshal()),
+ Namespace: namespace,
+ HashAlgorithm: defaultHashAlgorithm,
+ Signature: string(ssh.Marshal(s)),
+ }
+
+ copy(sig.MagicHeader[:], magicHeader)
+
+ enc := pem.EncodeToMemory(&pem.Block{
+ Type: pemType,
+ Bytes: ssh.Marshal(sig),
+ })
+ return enc
+}
+
+// Decode parses a PEM armored signature block.
+func Decode(b []byte) (*Signature, error) {
+ pemBlock, _ := pem.Decode(b)
+ if pemBlock == nil {
+ return nil, errors.New("unable to decode pem file")
+ }
+
+ if pemBlock.Type != pemType {
+ return nil, fmt.Errorf("wrong pem block type: %s. Expected SSH-SIGNATURE", pemBlock.Type)
+ }
+
+ // Now we unmarshal it into the Signature block
+ sig := wrappedSig{}
+ if err := ssh.Unmarshal(pemBlock.Bytes, &sig); err != nil {
+ return nil, err
+ }
+
+ if sig.Version != 1 {
+ return nil, fmt.Errorf("unsupported signature version: %d", sig.Version)
+ }
+ if string(sig.MagicHeader[:]) != magicHeader {
+ return nil, fmt.Errorf("invalid magic header: %s", sig.MagicHeader[:])
+ }
+ if sig.Namespace != "file" {
+ return nil, fmt.Errorf("invalid signature namespace: %s", sig.Namespace)
+ }
+ if _, ok := supportedHashAlgorithms[sig.HashAlgorithm]; !ok {
+ return nil, fmt.Errorf("unsupported hash algorithm: %s", sig.HashAlgorithm)
+ }
+
+ // Now we can unpack the Signature and PublicKey blocks
+ sshSig := ssh.Signature{}
+ if err := ssh.Unmarshal([]byte(sig.Signature), &sshSig); err != nil {
+ return nil, err
+ }
+
+ pk, err := ssh.ParsePublicKey([]byte(sig.PublicKey))
+ if err != nil {
+ return nil, err
+ }
+
+ return &Signature{
+ signature: &sshSig,
+ pk: pk,
+ hashAlg: sig.HashAlgorithm,
+ }, nil
+}
diff --git a/pkg/signature/ssh/doc.go b/pkg/signature/ssh/doc.go
new file mode 100644
index 0000000..7bb26b8
--- /dev/null
+++ b/pkg/signature/ssh/doc.go
@@ -0,0 +1,17 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package ssh contains types and utilities related to SSH signatures.
+package ssh
diff --git a/pkg/signature/ssh/sign.go b/pkg/signature/ssh/sign.go
new file mode 100644
index 0000000..a2f3f92
--- /dev/null
+++ b/pkg/signature/ssh/sign.go
@@ -0,0 +1,134 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/sha512"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "golang.org/x/crypto/ssh"
+)
+
+// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L81
+type messageWrapper struct {
+ Namespace string
+ Reserved string
+ HashAlgorithm string
+ Hash string
+}
+
+// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L34
+type wrappedSig struct {
+ MagicHeader [6]byte
+ Version uint32
+ PublicKey string
+ Namespace string
+ Reserved string
+ HashAlgorithm string
+ Signature string
+}
+
+const (
+ magicHeader = "SSHSIG"
+ defaultHashAlgorithm = "sha512"
+)
+
+var supportedHashAlgorithms = map[string]func() hash.Hash{
+ "sha256": sha256.New,
+ "sha512": sha512.New,
+}
+
+func sign(s ssh.AlgorithmSigner, m io.Reader) (*ssh.Signature, error) {
+ hf := sha512.New()
+ if _, err := io.Copy(hf, m); err != nil {
+ return nil, err
+ }
+ mh := hf.Sum(nil)
+
+ sp := messageWrapper{
+ Namespace: "file",
+ HashAlgorithm: defaultHashAlgorithm,
+ Hash: string(mh),
+ }
+
+ dataMessageWrapper := ssh.Marshal(sp)
+ dataMessageWrapper = append([]byte(magicHeader), dataMessageWrapper...)
+
+ // ssh-rsa is not supported for RSA keys:
+ // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L71
+ // We can use the default value of "" for other key types though.
+ algo := ""
+ if s.PublicKey().Type() == ssh.KeyAlgoRSA {
+ algo = ssh.KeyAlgoRSASHA512
+ }
+ sig, err := s.SignWithAlgorithm(rand.Reader, dataMessageWrapper, algo)
+ if err != nil {
+ return nil, err
+ }
+ return sig, nil
+}
+
+// Signer implements signature.Signer for SSH keys.
+type Signer struct {
+ signer ssh.AlgorithmSigner
+}
+
+// PublicKey returns the public key for a Signer.
+func (s *Signer) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
+ return s.signer.PublicKey(), nil
+}
+
+// SignMessage signs the supplied message.
+func (s *Signer) SignMessage(message io.Reader, _ ...signature.SignOption) ([]byte, error) {
+ b, err := io.ReadAll(message)
+ if err != nil {
+ return nil, err
+ }
+ sig, err := s.signer.Sign(rand.Reader, b)
+ if err != nil {
+ return nil, err
+ }
+ return Armor(sig, s.signer.PublicKey()), nil
+}
+
+var _ signature.Signer = (*Signer)(nil)
+
+// Sign signs the supplied message with the private key.
+func Sign(sshPrivateKey string, data io.Reader) ([]byte, error) {
+ s, err := ssh.ParsePrivateKey([]byte(sshPrivateKey))
+ if err != nil {
+ return nil, err
+ }
+
+ as, ok := s.(ssh.AlgorithmSigner)
+ if !ok {
+ return nil, fmt.Errorf("private key %T is not a ssh.AlgorithmSigner", s)
+ }
+
+ sig, err := sign(as, data)
+ if err != nil {
+ return nil, err
+ }
+
+ armored := Armor(sig, s.PublicKey())
+ return armored, nil
+}
diff --git a/pkg/signature/ssh/sign_test.go b/pkg/signature/ssh/sign_test.go
new file mode 100644
index 0000000..8103927
--- /dev/null
+++ b/pkg/signature/ssh/sign_test.go
@@ -0,0 +1,353 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+ "bytes"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "golang.org/x/crypto/ssh"
+)
+
+var (
+ // Generated with "ssh-keygen -C test@rekor.dev -f id_rsa"
+ sshPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEA16H5ImoRO7mr41r8Z8JFBdu6jIM+6XU8M0r9F81RuhLYqzr9zw1n
+LeGCqFxPXNBKm8ZyH2BCsBHsbXbwe85IMHM3SUh8X/9fI0Lpi5/xbqAproFUpNR+UJYv6s
+8AaWk5zpN1rmpBrqGFJfGQKJCioDiiwNGmSdVkUNmQmYIANxJMDWYmNe8vUOh6nYEHB+lz
+fGgDAAzVSXTACW994UkSY47AD05swU4rIT/JWA6BkUrEhO//F0QQhFeROCPJiPRhJXGcFf
+9SicffJqR/ELzM1zNYnRXMD0bbdTUwDrIcIFFNBbtcfJVOUUCGumSlt+qjUC7y8cvwbHAu
+wf5nS6baA7P6LfTYplF2XIAkdWtkN6O1ouoyIHICXMlddDW2vNaJeEXTeKjx51WSM7qPnQ
+ZKsBtwjLQeEY/OPkIvu88lNNYSD63qMUA12msohjwVFCIgJVvYLIrkViczZ7t3L7lgy1X0
+CJI4e1roOfM/r9jTieyDHchEYpZYcw3L1R2qtePlAAAFiHdJQKl3SUCpAAAAB3NzaC1yc2
+EAAAGBANeh+SJqETu5q+Na/GfCRQXbuoyDPul1PDNK/RfNUboS2Ks6/c8NZy3hgqhcT1zQ
+SpvGch9gQrAR7G128HvOSDBzN0lIfF//XyNC6Yuf8W6gKa6BVKTUflCWL+rPAGlpOc6Tda
+5qQa6hhSXxkCiQoqA4osDRpknVZFDZkJmCADcSTA1mJjXvL1Doep2BBwfpc3xoAwAM1Ul0
+wAlvfeFJEmOOwA9ObMFOKyE/yVgOgZFKxITv/xdEEIRXkTgjyYj0YSVxnBX/UonH3yakfx
+C8zNczWJ0VzA9G23U1MA6yHCBRTQW7XHyVTlFAhrpkpbfqo1Au8vHL8GxwLsH+Z0um2gOz
++i302KZRdlyAJHVrZDejtaLqMiByAlzJXXQ1trzWiXhF03io8edVkjO6j50GSrAbcIy0Hh
+GPzj5CL7vPJTTWEg+t6jFANdprKIY8FRQiICVb2CyK5FYnM2e7dy+5YMtV9AiSOHta6Dnz
+P6/Y04nsgx3IRGKWWHMNy9UdqrXj5QAAAAMBAAEAAAGAJyaOcFQnuttUPRxY9ZHNLGofrc
+Fqm8KgYoO7/iVWMF2Zn0U/rec2E5t9OIpCEozy7uOR9uZoVUV70sgkk6X5b2qL4C9b/aYF
+JQbSFnq8wCQuTTPIJYE7SfBq1Mwuu/TR/RLC7B74u/cxkJkSXnscO9Dso+ussH0hEJjf6y
+8yUM1up4Qjbel2gs8i7BPwLdySDkVoPgsWcpbTAyOODGhTAWZ6soy/rD1AEXJeYTGJDtMv
+aR+WBihig1TO1g2RWt9bqqiG7PIlljd3ZsjSSU5y3t6ZN/8j5keKD032EtxbZB0WFD3Ar4
+FbFwlW+urb2MQ0JyNKOio3nhdjolXYkJa+C6LXdaaml/8BhMR1eLoMe8nS45w76o8mdJWX
+wsirB8tvjCLY0QBXgGv/1DTsKu/wEFCW2/Y0e50gF7pHAlYFNmKDcgI9OyORRYhFbV4D82
+fI8JLQ42ZJkS/0t6xQma8WC88pbHGEuVSB6CE/p25fyYRX+UPTQ79tWFvLV4kNQAaBAAAA
+wEvyd6H8ePyBXImg8JzGxthufB0eXSfZBrabjf6e6bR2ivpJsHmB64gbMkV6MFV7EWYX1B
+wYPQxf4gA2Ez7aJvDtfE7uV6pa0WJS3hW1+be8DHEftmLSbTy/TEvDujNb2gqoi7uWQXWJ
+yYWZlYO65r1a6HucryQ8+78fTuTRbZALO43vNGz0oXH1hPSddkcbNAhZTsD0rQKNwqVTe5
+wl+6Cduy/CQwjHLYrY73MyWy1Vh1LXhAdGMPnWZwGIu/dnkgAAAMEA9KuaoGnfnLQkrjeR
+tO4RCRS2quNRvm4L6i4vHgTDsYtoSlR1ujge7SGOOmIPS4XVjZN5zzCOA7+EDVnuz3WWmx
+hmkjpG1YxzmJGaWoYdeo3a6UgJtisfMp8eUKqjJT1mhsCliCWtaOQNRoQieDQmgwZzSX/v
+ZiGsOIKa6cR37eKvOJSjVrHsAUzdtYrmi8P2gvAUFWyzXobAtpzHcWrwWkOEIm04G0OGXb
+J46hfIX3f45E5EKXvFzexGgVOD2I7hAAAAwQDhniYAizfW9YfG7UJWekkl42xMP7Cb8b0W
+SindSIuE8bFTukV1yxbmNZp/f0pKvn/DWc2n0I0bwSGZpy8BCY46RKKB2DYQavY/tGcC1N
+AynKuvbtWs11A0mTXmq3WwHVXQDozMwJ2nnHpm0UHspPuHqkYpurlP+xoFsocaQ9QwITyp
+lL4qHtXBEzaT8okkcGZBHdSx3gk4TzCsEDOP7ZZPLq42lpKMK10zFPTMd0maXtJDYKU/b4
+gAATvvPoylyYUAAAAOdGVzdEByZWtvci5kZXYBAgMEBQ==
+-----END OPENSSH PRIVATE KEY-----
+`
+ sshPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXofkiahE7uavjWvxnwkUF27qMgz7pdTwzSv0XzVG6EtirOv3PDWct4YKoXE9c0EqbxnIfYEKwEextdvB7zkgwczdJSHxf/18jQumLn/FuoCmugVSk1H5Qli/qzwBpaTnOk3WuakGuoYUl8ZAokKKgOKLA0aZJ1WRQ2ZCZggA3EkwNZiY17y9Q6HqdgQcH6XN8aAMADNVJdMAJb33hSRJjjsAPTmzBTishP8lYDoGRSsSE7/8XRBCEV5E4I8mI9GElcZwV/1KJx98mpH8QvMzXM1idFcwPRtt1NTAOshwgUU0Fu1x8lU5RQIa6ZKW36qNQLvLxy/BscC7B/mdLptoDs/ot9NimUXZcgCR1a2Q3o7Wi6jIgcgJcyV10Nba81ol4RdN4qPHnVZIzuo+dBkqwG3CMtB4Rj84+Qi+7zyU01hIPreoxQDXaayiGPBUUIiAlW9gsiuRWJzNnu3cvuWDLVfQIkjh7Wug58z+v2NOJ7IMdyERillhzDcvVHaq14+U= test@rekor.dev
+`
+ // Generated with "ssh-keygen -C other-test@rekor.dev -f id_rsa"
+ otherSSHPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAw/WCSWC9TEvCQOwO+T68EvNa3OSIv1Y0+sT8uSvyjPyEO0+p0t8C
+g/zy67vOxiQpU5jN6MItjXAjMmeCm8GKMt6gk+cDoaAev/ZfjuzSL7RayExpmhBleh2X3G
+KLkkXF9ABFNchlTqSLOZiEjDoNpbFv16KT1sE6CqW8DjxXQkQk9JK65hLH+BxeWMNCEJVa
+Cma4X04aJmC7zJAi5yGeeT0SKVqMohavF90O6XiYFCQHuwXPPyHfocqgudmXnozz+6D6ax
+JKZMwQsNp3WKumOjlzWnxBCCB1l2jN6Rag8aJ2277iMFXRwjTL/8jaEsW4KkysDf0GjV2/
+iqbr0q5b0arDYbv7CrGBR+uH0wGz/Zog1x5iZANObhZULpDrLVJidEMc27HXBb7PMsNDy7
+BGYRB1yc0d0y83p8mUqvOlWSArxn1WnAZO04pAgTrclrhEh4ZXOkn2Sn82eu3DpQ8inkol
+Y4IfnhIfbOIeemoUNq1tOUquhow9GLRM6INieHLBAAAFkPPnA1jz5wNYAAAAB3NzaC1yc2
+EAAAGBAMP1gklgvUxLwkDsDvk+vBLzWtzkiL9WNPrE/Lkr8oz8hDtPqdLfAoP88uu7zsYk
+KVOYzejCLY1wIzJngpvBijLeoJPnA6GgHr/2X47s0i+0WshMaZoQZXodl9xii5JFxfQART
+XIZU6kizmYhIw6DaWxb9eik9bBOgqlvA48V0JEJPSSuuYSx/gcXljDQhCVWgpmuF9OGiZg
+u8yQIuchnnk9EilajKIWrxfdDul4mBQkB7sFzz8h36HKoLnZl56M8/ug+msSSmTMELDad1
+irpjo5c1p8QQggdZdozekWoPGidtu+4jBV0cI0y//I2hLFuCpMrA39Bo1dv4qm69KuW9Gq
+w2G7+wqxgUfrh9MBs/2aINceYmQDTm4WVC6Q6y1SYnRDHNux1wW+zzLDQ8uwRmEQdcnNHd
+MvN6fJlKrzpVkgK8Z9VpwGTtOKQIE63Ja4RIeGVzpJ9kp/Nnrtw6UPIp5KJWOCH54SH2zi
+HnpqFDatbTlKroaMPRi0TOiDYnhywQAAAAMBAAEAAAGAYycx4oEhp55Zz1HijblxnsEmQ8
+kbbH1pV04fdm7HTxFis0Qu8PVIp5JxNFiWWunnQ1Z5MgI23G9WT+XST4+RpwXBCLWGv9xu
+UsGOPpqUC/FdUiZf9MXBIxYgRjJS3xORA1KzsnAQ2sclb2I+B1pEl4d9yQWJesvQ25xa2H
+Utzej/LgWkrk/ogSGRl6ZNImj/421wc0DouGyP+gUgtATt0/jT3LrlmAqUVCXVqssLYH2O
+r9JTuGUibBJEW2W/c0lsM0jaHa5bGAdL3nhDuF1Q6KFB87mZoNw8c2znYoTzQ3FyWtIEZI
+V/9oWrkS7V6242SKSR9tJoEzK0jtrKC/FZwBiI4hPcwoqY6fZbT1701i/n50xWEfEUOLVm
+d6VqNKyAbIaZIPN0qfZuD+xdrHuM3V6k/rgFxGl4XTrp/N4AsruiQs0nRQKNTw3fHE0zPq
+UTxSeMvjywRCepxhBFCNh8NHydapclHtEPEGdTVHohL3krJehstPO/IuRyKLfSVtL1AAAA
+wQCmGA8k+uW6mway9J3jp8mlMhhp3DCX6DAcvalbA/S5OcqMyiTM3c/HD5OJ6OYFDldcqu
+MPEgLRL2HfxL29LsbQSzjyOIrfp5PLJlo70P5lXS8u2QPbo4/KQJmQmsIX18LDyU2zRtNA
+C2WfBiHSZV+guLhmHms9S5gQYKt2T5OnY/W0tmnInx9lmFCMC+XKS1iSQ2o433IrtCPQJp
+IXZd59OQpO9QjJABgJIDtXxFIXt45qpXduDPJuggrhg81stOwAAADBAPX73u/CY+QUPts+
+LV185Z4mZ2y+qu2ZMCAU3BnpHktGZZ1vFN1Xq9o8KdnuPZ+QJRdO8eKMWpySqrIdIbTYLm
+9nXmVH0uNECIEAvdU+wgKeR+BSHxCRVuTF4YSygmNadgH/z+oRWLgOblGo2ywFBoXsIAKQ
+paNu1MFGRUmhz67+dcpkkBUDRU9loAgBKexMo8D9vkR0YiHLOUjCrtmEZRNm0YRZt0gQhD
+ZSD1fOH0fZDcCVNpGP2zqAKos4EGLnkwAAAMEAy/AuLtPKA2u9oCA8e18ZnuQRAi27FBVU
+rU2D7bMg1eS0IakG8v0gE9K6WdYzyArY1RoKB3ZklK5VmJ1cOcWc2x3Ejc5jcJgc8cC6lZ
+wwjpE8HfWL1kIIYgPdcexqFc+l6MdgH6QMKU3nLg1LsM4v5FEldtk/2dmnw620xnFfstpF
+VxSZNdKrYfM/v9o6sRaDRqSfH1dG8BvkUxPznTAF+JDxBENcKXYECcq9f6dcl1w5IEnNTD
+Wry/EKQvgvOUjbAAAAFG90aGVyLXRlc3RAcmVrb3IuZGV2AQIDBAUG
+-----END OPENSSH PRIVATE KEY-----
+`
+ otherSSHPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDD9YJJYL1MS8JA7A75PrwS81rc5Ii/VjT6xPy5K/KM/IQ7T6nS3wKD/PLru87GJClTmM3owi2NcCMyZ4KbwYoy3qCT5wOhoB6/9l+O7NIvtFrITGmaEGV6HZfcYouSRcX0AEU1yGVOpIs5mISMOg2lsW/XopPWwToKpbwOPFdCRCT0krrmEsf4HF5Yw0IQlVoKZrhfThomYLvMkCLnIZ55PRIpWoyiFq8X3Q7peJgUJAe7Bc8/Id+hyqC52ZeejPP7oPprEkpkzBCw2ndYq6Y6OXNafEEIIHWXaM3pFqDxonbbvuIwVdHCNMv/yNoSxbgqTKwN/QaNXb+KpuvSrlvRqsNhu/sKsYFH64fTAbP9miDXHmJkA05uFlQukOstUmJ0QxzbsdcFvs8yw0PLsEZhEHXJzR3TLzenyZSq86VZICvGfVacBk7TikCBOtyWuESHhlc6SfZKfzZ67cOlDyKeSiVjgh+eEh9s4h56ahQ2rW05Sq6GjD0YtEzog2J4csE= other-test@rekor.dev
+`
+
+ // Generated with ssh-keygen -C test@rekor.dev -t ed25519 -f id_ed25519
+ ed25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBB45zRHxPPFtabwS3Vd6Lb9vMe+tIHZj2qN5VQ+bgLfQAAAJgyRa3cMkWt
+3AAAAAtzc2gtZWQyNTUxOQAAACBB45zRHxPPFtabwS3Vd6Lb9vMe+tIHZj2qN5VQ+bgLfQ
+AAAED7y4N/DsVnRQiBZNxEWdsJ9RmbranvtQ3X9jnb6gFed0HjnNEfE88W1pvBLdV3otv2
+8x760gdmPao3lVD5uAt9AAAADnRlc3RAcmVrb3IuZGV2AQIDBAUGBw==
+-----END OPENSSH PRIVATE KEY-----
+`
+ ed25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9 test@rekor.dev
+`
+)
+
+func TestFromOpenSSH(t *testing.T) {
+ otherPub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(otherSSHPublicKey))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshPublicKey))
+ // if err != nil {
+ // t.Fatal(err)
+ // }
+ for _, tt := range []struct {
+ name string
+ pub string
+ priv string
+ }{
+ {
+ name: "rsa",
+ pub: sshPublicKey,
+ priv: sshPrivateKey,
+ },
+ {
+ name: "ed25519",
+ pub: ed25519PublicKey,
+ priv: ed25519PrivateKey,
+ },
+ } {
+ if _, err := exec.LookPath("ssh-keygen"); err != nil {
+ t.Skip("skip TestFromOpenSSH: missing ssh-keygen in PATH")
+ }
+ t.Run(tt.name, func(t *testing.T) {
+ tt := tt
+
+ // Test that a signature from the cli can validate here.
+ td := t.TempDir()
+
+ data := []byte("hello, ssh world")
+ dataPath := write(t, data, td, "data")
+
+ privPath := write(t, []byte(tt.priv), td, "id")
+ write(t, []byte(tt.pub), td, "id.pub")
+
+ sigPath := dataPath + ".sig"
+ run(t, nil, "ssh-keygen", "-Y", "sign", "-n", "file", "-f", privPath, dataPath)
+
+ sigBytes, err := os.ReadFile(sigPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(tt.pub))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := Verify(bytes.NewReader(data), sigBytes, pub); err != nil {
+ t.Error(err)
+ }
+
+ // It should not verify if we check against another public key
+ if err := Verify(bytes.NewReader(data), sigBytes, otherPub); err == nil {
+ t.Error("expected error with incorrect key")
+ }
+
+ // It should not verify if the data is tampered
+ if err := Verify(strings.NewReader("bad data"), sigBytes, pub); err == nil {
+ t.Error("expected error with incorrect data")
+ }
+ })
+ }
+}
+
+func TestToOpenSSH(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ pub string
+ priv string
+ }{
+ {
+ name: "rsa",
+ pub: sshPublicKey,
+ priv: sshPrivateKey,
+ },
+ {
+ name: "ed25519",
+ pub: ed25519PublicKey,
+ priv: ed25519PrivateKey,
+ },
+ } {
+ if _, err := exec.LookPath("ssh-keygen"); err != nil {
+ t.Skip("skip TestToOpenSSH: missing ssh-keygen in PATH")
+ }
+ t.Run(tt.name, func(t *testing.T) {
+ tt := tt
+ // Test that a signature from here can validate in the CLI.
+ td := t.TempDir()
+
+ data := []byte("hello, ssh world")
+ write(t, data, td, "data")
+
+ armored, err := Sign(tt.priv, bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sigPath := write(t, armored, td, "oursig")
+
+ // Create an allowed_signers file with two keys to check against.
+ allowedSigner := "test@rekor.dev " + tt.pub + "\n"
+ allowedSigner += "othertest@rekor.dev " + otherSSHPublicKey + "\n"
+ allowedSigners := write(t, []byte(allowedSigner), td, "allowed_signer")
+
+ // We use the correct principal here so it should work.
+ run(t, data, "ssh-keygen", "-Y", "verify", "-f", allowedSigners,
+ "-I", "test@rekor.dev", "-n", "file", "-s", sigPath)
+
+ // Just to be sure, check against the other public key as well.
+ runErr(t, data, "ssh-keygen", "-Y", "verify", "-f", allowedSigners,
+ "-I", "othertest@rekor.dev", "-n", "file", "-s", sigPath)
+
+ // It should error if we run it against other data
+ data = []byte("other data!")
+ runErr(t, data, "ssh-keygen", "-Y", "check-novalidate", "-n", "file", "-s", sigPath)
+ })
+ }
+}
+
+func TestRoundTrip(t *testing.T) {
+ data := []byte("my good data to be signed!")
+
+ // Create one extra signature for all the tests.
+ otherSig, err := Sign(otherSSHPrivateKey, bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ otherPub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(otherSSHPublicKey))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, tt := range []struct {
+ name string
+ pub string
+ priv string
+ }{
+ {
+ name: "rsa",
+ pub: sshPublicKey,
+ priv: sshPrivateKey,
+ },
+ {
+ name: "ed25519",
+ pub: ed25519PublicKey,
+ priv: ed25519PrivateKey,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ tt := tt
+ sig, err := Sign(tt.priv, bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(tt.pub))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Check the signature against that data and public key
+ if err := Verify(bytes.NewReader(data), sig, pub); err != nil {
+ t.Error(err)
+ }
+
+ // Now check it against invalid data.
+ if err := Verify(strings.NewReader("invalid data!"), sig, pub); err == nil {
+ t.Error("expected error!")
+ }
+
+ // Now check it against the wrong key.
+ if err := Verify(bytes.NewReader(data), sig, otherPub); err == nil {
+ t.Error("expected error!")
+ }
+
+ // Now check it against an invalid signature data.
+ if err := Verify(bytes.NewReader(data), []byte("invalid signature!"), pub); err == nil {
+ t.Error("expected error!")
+ }
+
+ // Once more, use the wrong signature and check it against the original (wrong public key)
+ if err := Verify(bytes.NewReader(data), otherSig, pub); err == nil {
+ t.Error("expected error!")
+ }
+ // It should work against the correct public key.
+ if err := Verify(bytes.NewReader(data), otherSig, otherPub); err != nil {
+ t.Error(err)
+ }
+ })
+ }
+}
+
+func write(t *testing.T, d []byte, fp ...string) string {
+ p := filepath.Join(fp...)
+ if err := os.WriteFile(p, d, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ return p
+}
+
+func run(t *testing.T, stdin []byte, args ...string) {
+ t.Helper()
+ /* #nosec */
+ cmd := exec.Command(args[0], args[1:]...)
+ cmd.Stdin = bytes.NewReader(stdin)
+ out, err := cmd.CombinedOutput()
+ t.Logf("cmd %v: %s", cmd, string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func runErr(t *testing.T, stdin []byte, args ...string) {
+ t.Helper()
+ /* #nosec */
+ cmd := exec.Command(args[0], args[1:]...)
+ cmd.Stdin = bytes.NewReader(stdin)
+ out, err := cmd.CombinedOutput()
+ t.Logf("cmd %v: %s", cmd, string(out))
+ if err == nil {
+ t.Fatal("expected error")
+ }
+}
diff --git a/pkg/signature/ssh/verify.go b/pkg/signature/ssh/verify.go
new file mode 100644
index 0000000..6603e8c
--- /dev/null
+++ b/pkg/signature/ssh/verify.go
@@ -0,0 +1,58 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+ "io"
+
+ "github.com/sigstore/sigstore/pkg/signature"
+ "golang.org/x/crypto/ssh"
+)
+
+// Verify verifies the supplied signature against the specified key.
+func Verify(message io.Reader, armoredSignature []byte, pubKey ssh.PublicKey) error {
+ decodedSignature, err := Decode(armoredSignature)
+ if err != nil {
+ return err
+ }
+
+ // Hash the message so we can verify it against the signature.
+ h := supportedHashAlgorithms[decodedSignature.hashAlg]()
+ if _, err := io.Copy(h, message); err != nil {
+ return err
+ }
+ hm := h.Sum(nil)
+
+ toVerify := messageWrapper{
+ Namespace: "file",
+ HashAlgorithm: decodedSignature.hashAlg,
+ Hash: string(hm),
+ }
+ signedMessage := ssh.Marshal(toVerify)
+ signedMessage = append([]byte(magicHeader), signedMessage...)
+ return pubKey.Verify(signedMessage, decodedSignature.signature)
+}
+
+var _ signature.Verifier = (*Signer)(nil)
+
+// VerifySignature verifies a suppled signature.
+func (s *Signer) VerifySignature(signature, message io.Reader, _ ...signature.VerifyOption) error {
+ b, err := io.ReadAll(signature)
+ if err != nil {
+ return err
+ }
+ return Verify(message, b, s.signer.PublicKey())
+}
diff --git a/pkg/signature/sv_test.go b/pkg/signature/sv_test.go
new file mode 100644
index 0000000..93799aa
--- /dev/null
+++ b/pkg/signature/sv_test.go
@@ -0,0 +1,173 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ crand "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "strings"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// Per golangci-lint `hashFunc` always receives `crypto.SHA256`
+func testingSigner(t *testing.T, s Signer, alg string, hashFunc crypto.Hash, message []byte) { // nolint: unparam
+ t.Helper()
+
+ isED25519 := alg == "ed25519"
+
+ var digest []byte
+ if !isED25519 {
+ hasher := hashFunc.New()
+ _, _ = hasher.Write(message)
+ digest = hasher.Sum(nil)
+ }
+
+ if _, err := s.SignMessage(nil); err == nil {
+ t.Error("didn't error out for nil message")
+ }
+
+ // if nil is passed for rand, default (crypto/rand.Reader) should be used
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithRand(nil)); err != nil && !isED25519 {
+ t.Errorf("unexpected error passing nil Rand: %v", err)
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithRand(crand.Reader)); err != nil {
+ t.Errorf("unexpected error passing valid Rand: %v", err)
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithRand(crand.Reader), options.WithDigest(digest)); err != nil {
+ t.Errorf("unexpected error passing valid Rand and Digest: %v", err)
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithRand(crand.Reader), options.WithDigest(digest), options.WithCryptoSignerOpts(hashFunc)); err != nil {
+ t.Errorf("unexpected error passing valid Rand and Digest and Opts: %v", err)
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithDigest(digest)); err != nil {
+ t.Errorf("unexpected error passing valid Digest: %v", err)
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithDigest(digest), options.WithCryptoSignerOpts(crypto.Hash(0))); err == nil && !isED25519 {
+ t.Error("no error passing invalid opts")
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithDigest(digest), options.WithCryptoSignerOpts(crypto.SHA512)); err == nil && !isED25519 {
+ t.Error("no error passing mismatched Digest and opts")
+ }
+
+ if _, err := s.SignMessage(bytes.NewReader(message), options.WithCryptoSignerOpts(nil)); err != nil {
+ t.Errorf("unexpected error passing nil options: %v", err)
+ }
+
+ cs, ok := s.(crypto.Signer)
+ if !ok {
+ t.Fatalf("expected crypto.Signer")
+ }
+
+ if _, err := cs.Sign(nil, nil, nil); err == nil {
+ t.Errorf("no error passing nil for all args to Sign: %v", err)
+ }
+
+ if isED25519 {
+ if _, err := cs.Sign(nil, message, crypto.Hash(0)); err != nil {
+ t.Errorf("unexpected error passing nil Rand, message and crypto.Hash(0) to Sign: %v", err)
+ }
+ }
+
+ if _, err := cs.Sign(nil, digest, nil); err != nil && !isED25519 {
+ t.Errorf("unexpected error passing nil for Rand and Opts to Sign: %v", err)
+ }
+
+ if _, err := cs.Sign(nil, digest, &rsa.PSSOptions{Hash: hashFunc}); err != nil && !isED25519 {
+ t.Errorf("unexpected error passing nil for Rand and valid Opts to Sign: %v", err)
+ }
+
+ if _, err := cs.Sign(crand.Reader, digest, &rsa.PSSOptions{Hash: hashFunc}); err != nil && !isED25519 {
+ t.Errorf("unexpected error passing valid Rand and valid Opts to Sign: %v", err)
+ }
+
+ if _, err := cs.Sign(crand.Reader, digest, nil); err != nil && !isED25519 {
+ t.Errorf("unexpected error passing valid Rand and nil Opts to Sign: %v", err)
+ }
+
+ if k := cs.Public(); k == nil {
+ t.Error("Public() returned empty key")
+ }
+
+ if k, err := s.PublicKey(); k == nil || err != nil {
+ t.Errorf("PublicKey() returned empty key or err: %v", err)
+ }
+
+ if k, err := s.PublicKey(options.WithContext(context.Background())); k == nil || err != nil {
+ t.Errorf("PublicKey(context.Background()) returned empty key or err: %v", err)
+ }
+}
+
+func assertPublicKeyIsx509Marshalable(t *testing.T, pub crypto.PublicKey) {
+ t.Helper()
+
+ if _, err := x509.MarshalPKIXPublicKey(pub); err != nil {
+ t.Errorf("x509.MarshalPKIXPublicKey(%T) returned error: %v", pub, err)
+ }
+}
+
+// Per golangci-lint `hashFunc` always receives `crypto.SHA256`
+func testingVerifier(t *testing.T, v Verifier, alg string, hashFunc crypto.Hash, signature, message []byte) { // nolint: unparam
+ t.Helper()
+
+ isED25519 := alg == "ed25519"
+
+ var digest []byte
+ if !isED25519 {
+ hasher := hashFunc.New()
+ _, _ = hasher.Write(message)
+ digest = hasher.Sum(nil)
+ }
+
+ if err := v.VerifySignature(bytes.NewReader(signature), nil); err == nil {
+ t.Error("no error when passing nil as message")
+ }
+
+ if err := v.VerifySignature(nil, bytes.NewReader(message)); err == nil {
+ t.Error("no error when passing nil as signature")
+ }
+
+ if err := v.VerifySignature(strings.NewReader("not the sig"), bytes.NewReader(message)); err == nil {
+ t.Error("no error when passing incorrect signature")
+ }
+
+ if err := v.VerifySignature(bytes.NewReader(signature), bytes.NewReader(message)); err != nil {
+ t.Errorf("unexpected error when verifying valid bytes.NewReader(message): %v", err)
+ }
+
+ if err := v.VerifySignature(bytes.NewReader(signature), bytes.NewReader(message), options.WithDigest(digest)); err != nil {
+ t.Errorf("unexpected error when using valid bytes.NewReader(message) with digest: %v", err)
+ }
+
+ if err := v.VerifySignature(bytes.NewReader(signature), bytes.NewReader(message), options.WithDigest(digest), options.WithCryptoSignerOpts(hashFunc)); err != nil {
+ t.Errorf("unexpected error when using valid bytes.NewReader(message) with digest & opts: %v", err)
+ }
+
+ if err := v.VerifySignature(bytes.NewReader(signature), bytes.NewReader(message), options.WithDigest(digest), options.WithCryptoSignerOpts(crypto.SHA512)); err == nil && !isED25519 {
+ t.Error("no error when using mismatched hashFunc with digest & opts")
+ }
+}
diff --git a/pkg/signature/util.go b/pkg/signature/util.go
new file mode 100644
index 0000000..8852ecc
--- /dev/null
+++ b/pkg/signature/util.go
@@ -0,0 +1,55 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+
+ sigpayload "github.com/sigstore/sigstore/pkg/signature/payload"
+)
+
+// SignImage signs a container manifest using the specified signer object
+func SignImage(signer SignerVerifier, image name.Digest, optionalAnnotations map[string]interface{}) (payload, signature []byte, err error) {
+ imgPayload := sigpayload.Cosign{
+ Image: image,
+ Annotations: optionalAnnotations,
+ }
+ payload, err = json.Marshal(imgPayload)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal payload to JSON: %w", err)
+ }
+ signature, err = signer.SignMessage(bytes.NewReader(payload))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to sign payload: %w", err)
+ }
+ return payload, signature, nil
+}
+
+// VerifyImageSignature verifies a signature over a container manifest
+func VerifyImageSignature(signer SignerVerifier, payload, signature []byte) (image name.Digest, annotations map[string]interface{}, err error) {
+ if err := signer.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload)); err != nil {
+ return name.Digest{}, nil, fmt.Errorf("signature verification failed: %w", err)
+ }
+ var imgPayload sigpayload.Cosign
+ if err := json.Unmarshal(payload, &imgPayload); err != nil {
+ return name.Digest{}, nil, fmt.Errorf("could not deserialize image payload: %w", err)
+ }
+ return imgPayload.Image, imgPayload.Annotations, nil
+}
diff --git a/pkg/signature/util_test.go b/pkg/signature/util_test.go
new file mode 100644
index 0000000..c64e08b
--- /dev/null
+++ b/pkg/signature/util_test.go
@@ -0,0 +1,91 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "testing"
+
+ "github.com/go-test/deep"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+const validDigest = "sha256:d34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33fd34db33f"
+
+func mustParseDigest(t *testing.T, digestStr string) name.Digest {
+ t.Helper()
+ digest, err := name.NewDigest(digestStr)
+ if err != nil {
+ t.Fatalf("could not parse digest %q: %v", digestStr, err)
+ }
+ return digest
+}
+
+func TestProviderRoundtrip(t *testing.T) {
+ ecdsaSV, _, err := NewDefaultECDSASignerVerifier()
+ if err != nil {
+ t.Fatalf("Could not generate ecdsa SignerVerifier for test: %v", err)
+ }
+ rsaSV, _, err := NewDefaultRSAPSSSignerVerifier()
+ if err != nil {
+ t.Fatalf("Could not generate rsa SignerVerifier for test: %v", err)
+ }
+
+ testCases := []struct {
+ desc string
+ sv SignerVerifier
+ digest name.Digest
+ claims map[string]interface{}
+ }{
+ {
+ desc: "ECDSA",
+ sv: ecdsaSV,
+ digest: mustParseDigest(t, "example.com/ecdsa@"+validDigest),
+ claims: map[string]interface{}{
+ "creator": "ECDSA",
+ "optional": "extras",
+ },
+ },
+ {
+ desc: "RSA",
+ sv: rsaSV,
+ digest: mustParseDigest(t, "example.com/rsa@"+validDigest),
+ claims: map[string]interface{}{
+ "creator": "RSA",
+ "Floaty McFloatface": 6.022e23,
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ payload, sig, err := SignImage(tc.sv, tc.digest, tc.claims)
+ if err != nil {
+ t.Fatalf("SignImage returned error: %v", err)
+ }
+
+ rtDigest, rtClaims, err := VerifyImageSignature(tc.sv, payload, sig)
+ if err != nil {
+ t.Fatalf("VerifyImageSignature returned error: %v", err)
+ }
+ if tc.digest.Name() != rtDigest.Name() {
+ t.Errorf("got digest %q, wanted %q", rtDigest.Name(), tc.digest.Name())
+ }
+ if diff := deep.Equal(tc.claims, rtClaims); diff != nil {
+ t.Errorf("claims were altered during the roundtrip: %v", diff)
+ }
+ })
+ }
+}
diff --git a/pkg/signature/verifier.go b/pkg/signature/verifier.go
new file mode 100644
index 0000000..9ca6049
--- /dev/null
+++ b/pkg/signature/verifier.go
@@ -0,0 +1,100 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+// Verifier verifies the digital signature using a specified public key
+type Verifier interface {
+ PublicKeyProvider
+ VerifySignature(signature, message io.Reader, opts ...VerifyOption) error
+}
+
+// LoadVerifier returns a signature.Verifier based on the algorithm of the public key
+// provided that will use the hash function specified when computing digests.
+//
+// If publicKey is an RSA key, a RSAPKCS1v15Verifier will be returned. If a
+// RSAPSSVerifier is desired instead, use the LoadRSAPSSVerifier() method directly.
+func LoadVerifier(publicKey crypto.PublicKey, hashFunc crypto.Hash) (Verifier, error) {
+ switch pk := publicKey.(type) {
+ case *rsa.PublicKey:
+ return LoadRSAPKCS1v15Verifier(pk, hashFunc)
+ case *ecdsa.PublicKey:
+ return LoadECDSAVerifier(pk, hashFunc)
+ case ed25519.PublicKey:
+ return LoadED25519Verifier(pk)
+ }
+ return nil, errors.New("unsupported public key type")
+}
+
+// LoadUnsafeVerifier returns a signature.Verifier based on the algorithm of the public key
+// provided that will use SHA1 when computing digests for RSA and ECDSA signatures.
+//
+// If publicKey is an RSA key, a RSAPKCS1v15Verifier will be returned. If a
+// RSAPSSVerifier is desired instead, use the LoadRSAPSSVerifier() method directly.
+func LoadUnsafeVerifier(publicKey crypto.PublicKey) (Verifier, error) {
+ switch pk := publicKey.(type) {
+ case *rsa.PublicKey:
+ if pk == nil {
+ return nil, errors.New("invalid RSA public key specified")
+ }
+ return &RSAPKCS1v15Verifier{
+ publicKey: pk,
+ hashFunc: crypto.SHA1,
+ }, nil
+ case *ecdsa.PublicKey:
+ if pk == nil {
+ return nil, errors.New("invalid ECDSA public key specified")
+ }
+ return &ECDSAVerifier{
+ publicKey: pk,
+ hashFunc: crypto.SHA1,
+ }, nil
+ case ed25519.PublicKey:
+ return LoadED25519Verifier(pk)
+ }
+ return nil, errors.New("unsupported public key type")
+}
+
+// LoadVerifierFromPEMFile returns a signature.Verifier based on the contents of a
+// file located at path. The Verifier wil use the hash function specified when computing digests.
+//
+// If the publickey is an RSA key, a RSAPKCS1v15Verifier will be returned. If a
+// RSAPSSVerifier is desired instead, use the LoadRSAPSSVerifier() and cryptoutils.UnmarshalPEMToPublicKey() methods directly.
+func LoadVerifierFromPEMFile(path string, hashFunc crypto.Hash) (Verifier, error) {
+ fileBytes, err := os.ReadFile(filepath.Clean(path))
+ if err != nil {
+ return nil, err
+ }
+
+ pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(fileBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return LoadVerifier(pubKey, hashFunc)
+}
diff --git a/pkg/signature/verifier_test.go b/pkg/signature/verifier_test.go
new file mode 100644
index 0000000..2440996
--- /dev/null
+++ b/pkg/signature/verifier_test.go
@@ -0,0 +1,36 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "testing"
+)
+
+func TestLoadUnsafeVerifier(t *testing.T) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("unexpected error generating key: %v", err)
+ }
+ verifier, err := LoadUnsafeVerifier(key.Public())
+ if err != nil {
+ t.Fatalf("unexpected error loading verifier: %v", err)
+ }
+ pubKey, _ := verifier.PublicKey()
+ if !key.PublicKey.Equal(pubKey) {
+ t.Fatalf("public keys were not equal")
+ }
+}
diff --git a/pkg/tuf/client.go b/pkg/tuf/client.go
new file mode 100644
index 0000000..c652a52
--- /dev/null
+++ b/pkg/tuf/client.go
@@ -0,0 +1,742 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/theupdateframework/go-tuf/client"
+ tuf_leveldbstore "github.com/theupdateframework/go-tuf/client/leveldbstore"
+ "github.com/theupdateframework/go-tuf/data"
+ "github.com/theupdateframework/go-tuf/util"
+)
+
+const (
+ // DefaultRemoteRoot is the default remote TUF root location.
+ DefaultRemoteRoot = "https://tuf-repo-cdn.sigstore.dev"
+ // defaultRemoteGCSBucket is the name of the GCS bucket that holds sigstore's public good production TUF root
+ defaultRemoteGCSBucket = "sigstore-tuf-root"
+ // defaultRemoteRootNoCDN is the URL of the GCS HTTP endpoint for the DefaultRootGCSBucket content
+ defaultRemoteRootNoCDN = "https://sigstore-tuf-root.storage.googleapis.com"
+ // defaultRemoteRootNoCDNAlt is an alternate URL to the GCS HTTP endpoint for the DefaultRootGCSBucket content
+ defaultRemoteRootNoCDNAlt = "https://storage.googleapis.com/sigstore-tuf-root"
+
+ // TufRootEnv is the name of the environment variable that locates an alternate local TUF root location.
+ TufRootEnv = "TUF_ROOT"
+
+ // SigstoreNoCache is the name of the environment variable that, if set, configures this code to only store root data in memory.
+ SigstoreNoCache = "SIGSTORE_NO_CACHE"
+)
+
+var (
+ // singletonTUF holds a single instance of TUF that will get reused on
+ // subsequent invocations of initializeTUF
+ singletonTUF *TUF
+ singletonTUFOnce = new(sync.Once)
+ singletonTUFErr error
+
+ // initMu locks concurrent calls to initializeTUF
+ initMu sync.Mutex
+)
+
+// getRemoteRoot is a var for testing.
+var getRemoteRoot = func() string { return DefaultRemoteRoot }
+
+type TUF struct {
+ sync.Mutex
+ client *client.Client
+ targets targetImpl
+ local client.LocalStore
+ remote client.RemoteStore
+ embedded fs.FS
+ mirror string // location of mirror
+}
+
+// Mirror returns the mirror configured; note if the object was configured with a legacy reference
+// to the GCS HTTP endpoint for sigstore's public good trust root, this will return DefaultRemoteRoot
+// which is a CDN fronting that DefaultRemoteGCSBucket
+func (t *TUF) Mirror() string {
+ switch t.mirror {
+ case defaultRemoteGCSBucket, defaultRemoteRootNoCDN, defaultRemoteRootNoCDNAlt:
+ return DefaultRemoteRoot
+ default:
+ return t.mirror
+ }
+}
+
+// JSON output representing the configured root status
+type RootStatus struct {
+ Local string `json:"local"`
+ Remote string `json:"remote"`
+ Metadata map[string]MetadataStatus `json:"metadata"`
+ Targets []string `json:"targets"`
+}
+
+type MetadataStatus struct {
+ Version int `json:"version"`
+ Size int `json:"len"`
+ Expiration string `json:"expiration"`
+ Error string `json:"error"`
+}
+
+type TargetFile struct {
+ Target []byte
+ Status StatusKind
+}
+
+type customMetadata struct {
+ Usage UsageKind `json:"usage"`
+ Status StatusKind `json:"status"`
+}
+
+type sigstoreCustomMetadata struct {
+ Sigstore customMetadata `json:"sigstore"`
+}
+
+type signedMeta struct {
+ Type string `json:"_type"`
+ Expires time.Time `json:"expires"`
+ Version int64 `json:"version"`
+}
+
+// RemoteCache contains information to cache on the location of the remote
+// repository.
+type remoteCache struct {
+ Mirror string `json:"mirror"`
+}
+
+func resetForTests() {
+ singletonTUFOnce = new(sync.Once)
+}
+
+func getExpiration(metadata []byte) (*time.Time, error) {
+ s := &data.Signed{}
+ if err := json.Unmarshal(metadata, s); err != nil {
+ return nil, err
+ }
+ sm := &signedMeta{}
+ if err := json.Unmarshal(s.Signed, sm); err != nil {
+ return nil, err
+ }
+ return &sm.Expires, nil
+}
+
+func getVersion(metadata []byte) (int64, error) {
+ s := &data.Signed{}
+ if err := json.Unmarshal(metadata, s); err != nil {
+ return 0, err
+ }
+ sm := &signedMeta{}
+ if err := json.Unmarshal(s.Signed, sm); err != nil {
+ return 0, err
+ }
+ return sm.Version, nil
+}
+
+var isExpiredTimestamp = func(metadata []byte) bool {
+ expiration, err := getExpiration(metadata)
+ if err != nil {
+ return true
+ }
+ return time.Until(*expiration) <= 0
+}
+
+func getMetadataStatus(b []byte) (*MetadataStatus, error) {
+ expires, err := getExpiration(b)
+ if err != nil {
+ return nil, err
+ }
+ version, err := getVersion(b)
+ if err != nil {
+ return nil, err
+ }
+ return &MetadataStatus{
+ Size: len(b),
+ Expiration: expires.Format(time.RFC822),
+ Version: int(version),
+ }, nil
+}
+
+func (t *TUF) getRootStatus() (*RootStatus, error) {
+ local := rootCacheDir()
+ if noCache() {
+ local = "in-memory"
+ }
+ status := &RootStatus{
+ Local: local,
+ Remote: t.Mirror(),
+ Metadata: make(map[string]MetadataStatus),
+ Targets: []string{},
+ }
+
+ // Get targets
+ targets, err := t.client.Targets()
+ if err != nil {
+ return nil, err
+ }
+ for t := range targets {
+ status.Targets = append(status.Targets, t)
+ }
+
+ // Get metadata expiration
+ trustedMeta, err := t.local.GetMeta()
+ if err != nil {
+ return nil, fmt.Errorf("getting trusted meta: %w", err)
+ }
+ for role, md := range trustedMeta {
+ mdStatus, err := getMetadataStatus(md)
+ if err != nil {
+ status.Metadata[role] = MetadataStatus{Error: err.Error()}
+ continue
+ }
+ status.Metadata[role] = *mdStatus
+ }
+
+ return status, nil
+}
+
+func getRoot(meta map[string]json.RawMessage, fallback fs.FS) (json.RawMessage, error) {
+ if trustedRoot, ok := meta["root.json"]; ok {
+ return trustedRoot, nil
+ }
+ // On first initialize, there will be no root in the TUF DB, so read from embedded.
+ rd, ok := fallback.(fs.ReadFileFS)
+ if !ok {
+ return nil, errors.New("fs.ReadFileFS unimplemented for embedded repo")
+ }
+ trustedRoot, err := rd.ReadFile(path.Join("repository", "root.json"))
+ if err != nil {
+ return nil, err
+ }
+ return trustedRoot, nil
+}
+
+// GetRootStatus gets the current root status for info logging
+func GetRootStatus(ctx context.Context) (*RootStatus, error) {
+ t, err := NewFromEnv(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return t.getRootStatus()
+}
+
+// initializeTUF creates a TUF client using the following params:
+// * embed: indicates using the embedded metadata and in-memory file updates.
+// When this is false, this uses a filesystem cache.
+// * mirror: provides a reference to a remote GCS or HTTP mirror.
+// * root: provides an external initial root.json. When this is not provided, this
+// defaults to the embedded root.json.
+// * embedded: An embedded filesystem that provides a trusted root and pre-downloaded
+// targets in a targets/ subfolder.
+// * forceUpdate: indicates checking the remote for an update, even when the local
+// timestamp.json is up to date.
+func initializeTUF(mirror string, root []byte, embedded fs.FS, forceUpdate bool) (*TUF, error) {
+ initMu.Lock()
+ defer initMu.Unlock()
+
+ // TODO: If a temporary error occurs for a long-running process, this singleton will
+ // never retry
+ singletonTUFOnce.Do(func() {
+ t := &TUF{
+ mirror: mirror,
+ embedded: embedded,
+ }
+
+ t.targets = newFileImpl()
+ t.local, singletonTUFErr = newLocalStore()
+ if singletonTUFErr != nil {
+ return
+ }
+
+ t.remote, singletonTUFErr = remoteFromMirror(t.Mirror())
+ if singletonTUFErr != nil {
+ return
+ }
+
+ t.client = client.NewClient(t.local, t.remote)
+
+ trustedMeta, err := t.local.GetMeta()
+ if err != nil {
+ singletonTUFErr = fmt.Errorf("getting trusted meta: %w", err)
+ return
+ }
+
+ // If the caller does not supply a root, then either use the root in the local store
+ // or default to the embedded one.
+ if root == nil {
+ root, err = getRoot(trustedMeta, t.embedded)
+ if err != nil {
+ singletonTUFErr = fmt.Errorf("getting trusted root: %w", err)
+ return
+ }
+ }
+
+ if err := t.client.Init(root); err != nil {
+ singletonTUFErr = fmt.Errorf("unable to initialize client, local cache may be corrupt: %w", err)
+ return
+ }
+
+ singletonTUF = t
+ })
+ if singletonTUFErr != nil {
+ return nil, singletonTUFErr
+ }
+
+ trustedMeta, err := singletonTUF.local.GetMeta()
+ if err != nil {
+ return nil, fmt.Errorf("getting trusted meta: %w", err)
+ }
+
+ // We may already have an up-to-date local store! Check to see if it needs to be updated.
+ trustedTimestamp, ok := trustedMeta["timestamp.json"]
+ if ok && !isExpiredTimestamp(trustedTimestamp) && !forceUpdate {
+ // We're golden so stash the TUF object for later use
+ return singletonTUF, nil
+ }
+
+ // Update if local is not populated or out of date.
+ if err := singletonTUF.updateMetadataAndDownloadTargets(); err != nil {
+ return nil, fmt.Errorf("updating local metadata and targets: %w", err)
+ }
+
+ return singletonTUF, nil
+}
+
+// TODO: Remove ctx arg.
+func NewFromEnv(_ context.Context) (*TUF, error) {
+ // Check for the current remote mirror.
+ mirror := getRemoteRoot()
+ b, err := os.ReadFile(cachedRemote(rootCacheDir()))
+ if err == nil {
+ remoteInfo := remoteCache{}
+ if err := json.Unmarshal(b, &remoteInfo); err == nil {
+ mirror = remoteInfo.Mirror
+ }
+ }
+
+ // Initializes a new TUF object from the local cache or defaults.
+ return initializeTUF(mirror, nil, getEmbedded(), false)
+}
+
+func Initialize(_ context.Context, mirror string, root []byte) error {
+ // Initialize the client. Force an update with remote.
+ tuf, err := initializeTUF(mirror, root, getEmbedded(), true)
+ if err != nil {
+ return err
+ }
+
+ // Store the remote for later if we are caching.
+ if !noCache() {
+ remoteInfo := &remoteCache{Mirror: tuf.Mirror()}
+ b, err := json.Marshal(remoteInfo)
+ if err != nil {
+ return err
+ }
+ if err := os.WriteFile(cachedRemote(rootCacheDir()), b, 0o600); err != nil {
+ return fmt.Errorf("storing remote: %w", err)
+ }
+ }
+ return nil
+}
+
+// Checks if the testTarget matches the valid target file metadata.
+func isValidTarget(testTarget []byte, validMeta data.TargetFileMeta) (bool, error) {
+ localMeta, err := util.GenerateTargetFileMeta(
+ bytes.NewReader(testTarget),
+ "sha256", "sha512")
+ if err != nil {
+ return false, err
+ }
+ if err := util.TargetFileMetaEqual(localMeta, validMeta); err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+func (t *TUF) GetTarget(name string) ([]byte, error) {
+ t.Lock()
+ defer t.Unlock()
+ // Get valid target metadata. Does a local verification.
+ validMeta, err := t.client.Target(name)
+ if err != nil {
+ return nil, fmt.Errorf("error verifying local metadata; local cache may be corrupt: %w", err)
+ }
+ targetBytes, err := t.targets.Get(name)
+ if err != nil {
+ return nil, err
+ }
+
+ if valid, err := isValidTarget(targetBytes, validMeta); !valid {
+ return nil, fmt.Errorf("cache contains invalid target; local cache may be corrupt: %w", err)
+ }
+
+ return targetBytes, nil
+}
+
+// Get target files by a custom usage metadata tag. If there are no files found,
+// use the fallback target names to fetch the targets by name.
+func (t *TUF) GetTargetsByMeta(usage UsageKind, fallbacks []string) ([]TargetFile, error) {
+ t.Lock()
+ targets, err := t.client.Targets()
+ t.Unlock()
+ if err != nil {
+ return nil, fmt.Errorf("error getting targets: %w", err)
+ }
+ var matchedTargets []TargetFile
+ for name, targetMeta := range targets {
+ // Skip any targets that do not include custom metadata.
+ if targetMeta.Custom == nil {
+ continue
+ }
+ var scm sigstoreCustomMetadata
+ err := json.Unmarshal(*targetMeta.Custom, &scm)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "**Warning** Custom metadata not configured properly for target %s, skipping target\n", name)
+ continue
+ }
+ if scm.Sigstore.Usage == usage {
+ target, err := t.GetTarget(name)
+ if err != nil {
+ return nil, fmt.Errorf("error getting target %s by usage: %w", name, err)
+ }
+ matchedTargets = append(matchedTargets, TargetFile{Target: target, Status: scm.Sigstore.Status})
+ }
+ }
+ if len(matchedTargets) == 0 {
+ for _, fallback := range fallbacks {
+ target, err := t.GetTarget(fallback)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "**Warning** Missing fallback target %s, skipping\n", fallback)
+ continue
+ }
+ matchedTargets = append(matchedTargets, TargetFile{Target: target, Status: Active})
+ }
+ }
+ if len(matchedTargets) == 0 {
+ return matchedTargets, fmt.Errorf("no matching targets by custom metadata, fallbacks not found: %s", strings.Join(fallbacks, ", "))
+ }
+ return matchedTargets, nil
+}
+
+// updateClient() updates the TUF client and also caches new metadata, if needed.
+func (t *TUF) updateClient() (data.TargetFiles, error) {
+ targets, err := t.client.Update()
+ if err != nil {
+ // Get some extra information for debugging. What was the state of the top-level
+ // metadata on the remote?
+ status := struct {
+ Mirror string `json:"mirror"`
+ Metadata map[string]MetadataStatus `json:"metadata"`
+ }{
+ Mirror: t.Mirror(),
+ Metadata: make(map[string]MetadataStatus),
+ }
+ for _, md := range []string{"root.json", "targets.json", "snapshot.json", "timestamp.json"} {
+ r, _, err := t.remote.GetMeta(md)
+ if err != nil {
+ // May be missing, or failed download.
+ continue
+ }
+ defer r.Close()
+ b, err := io.ReadAll(r)
+ if err != nil {
+ continue
+ }
+ mdStatus, err := getMetadataStatus(b)
+ if err != nil {
+ continue
+ }
+ status.Metadata[md] = *mdStatus
+ }
+ b, innerErr := json.MarshalIndent(status, "", "\t")
+ if innerErr != nil {
+ return nil, innerErr
+ }
+ return nil, fmt.Errorf("error updating to TUF remote mirror: %w\nremote status:%s", err, string(b))
+ }
+ // Success! Cache new metadata, if needed.
+ if noCache() {
+ return targets, nil
+ }
+ // Sync the on-disk cache with the metadata from the in-memory store.
+ tufDB := filepath.FromSlash(filepath.Join(rootCacheDir(), "tuf.db"))
+ diskLocal, err := tuf_leveldbstore.FileLocalStore(tufDB)
+ defer func() {
+ if diskLocal != nil {
+ diskLocal.Close()
+ }
+ }()
+ if err != nil {
+ return nil, fmt.Errorf("creating cached local store: %w", err)
+ }
+ if err := syncLocalMeta(t.local, diskLocal); err != nil {
+ return nil, err
+ }
+ // Return updated targets.
+ return targets, nil
+}
+
+func (t *TUF) updateMetadataAndDownloadTargets() error {
+ // Download updated targets and cache new metadata and targets in ${TUF_ROOT}.
+ // NOTE: This only returns *updated* targets.
+ targetFiles, err := t.updateClient()
+ if err != nil {
+ return err
+ }
+
+ // Download **newly** updated targets.
+ // TODO: Consider lazily downloading these -- be careful with embedded targets if so.
+ for name, targetMeta := range targetFiles {
+ if err := maybeDownloadRemoteTarget(name, targetMeta, t); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+type targetDestination struct {
+ buf *bytes.Buffer
+}
+
+func (t *targetDestination) Write(b []byte) (int, error) {
+ return t.buf.Write(b)
+}
+
+func (t *targetDestination) Delete() error {
+ t.buf = &bytes.Buffer{}
+ return nil
+}
+
+func maybeDownloadRemoteTarget(name string, meta data.TargetFileMeta, t *TUF) error {
+ // If we already have the target locally, don't bother downloading from remote storage.
+ if cachedTarget, err := t.targets.Get(name); err == nil {
+ // If the target we have stored matches the meta, use that.
+ if valid, _ := isValidTarget(cachedTarget, meta); valid {
+ return nil
+ }
+ }
+
+ // Check if we already have the target in the embedded store.
+ w := bytes.Buffer{}
+ rd, ok := t.embedded.(fs.ReadFileFS)
+ if !ok {
+ return errors.New("fs.ReadFileFS unimplemented for embedded repo")
+ }
+ b, err := rd.ReadFile(path.Join("repository", "targets", name))
+
+ if err == nil {
+ // Unfortunately go:embed appears to somehow replace our line endings on windows, we need to switch them back.
+ // It should theoretically be safe to do this everywhere - but the files only seem to get mutated on Windows so
+ // let's only change them back there.
+ if runtime.GOOS == "windows" {
+ b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
+ }
+
+ if valid, _ := isValidTarget(b, meta); valid {
+ if _, err := io.Copy(&w, bytes.NewReader(b)); err != nil {
+ return fmt.Errorf("using embedded target: %w", err)
+ }
+ }
+ }
+
+ // Nope -- no local matching target, go download it.
+ if w.Len() == 0 {
+ dest := targetDestination{buf: &w}
+ if err := t.client.Download(name, &dest); err != nil {
+ return fmt.Errorf("downloading target: %w", err)
+ }
+ }
+
+ // Set the target in the cache.
+ if err := t.targets.Set(name, w.Bytes()); err != nil {
+ return err
+ }
+ return nil
+}
+
+func rootCacheDir() string {
+ rootDir := os.Getenv(TufRootEnv)
+ if rootDir == "" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ home = ""
+ }
+ return filepath.FromSlash(filepath.Join(home, ".sigstore", "root"))
+ }
+ return rootDir
+}
+
+func cachedRemote(cacheRoot string) string {
+ return filepath.FromSlash(filepath.Join(cacheRoot, "remote.json"))
+}
+
+func cachedTargetsDir(cacheRoot string) string {
+ return filepath.FromSlash(filepath.Join(cacheRoot, "targets"))
+}
+
+func syncLocalMeta(from, to client.LocalStore) error {
+ // Copy trusted metadata in the from LocalStore into the to LocalStore.
+ tufLocalStoreMeta, err := from.GetMeta()
+ if err != nil {
+ return fmt.Errorf("getting metadata to sync: %w", err)
+ }
+ for k, v := range tufLocalStoreMeta {
+ if err := to.SetMeta(k, v); err != nil {
+ return fmt.Errorf("syncing local store for metadata %s", k)
+ }
+ }
+ return nil
+}
+
+// Local store implementations
+func newLocalStore() (client.LocalStore, error) {
+ local := client.MemoryLocalStore()
+ if noCache() {
+ return local, nil
+ }
+ // Otherwise populate the in-memory local store with data fetched from the cache.
+ tufDB := filepath.FromSlash(filepath.Join(rootCacheDir(), "tuf.db"))
+ diskLocal, err := tuf_leveldbstore.FileLocalStore(tufDB)
+ defer func() {
+ if diskLocal != nil {
+ diskLocal.Close()
+ }
+ }()
+ if err != nil {
+ return nil, fmt.Errorf("creating cached local store: %w", err)
+ }
+ // Populate the in-memory local store with data fetched from the cache.
+ if err := syncLocalMeta(diskLocal, local); err != nil {
+ return nil, err
+ }
+ return local, nil
+}
+
+//go:embed repository
+var embeddedRootRepo embed.FS
+
+// getEmbedded is a var for testing.
+var getEmbedded = func() fs.FS { return embeddedRootRepo }
+
+// Target Implementations
+type targetImpl interface {
+ Set(string, []byte) error
+ Get(string) ([]byte, error)
+}
+
+func newFileImpl() targetImpl {
+ memTargets := &memoryCache{}
+ if noCache() {
+ return memTargets
+ }
+ // Otherwise use a disk-cache with in-memory cached targets.
+ return &diskCache{
+ base: cachedTargetsDir(rootCacheDir()),
+ memory: memTargets,
+ }
+}
+
+// In-memory cache for targets
+type memoryCache struct {
+ targets map[string][]byte
+}
+
+func (m *memoryCache) Set(p string, b []byte) error {
+ if m.targets == nil {
+ m.targets = map[string][]byte{}
+ }
+ m.targets[p] = b
+ return nil
+}
+
+func (m *memoryCache) Get(p string) ([]byte, error) {
+ if m.targets == nil {
+ return nil, fmt.Errorf("no cached targets available, cannot retrieve %s", p)
+ }
+ b, ok := m.targets[p]
+ if !ok {
+ return nil, fmt.Errorf("missing cached target %s", p)
+ }
+ return b, nil
+}
+
+// On-disk cache for targets
+type diskCache struct {
+ // Base directory for accessing targets.
+ base string
+ // An in-memory map of targets that are kept in sync.
+ memory *memoryCache
+}
+
+func (d *diskCache) Get(p string) ([]byte, error) {
+ // Read from the in-memory cache first.
+ if b, err := d.memory.Get(p); err == nil {
+ return b, nil
+ }
+ fp := filepath.FromSlash(filepath.Join(d.base, p))
+ return os.ReadFile(fp)
+}
+
+func (d *diskCache) Set(p string, b []byte) error {
+ if err := d.memory.Set(p, b); err != nil {
+ return err
+ }
+
+ fp := filepath.FromSlash(filepath.Join(d.base, p))
+ if err := os.MkdirAll(filepath.Dir(fp), 0o700); err != nil {
+ return fmt.Errorf("creating targets dir: %w", err)
+ }
+
+ return os.WriteFile(fp, b, 0o600)
+}
+
+func noCache() bool {
+ b, err := strconv.ParseBool(os.Getenv(SigstoreNoCache))
+ if err != nil {
+ return false
+ }
+ return b
+}
+
+func remoteFromMirror(mirror string) (client.RemoteStore, error) {
+ // This is for compatibility with specifying a GCS bucket remote.
+ u, parseErr := url.ParseRequestURI(mirror)
+ if parseErr != nil {
+ return client.HTTPRemoteStore(fmt.Sprintf("https://%s.storage.googleapis.com", mirror), nil, nil)
+ }
+ if u.Scheme != "file" {
+ return client.HTTPRemoteStore(mirror, nil, nil)
+ }
+ // Use local filesystem for remote.
+ return client.NewFileRemoteStore(os.DirFS(u.Path), "")
+}
diff --git a/pkg/tuf/client_test.go b/pkg/tuf/client_test.go
new file mode 100644
index 0000000..0ead4b5
--- /dev/null
+++ b/pkg/tuf/client_test.go
@@ -0,0 +1,887 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "sync"
+ "testing"
+ "testing/fstest"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/theupdateframework/go-tuf"
+ "github.com/theupdateframework/go-tuf/data"
+ "github.com/theupdateframework/go-tuf/verify"
+)
+
+// These are the expected targets from the Sigstore root.
+// This may not be the total complete list to ensure that targets
+// can be added during new ceremonies.
+var targets = []string{
+ "artifact.pub",
+ "fulcio_v1.crt.pem",
+ "ctfe.pub",
+ "rekor.pub",
+ "ctfe_2022.pub",
+ "fulcio.crt.pem",
+ "fulcio_intermediate_v1.crt.pem",
+ "trusted_root.json",
+}
+
+func TestNewFromEnv(t *testing.T) {
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+ ctx := context.Background()
+
+ // Make sure nothing is expired
+ tuf, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+
+ // Now try with expired targets
+ forceExpiration(t, true)
+ tuf, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+
+ if err := Initialize(ctx, DefaultRemoteRoot, nil); err != nil {
+ t.Error()
+ }
+ if l := dirLen(t, td); l == 0 {
+ t.Errorf("expected filesystem writes, got %d entries", l)
+ }
+
+ // And go from there!
+ tuf, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+}
+
+func TestLegacyURLToCDN(t *testing.T) {
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+ remoteInfo := &remoteCache{Mirror: defaultRemoteRootNoCDN}
+ b, err := json.Marshal(remoteInfo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(cachedRemote(td), b, 0o600); err != nil {
+ t.Fatalf("storing remote: %v", err)
+ }
+
+ // First initialization, populate the cache.
+ tuf, err := NewFromEnv(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("legacy prod GCS HTTP endpoint was not mapped to CDN")
+ }
+}
+
+func TestAltLegacyURLToCDN(t *testing.T) {
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+ remoteInfo := &remoteCache{Mirror: defaultRemoteRootNoCDNAlt}
+ b, err := json.Marshal(remoteInfo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(cachedRemote(td), b, 0o600); err != nil {
+ t.Fatalf("storing remote: %v", err)
+ }
+
+ // First initialization, populate the cache.
+ tuf, err := NewFromEnv(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("legacy prod GCS HTTP endpoint was not mapped to CDN")
+ }
+}
+
+func TestCDNRewriteforMirror(t *testing.T) {
+ tuf := &TUF{
+ mirror: defaultRemoteGCSBucket,
+ }
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("reference to default remote GCS bucket was not redirected to CDN")
+ }
+
+ tuf.mirror = defaultRemoteRootNoCDN
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("reference to default remote GCS HTTP endpoint was not redirected to CDN")
+ }
+
+ tuf.mirror = defaultRemoteRootNoCDNAlt
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("reference to alternate remote GCS HTTP endpoint was not redirected to CDN")
+ }
+}
+
+func TestLegacyBucketToCDN(t *testing.T) {
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+ remoteInfo := &remoteCache{Mirror: defaultRemoteGCSBucket}
+ b, err := json.Marshal(remoteInfo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(cachedRemote(td), b, 0o600); err != nil {
+ t.Fatalf("storing remote: %v", err)
+ }
+
+ // First initialization, populate the cache.
+ tuf, err := NewFromEnv(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tuf.Mirror() != DefaultRemoteRoot {
+ t.Fatal("legacy prod bucket was not mapped to CDN")
+ }
+}
+
+func TestNoCache(t *testing.T) {
+ ctx := context.Background()
+ // Once more with NO_CACHE
+ t.Setenv("SIGSTORE_NO_CACHE", "true")
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+
+ // First initialization, populate the cache.
+ tuf, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+
+ // Force expiration so we have some content to download
+ forceExpiration(t, true)
+
+ tuf, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+
+ // No filesystem writes when using SIGSTORE_NO_CACHE.
+ if l := dirLen(t, td); l != 0 {
+ t.Errorf("expected no filesystem writes, got %d entries", l)
+ }
+ resetForTests()
+}
+
+func TestCache(t *testing.T) {
+ ctx := context.Background()
+ // Once more with cache.
+ t.Setenv("SIGSTORE_NO_CACHE", "false")
+ td := t.TempDir()
+ t.Setenv("TUF_ROOT", td)
+
+ // Make sure nothing is in that directory to start with
+ if l := dirLen(t, td); l != 0 {
+ t.Errorf("expected no filesystem writes, got %d entries", l)
+ }
+
+ // First initialization, populate the cache. Expect disk writes.
+ tuf, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+ cachedDirLen := dirLen(t, td)
+ if cachedDirLen == 0 {
+ t.Errorf("expected filesystem writes, got %d entries", cachedDirLen)
+ }
+
+ // Nothing should get downloaded if everything is up to date.
+ forceExpiration(t, false)
+ _, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resetForTests()
+
+ if l := dirLen(t, td); cachedDirLen != l {
+ t.Errorf("expected no filesystem writes, got %d entries", l-cachedDirLen)
+ }
+
+ // Forcing expiration, but expect no disk writes because all targets up to date.
+ forceExpiration(t, true)
+ tuf, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if l := dirLen(t, td); l != cachedDirLen {
+ t.Errorf("expected filesystem writes, got %d entries", l)
+ }
+ checkTargetsAndMeta(t, tuf, targets)
+ resetForTests()
+}
+
+func TestCustomRoot(t *testing.T) {
+ ctx := context.Background()
+ // Create a remote repository.
+ td := t.TempDir()
+ remote, r := newTufRepo(t, td, "foo")
+
+ // Serve remote repository.
+ s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository"))))
+ defer s.Close()
+
+ // Initialize with custom root.
+ tufRoot := t.TempDir()
+ t.Setenv("TUF_ROOT", tufRoot)
+ meta, err := remote.GetMeta()
+ if err != nil {
+ t.Error(err)
+ }
+ rootBytes, ok := meta["root.json"]
+ if !ok {
+ t.Error(err)
+ }
+ if err := Initialize(ctx, s.URL, rootBytes); err != nil {
+ t.Error(err)
+ }
+ if l := dirLen(t, tufRoot); l == 0 {
+ t.Errorf("expected filesystem writes, got %d entries", l)
+ }
+
+ // Successfully get target.
+ tufObj, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b, err := tufObj.GetTarget("foo.txt"); err != nil || !bytes.Equal(b, []byte("foo")) {
+ t.Fatal(err)
+ }
+ resetForTests()
+
+ // Force expiration on the first timestamp and internal go-tuf verification.
+ forceExpirationVersion(t, 1)
+ oldIsExpired := verify.IsExpired
+ verify.IsExpired = func(time time.Time) bool {
+ return true
+ }
+
+ // This should cause an error that remote metadata is expired.
+ if _, err = NewFromEnv(ctx); err == nil {
+ t.Errorf("expected expired timestamp from the remote")
+ }
+
+ // Let internal TUF verification succeed normally now.
+ verify.IsExpired = oldIsExpired
+
+ // Update remote targets, issue a timestamp v2.
+ updateTufRepo(t, td, r, "foo1")
+
+ // Use newTuf and successfully get updated metadata using the cached remote location.
+ resetForTests()
+ tufObj, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b, err := tufObj.GetTarget("foo.txt"); err != nil || !bytes.Equal(b, []byte("foo1")) {
+ t.Fatal(err)
+ }
+ resetForTests()
+}
+
+func TestCustomRootFileRemoteStore(t *testing.T) {
+ ctx := context.Background()
+ // Create a remote repository.
+ td := t.TempDir()
+ remote, r := newTufRepo(t, td, "foo")
+
+ // Initialize with custom root.
+ tufRoot := t.TempDir()
+ t.Setenv("TUF_ROOT", tufRoot)
+ meta, err := remote.GetMeta()
+ if err != nil {
+ t.Error(err)
+ }
+ rootBytes, ok := meta["root.json"]
+ if !ok {
+ t.Error(err)
+ }
+ // Tack on repository to the end of the td above since that's where
+ // newTufRepo creates the repository.
+ fileURI := fmt.Sprintf("file://%s", filepath.Join(td, "repository"))
+ if err := Initialize(ctx, fileURI, rootBytes); err != nil {
+ t.Error(err)
+ }
+ if l := dirLen(t, tufRoot); l == 0 {
+ t.Errorf("expected filesystem writes, got %d entries", l)
+ }
+
+ // Successfully get target.
+ tufObj, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b, err := tufObj.GetTarget("foo.txt"); err != nil || !bytes.Equal(b, []byte("foo")) {
+ t.Fatal(err)
+ }
+ resetForTests()
+
+ // Force expiration on the first timestamp and internal go-tuf verification.
+ forceExpirationVersion(t, 1)
+ oldIsExpired := verify.IsExpired
+ verify.IsExpired = func(time time.Time) bool {
+ return true
+ }
+
+ // This should cause an error that remote metadata is expired.
+ if _, err = NewFromEnv(ctx); err == nil {
+ t.Errorf("expected expired timestamp from the remote")
+ }
+
+ // Let internal TUF verification succeed normally now.
+ verify.IsExpired = oldIsExpired
+
+ // Update remote targets, issue a timestamp v2.
+ updateTufRepo(t, td, r, "foo1")
+
+ // Use newTuf and successfully get updated metadata using the cached remote location.
+ resetForTests()
+ tufObj, err = NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b, err := tufObj.GetTarget("foo.txt"); err != nil || !bytes.Equal(b, []byte("foo1")) {
+ t.Fatal(err)
+ }
+ resetForTests()
+}
+
+func TestGetTargetsByMeta(t *testing.T) {
+ ctx := context.Background()
+ // Create a remote repository.
+ td := t.TempDir()
+ remote, _ := newTufCustomRepo(t, td, "foo")
+
+ // Serve remote repository.
+ s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository"))))
+ defer s.Close()
+
+ // Initialize with custom root.
+ tufRoot := t.TempDir()
+ t.Setenv("TUF_ROOT", tufRoot)
+ meta, err := remote.GetMeta()
+ if err != nil {
+ t.Error(err)
+ }
+ rootBytes, ok := meta["root.json"]
+ if !ok {
+ t.Error(err)
+ }
+ if err := Initialize(ctx, s.URL, rootBytes); err != nil {
+ t.Error(err)
+ }
+ if l := dirLen(t, tufRoot); l == 0 {
+ t.Errorf("expected filesystem writes, got %d entries", l)
+ }
+
+ tufObj, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resetForTests()
+ // Fetch a target with no custom metadata.
+ targets, err := tufObj.GetTargetsByMeta(UnknownUsage, []string{"fooNoCustom.txt"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(targets) != 1 {
+ t.Fatalf("expected one target without custom metadata, got %d targets", len(targets))
+ }
+ if !bytes.Equal(targets[0].Target, []byte("foo")) {
+ t.Fatalf("target metadata mismatched, expected: %s, got: %s", "foo", string(targets[0].Target))
+ }
+ if targets[0].Status != Active {
+ t.Fatalf("target without custom metadata not active, got: %v", targets[0].Status)
+ }
+ // Fetch multiple targets with no custom metadata.
+ targets, err = tufObj.GetTargetsByMeta(UnknownUsage, []string{"fooNoCustom.txt", "fooNoCustomOther.txt"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(targets) != 2 {
+ t.Fatalf("expected two targets without custom metadata, got %d targets", len(targets))
+ }
+ if targets[0].Status != Active || targets[1].Status != Active {
+ t.Fatalf("target without custom metadata not active, got: %v and %v", targets[0].Status, targets[1].Status)
+ }
+ // Specify multiple fallbacks with no custom metadata.
+ targets, err = tufObj.GetTargetsByMeta(UnknownUsage, []string{"fooNoCustom.txt", "fooNoCustomOtherMissingTarget.txt"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(targets) != 1 {
+ t.Fatalf("expected one targets without custom metadata, got %d targets", len(targets))
+ }
+ if targets[0].Status != Active {
+ t.Fatalf("target without custom metadata not active, got: %v and %v", targets[0].Status, targets[1].Status)
+ }
+ // Fetch targets with custom metadata.
+ targets, err = tufObj.GetTargetsByMeta(Fulcio, []string{"fooNoCustom.txt"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(targets) != 2 {
+ t.Fatalf("expected two targets without custom metadata, got %d targets", len(targets))
+ }
+ targetBytes := []string{string(targets[0].Target), string(targets[1].Target)}
+ expectedTB := []string{"foo", "foo"}
+ if !reflect.DeepEqual(targetBytes, expectedTB) {
+ t.Fatalf("target metadata mismatched, expected: %v, got: %v", expectedTB, targetBytes)
+ }
+ targetStatuses := []StatusKind{targets[0].Status, targets[1].Status}
+ sort.Slice(targetStatuses, func(i, j int) bool {
+ return targetStatuses[i] < targetStatuses[j]
+ })
+ expectedTS := []StatusKind{Active, Expired}
+ if !reflect.DeepEqual(targetStatuses, expectedTS) {
+ t.Fatalf("unexpected target status with custom metadata, expected %v, got: %v", expectedTS, targetStatuses)
+ }
+ // Error when fetching target that does not exist.
+ _, err = tufObj.GetTargetsByMeta(UsageKind(UnknownStatus), []string{"unknown.txt"})
+ expectedErr := "file not found: unknown.txt"
+ if !strings.Contains(err.Error(), "not found: unknown.txt") {
+ t.Fatalf("unexpected error fetching missing metadata, expected: %s, got: %s", expectedErr, err.Error())
+ }
+}
+
+func makeMapFS(repo string) (fs fstest.MapFS) {
+ fs = make(fstest.MapFS)
+ _ = filepath.Walk(repo,
+ func(fpath string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ rel, _ := filepath.Rel(repo, fpath)
+ if info.IsDir() {
+ fs[path.Join("repository", rel)] = &fstest.MapFile{Mode: os.ModeDir}
+ } else {
+ b, _ := os.ReadFile(fpath)
+ fs[path.Join("repository", rel)] = &fstest.MapFile{Data: b}
+ }
+ return nil
+ })
+ return
+}
+
+// Regression test for failure to fetch a target that does not exist in the embedded
+// repository on an update. The new target exists on the remote before the TUF object
+// is initialized.
+func TestUpdatedTargetNamesEmbedded(t *testing.T) {
+ td := t.TempDir()
+ // Set the TUF_ROOT so we don't interact with other tests and local TUF roots.
+ t.Setenv("TUF_ROOT", td)
+
+ origEmbedded := getEmbedded
+ origDefaultRemote := getRemoteRoot
+
+ // Create an "expired" embedded repository that does not contain newTarget.
+ ctx := context.Background()
+ store, r := newTufCustomRepo(t, td, "foo")
+ repository := filepath.FromSlash(filepath.Join(td, "repository"))
+ mapfs := makeMapFS(repository)
+ getEmbedded = func() fs.FS { return mapfs }
+
+ oldIsExpired := isExpiredTimestamp
+ isExpiredTimestamp = func(metadata []byte) bool {
+ m, _ := store.GetMeta()
+ timestampExpires, _ := getExpiration(m["timestamp.json"])
+ metadataExpires, _ := getExpiration(metadata)
+ return metadataExpires.Sub(*timestampExpires) <= 0
+ }
+ defer func() {
+ getEmbedded = origEmbedded
+ getRemoteRoot = origDefaultRemote
+ isExpiredTimestamp = oldIsExpired
+ }()
+
+ // Assert that the embedded repository does not contain the newTarget.
+ newTarget := "fooNew.txt"
+ rd, ok := getEmbedded().(fs.ReadFileFS)
+ if !ok {
+ t.Fatal("fs.ReadFileFS unimplemented for embedded repo")
+ }
+ if _, err := rd.ReadFile(path.Join("repository", "targets", newTarget)); err == nil {
+ t.Fatal("embedded repository should not contain new target")
+ }
+
+ // Serve an updated remote repository with the newTarget.
+ addNewCustomTarget(t, td, r, map[string]string{newTarget: "newdata"})
+ s := httptest.NewServer(http.FileServer(http.Dir(repository)))
+ defer s.Close()
+ getRemoteRoot = func() string { return s.URL }
+
+ // Initialize.
+ tufObj, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resetForTests()
+
+ // Try to retrieve the newly added target.
+ targets, err := tufObj.GetTargetsByMeta(Fulcio, []string{"fooNoCustom.txt"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(targets) != 3 {
+ t.Fatalf("expected three target without custom metadata, got %d targets", len(targets))
+ }
+ targetBytes := []string{string(targets[0].Target), string(targets[1].Target), string(targets[2].Target)}
+ expectedTB := []string{"foo", "foo", "newdata"}
+ if !cmp.Equal(targetBytes, expectedTB,
+ cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
+ t.Fatalf("target data mismatched, expected: %v, got: %v", expectedTB, targetBytes)
+ }
+}
+
+func checkTargetsAndMeta(t *testing.T, tuf *TUF, expected []string) {
+ // Check the targets
+ t.Helper()
+ for _, target := range expected {
+ if _, err := tuf.GetTarget(target); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // An invalid target
+ if _, err := tuf.GetTarget("invalid"); err == nil {
+ t.Error("expected error reading target, got nil")
+ }
+
+ // Check root status.
+ _, err := tuf.getRootStatus()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func dirLen(t *testing.T, td string) int {
+ t.Helper()
+ de, err := os.ReadDir(td)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return len(de)
+}
+
+func forceExpiration(t *testing.T, expire bool) {
+ oldIsExpiredTimestamp := isExpiredTimestamp
+ isExpiredTimestamp = func(_ []byte) bool {
+ return expire
+ }
+ t.Cleanup(func() {
+ isExpiredTimestamp = oldIsExpiredTimestamp
+ })
+}
+
+func forceExpirationVersion(t *testing.T, version int64) {
+ oldIsExpiredTimestamp := isExpiredTimestamp
+ isExpiredTimestamp = func(metadata []byte) bool {
+ s := &data.Signed{}
+ if err := json.Unmarshal(metadata, s); err != nil {
+ return true
+ }
+ sm := &data.Timestamp{}
+ if err := json.Unmarshal(s.Signed, sm); err != nil {
+ return true
+ }
+ return sm.Version <= version
+ }
+ t.Cleanup(func() {
+ isExpiredTimestamp = oldIsExpiredTimestamp
+ })
+}
+
+// newTufCustomRepo initializes a TUF repository with root, targets, snapshot, and timestamp roles
+// 4 targets are created to exercise various code paths, including two targets with no custom metadata,
+// one target with custom metadata marked as active, and another with custom metadata marked as expired.
+func newTufCustomRepo(t *testing.T, td, targetData string) (tuf.LocalStore, *tuf.Repo) {
+ scmActive, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Active}})
+ if err != nil {
+ t.Error(err)
+ }
+ scmExpired, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Expired}})
+ if err != nil {
+ t.Error(err)
+ }
+
+ remote := tuf.FileSystemStore(td, nil)
+ r, err := tuf.NewRepo(remote)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := r.Init(false); err != nil {
+ t.Error(err)
+ }
+ for _, role := range []string{"root", "targets", "snapshot", "timestamp"} {
+ if _, err := r.GenKey(role); err != nil {
+ t.Error(err)
+ }
+ }
+ for name, scm := range map[string]json.RawMessage{
+ "fooNoCustom.txt": nil, "fooNoCustomOther.txt": nil,
+ "fooActive.txt": scmActive, "fooExpired.txt": scmExpired,
+ } {
+ targetPath := filepath.FromSlash(filepath.Join(td, "staged", "targets", name))
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
+ t.Error(err)
+ }
+ if err := os.WriteFile(targetPath, []byte(targetData), 0o600); err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget(name, scm); err != nil {
+ t.Error(err)
+ }
+ }
+ if err := r.Snapshot(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Timestamp(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Commit(); err != nil {
+ t.Error(err)
+ }
+ return remote, r
+}
+
+func addNewCustomTarget(t *testing.T, td string, r *tuf.Repo, targetData map[string]string) {
+ scmActive, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Active}})
+ if err != nil {
+ t.Error(err)
+ }
+
+ for name, data := range targetData {
+ targetPath := filepath.FromSlash(filepath.Join(td, "staged", "targets", name))
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
+ t.Error(err)
+ }
+ if err := os.WriteFile(targetPath, []byte(data), 0o600); err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget(name, scmActive); err != nil {
+ t.Error(err)
+ }
+ }
+
+ if err := r.Snapshot(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Timestamp(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Commit(); err != nil {
+ t.Error(err)
+ }
+}
+
+func newTufRepo(t *testing.T, td, targetData string) (tuf.LocalStore, *tuf.Repo) {
+ remote := tuf.FileSystemStore(td, nil)
+ r, err := tuf.NewRepo(remote)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := r.Init(false); err != nil {
+ t.Error(err)
+ }
+ for _, role := range []string{"root", "targets", "snapshot", "timestamp"} {
+ if _, err := r.GenKey(role); err != nil {
+ t.Error(err)
+ }
+ }
+ targetPath := filepath.FromSlash(filepath.Join(td, "staged", "targets", "foo.txt"))
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
+ t.Error(err)
+ }
+ if err := os.WriteFile(targetPath, []byte(targetData), 0o600); err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget("foo.txt", nil); err != nil {
+ t.Error(err)
+ }
+ if err := r.Snapshot(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Timestamp(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Commit(); err != nil {
+ t.Error(err)
+ }
+ return remote, r
+}
+
+func updateTufRepo(t *testing.T, td string, r *tuf.Repo, targetData string) {
+ targetPath := filepath.FromSlash(filepath.Join(td, "staged", "targets", "foo.txt"))
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
+ t.Error(err)
+ }
+ if err := os.WriteFile(targetPath, []byte(targetData), 0o600); err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget("foo.txt", nil); err != nil {
+ t.Error(err)
+ }
+ if err := r.Snapshot(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Timestamp(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Commit(); err != nil {
+ t.Error(err)
+ }
+}
+
+func TestConcurrentAccessNewFromEnv(t *testing.T) {
+ var wg sync.WaitGroup
+
+ for i := 0; i < 20; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ tufObj, err := NewFromEnv(context.Background())
+ if err != nil {
+ t.Errorf("Failed to construct NewFromEnv: %s", err)
+ }
+ if tufObj == nil {
+ t.Error("Got back nil tufObj")
+ }
+ time.Sleep(1 * time.Second)
+ }()
+ }
+ wg.Wait()
+ resetForTests()
+}
+
+func TestConcurrentAccessInitialize(t *testing.T) {
+ var wg sync.WaitGroup
+
+ for i := 0; i < 20; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ err := Initialize(context.Background(), DefaultRemoteRoot, nil)
+ if err != nil {
+ t.Errorf("Failed to construct NewFromEnv: %s", err)
+ }
+ time.Sleep(1 * time.Second)
+ }()
+ }
+ wg.Wait()
+ resetForTests()
+}
+
+// Test to validate that sigstore TUF client can cache targets that
+// are located in sub-folders.
+func TestTargetsSubfolder(t *testing.T) {
+ ctx := context.Background()
+ // Create a remote repository.
+ td := t.TempDir()
+ remote, r := newTufCustomRepo(t, td, "foo")
+ newTarget := "subfolder/fooNew.txt"
+ addNewCustomTarget(t, td, r, map[string]string{newTarget: "newdata"})
+
+ // Serve remote repository.
+ s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository"))))
+ defer s.Close()
+
+ // Initialize with custom root.
+ tufRoot := t.TempDir()
+ // Set the TUF_ROOT so we don't interact with other tests and local TUF roots.
+ t.Setenv("TUF_ROOT", tufRoot)
+ meta, err := remote.GetMeta()
+ if err != nil {
+ t.Error(err)
+ }
+ rootBytes, ok := meta["root.json"]
+ if !ok {
+ t.Error(err)
+ }
+
+ if err := Initialize(ctx, s.URL, rootBytes); err != nil {
+ t.Error(err)
+ }
+
+ defer resetForTests()
+
+ tuf, err := NewFromEnv(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checkTargetsAndMeta(t, tuf, []string{newTarget})
+}
+
+func Test_remoteFromMirror(t *testing.T) {
+ // test GCS mirror
+ mirror := "test-bucket"
+ _, err := remoteFromMirror(mirror)
+ if err != nil {
+ t.Fatalf("unexpected error with GCS mirror: %v", err)
+ }
+
+ // test HTTP mirror
+ mirror = "https://tuf-repo-cdn.sigstage.dev"
+ _, err = remoteFromMirror(mirror)
+ if err != nil {
+ t.Fatalf("unexpected error with GCS mirror: %v", err)
+ }
+
+ // test local mirror
+ tufRoot := t.TempDir()
+ os.Mkdir(fmt.Sprintf("%s/targets", tufRoot), 0o0750)
+ mirror = fmt.Sprintf("file://%s", tufRoot)
+ _, err = remoteFromMirror(mirror)
+ if err != nil {
+ t.Fatalf("unexpected error with GCS mirror: %v", err)
+ }
+}
diff --git a/pkg/tuf/repository/root.json b/pkg/tuf/repository/root.json
new file mode 100644
index 0000000..c3ea9cb
--- /dev/null
+++ b/pkg/tuf/repository/root.json
@@ -0,0 +1,140 @@
+{
+ "signed": {
+ "_type": "root",
+ "spec_version": "1.0",
+ "version": 7,
+ "expires": "2023-10-04T13:08:11Z",
+ "keys": {
+ "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELrWvNt94v4R085ELeeCMxHp7PldF\n0/T1GxukUh2ODuggLGJE0pc1e8CSBf6CS91Fwo9FUOuRsjBUld+VqSyCdQ==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n"
+ }
+ },
+ "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c": {
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "keyid_hash_algorithms": [
+ "sha256",
+ "sha512"
+ ],
+ "keyval": {
+ "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEy8XKsmhBYDI8Jc0GwzBxeKax0cm5\nSTKEU65HPFunUn41sT8pi0FjM4IkHz/YUmwmLUO0Wt7lxhj6BkLIK4qYAw==\n-----END PUBLIC KEY-----\n"
+ }
+ }
+ },
+ "roles": {
+ "root": {
+ "keyids": [
+ "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c",
+ "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99",
+ "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f",
+ "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b",
+ "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de"
+ ],
+ "threshold": 3
+ },
+ "snapshot": {
+ "keyids": [
+ "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b"
+ ],
+ "threshold": 1
+ },
+ "targets": {
+ "keyids": [
+ "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c",
+ "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99",
+ "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f",
+ "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b",
+ "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de"
+ ],
+ "threshold": 3
+ },
+ "timestamp": {
+ "keyids": [
+ "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a"
+ ],
+ "threshold": 1
+ }
+ },
+ "consistent_snapshot": true
+ },
+ "signatures": [
+ {
+ "keyid": "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99",
+ "sig": "3046022100c0610c0055ce5c4a52d054d7322e7b514d55baf44423d63aa4daa077cc60fd1f022100a097f2803f090fb66c42ead915a2c46ebe7db53a32bf18f2188275cc936f8bdd"
+ },
+ {
+ "keyid": "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f",
+ "sig": "304502203134f0468810299d5493a867c40630b341296b92e59c29821311d353343bb3a4022100e667ae3d304e7e3da0894c7425f6b9ecd917106841280e5cf6f3496ad5f8f68e"
+ },
+ {
+ "keyid": "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b",
+ "sig": "3045022037fe5f45426f21eaaf4730d2136f2b1611d6379688f79b9d1e3f61719997135c022100b63b022d7b79d4694b96f416d88aa4d7b1a3bff8a01f4fb51e0f42137c7d2d06"
+ },
+ {
+ "keyid": "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de",
+ "sig": "3044022007cc8fcc4940809f2751ad5b535f4c5f53f5b4952f5b5696b09668e743306ac1022006dfcdf94e94c92163eeb1b47796db62cedaa730aa13aa61b573fe23714730f2"
+ }
+ ]
+} \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/artifact.pub b/pkg/tuf/repository/targets/artifact.pub
new file mode 100644
index 0000000..d6e745b
--- /dev/null
+++ b/pkg/tuf/repository/targets/artifact.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhyQCx0E9wQWSFI9ULGwy3BuRklnt
+IqozONbbdbqz11hlRJy9c7SG+hdcFl9jE9uE/dwtuwU2MqU9T/cN0YkWww==
+-----END PUBLIC KEY----- \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/ctfe.pub b/pkg/tuf/repository/targets/ctfe.pub
new file mode 100644
index 0000000..1bb1488
--- /dev/null
+++ b/pkg/tuf/repository/targets/ctfe.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu
+dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==
+-----END PUBLIC KEY----- \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/ctfe_2022.pub b/pkg/tuf/repository/targets/ctfe_2022.pub
new file mode 100644
index 0000000..32fa2ad
--- /dev/null
+++ b/pkg/tuf/repository/targets/ctfe_2022.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNK
+AaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==
+-----END PUBLIC KEY-----
diff --git a/pkg/tuf/repository/targets/fulcio.crt.pem b/pkg/tuf/repository/targets/fulcio.crt.pem
new file mode 100644
index 0000000..6a06ff3
--- /dev/null
+++ b/pkg/tuf/repository/targets/fulcio.crt.pem
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq
+MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
+MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu
+ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy
+A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas
+taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm
+MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
+FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u
+Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx
+Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup
+Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/fulcio_intermediate_v1.crt.pem b/pkg/tuf/repository/targets/fulcio_intermediate_v1.crt.pem
new file mode 100644
index 0000000..6d1c298
--- /dev/null
+++ b/pkg/tuf/repository/targets/fulcio_intermediate_v1.crt.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw
+KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
+MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl
+LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C
+AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7
+7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS
+0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB
+BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp
+KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI
+zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR
+nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP
+mygUY7Ii2zbdCdliiow=
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/fulcio_v1.crt.pem b/pkg/tuf/repository/targets/fulcio_v1.crt.pem
new file mode 100644
index 0000000..3afc46b
--- /dev/null
+++ b/pkg/tuf/repository/targets/fulcio_v1.crt.pem
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw
+KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
+MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl
+LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7
+XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex
+X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j
+YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY
+wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ
+KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM
+WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9
+TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/pkg/tuf/repository/targets/rekor.pub b/pkg/tuf/repository/targets/rekor.pub
new file mode 100644
index 0000000..050ef60
--- /dev/null
+++ b/pkg/tuf/repository/targets/rekor.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
+kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
+-----END PUBLIC KEY-----
diff --git a/pkg/tuf/repository/targets/trusted_root.json b/pkg/tuf/repository/targets/trusted_root.json
new file mode 100644
index 0000000..5ed6281
--- /dev/null
+++ b/pkg/tuf/repository/targets/trusted_root.json
@@ -0,0 +1,114 @@
+{
+ "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
+ "tlogs": [
+ {
+ "baseUrl": "https://rekor.sigstore.dev",
+ "hashAlgorithm": "SHA2_256",
+ "publicKey": {
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+ "validFor": {
+ "start": "2021-01-12T11:53:27.000Z"
+ }
+ },
+ "logId": {
+ "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
+ }
+ }
+ ],
+ "certificateAuthorities": [
+ {
+ "subject": {
+ "organization": "sigstore.dev",
+ "commonName": "sigstore"
+ },
+ "uri": "https://fulcio.sigstore.dev",
+ "certChain": {
+ "certificates": [
+ {
+ "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="
+ }
+ ]
+ },
+ "validFor": {
+ "start": "2021-03-07T03:20:29.000Z",
+ "end": "2022-12-31T23:59:59.999Z"
+ }
+ },
+ {
+ "subject": {
+ "organization": "sigstore.dev",
+ "commonName": "sigstore"
+ },
+ "uri": "https://fulcio.sigstore.dev",
+ "certChain": {
+ "certificates": [
+ {
+ "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
+ },
+ {
+ "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
+ }
+ ]
+ },
+ "validFor": {
+ "start": "2022-04-13T20:06:15.000Z"
+ }
+ }
+ ],
+ "ctlogs": [
+ {
+ "baseUrl": "https://ctfe.sigstore.dev/test",
+ "hashAlgorithm": "SHA2_256",
+ "publicKey": {
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+ "validFor": {
+ "start": "2021-03-14T00:00:00.000Z",
+ "end": "2022-10-31T23:59:59.999Z"
+ }
+ },
+ "logId": {
+ "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
+ }
+ },
+ {
+ "baseUrl": "https://ctfe.sigstore.dev/2022",
+ "hashAlgorithm": "SHA2_256",
+ "publicKey": {
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
+ "validFor": {
+ "start": "2022-10-20T00:00:00.000Z"
+ }
+ },
+ "logId": {
+ "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
+ }
+ }
+ ],
+ "timestampAuthorities": [
+ {
+ "subject": {
+ "organization": "GitHub, Inc.",
+ "commonName": "Internal Services Root"
+ },
+ "certChain": {
+ "certificates": [
+ {
+ "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe"
+ },
+ {
+ "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA=="
+ },
+ {
+ "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD"
+ }
+ ]
+ },
+ "validFor": {
+ "start": "2023-04-14T00:00:00.000Z"
+ }
+ }
+ ]
+}
diff --git a/pkg/tuf/status_type.go b/pkg/tuf/status_type.go
new file mode 100644
index 0000000..8a020e5
--- /dev/null
+++ b/pkg/tuf/status_type.go
@@ -0,0 +1,60 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "fmt"
+ "strings"
+)
+
+type StatusKind int
+
+const (
+ UnknownStatus StatusKind = iota
+ Active
+ Expired
+)
+
+var toStatusString = map[StatusKind]string{
+ UnknownStatus: "Unknown",
+ Active: "Active",
+ Expired: "Expired",
+}
+
+func (s StatusKind) String() string {
+ return toStatusString[s]
+}
+
+func (s StatusKind) MarshalText() ([]byte, error) {
+ str := s.String()
+ if len(str) == 0 {
+ return nil, fmt.Errorf("error while marshalling, int(StatusKind)=%d not valid", int(s))
+ }
+ return []byte(s.String()), nil
+}
+
+func (s *StatusKind) UnmarshalText(text []byte) error {
+ switch strings.ToLower(string(text)) {
+ case "unknown":
+ *s = UnknownStatus
+ case "active":
+ *s = Active
+ case "expired":
+ *s = Expired
+ default:
+ return fmt.Errorf("error while unmarshalling, StatusKind=%v not valid", string(text))
+ }
+ return nil
+}
diff --git a/pkg/tuf/status_type_test.go b/pkg/tuf/status_type_test.go
new file mode 100644
index 0000000..bc34a34
--- /dev/null
+++ b/pkg/tuf/status_type_test.go
@@ -0,0 +1,84 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestMarshalStatusType(t *testing.T) {
+ statuses := []StatusKind{UnknownStatus, Active, Expired}
+ bytes, err := json.Marshal(statuses)
+ if err != nil {
+ t.Fatalf("expected no error marshalling struct, got: %v", err)
+ }
+ expected := `["Unknown","Active","Expired"]`
+ if string(bytes) != expected {
+ t.Fatalf("error while marshalling, expected: %s, got: %s", expected, bytes)
+ }
+}
+
+func TestMarshalInvalidStatusType(t *testing.T) {
+ invalidStatus := 42
+ statuses := []StatusKind{StatusKind(invalidStatus)}
+ bytes, err := json.Marshal(statuses)
+ if bytes != nil {
+ t.Fatalf("expected error marshalling struct, got: %v", bytes)
+ }
+ expectedErr := fmt.Sprintf("error while marshalling, int(StatusKind)=%d not valid", invalidStatus)
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Fatalf("expected error marshalling struct, expected: %v, got: %v", expectedErr, err)
+ }
+}
+
+func TestUnmarshalStatusType(t *testing.T) {
+ var statuses []StatusKind
+ j := json.RawMessage(`["expired", "active", "unknown"]`)
+ err := json.Unmarshal(j, &statuses)
+ if err != nil {
+ t.Fatalf("expected no error unmarshalling struct, got: %v", err)
+ }
+ if !reflect.DeepEqual(statuses, []StatusKind{Expired, Active, UnknownStatus}) {
+ t.Fatalf("expected [Expired, Active, Unknown], got: %v", statuses)
+ }
+}
+
+func TestUnmarshalStatusTypeCapitalization(t *testing.T) {
+ // Any capitalization is allowed.
+ var statuses []StatusKind
+ j := json.RawMessage(`["eXpIrEd", "aCtIvE", "uNkNoWn"]`)
+ err := json.Unmarshal(j, &statuses)
+ if err != nil {
+ t.Fatalf("expected no error unmarshalling struct, got: %v", err)
+ }
+ if !reflect.DeepEqual(statuses, []StatusKind{Expired, Active, UnknownStatus}) {
+ t.Fatalf("expected [Expired, Active, Unknown], got: %v", statuses)
+ }
+}
+
+func TestUnmarshalInvalidStatusType(t *testing.T) {
+ var statuses []StatusKind
+ invalidStatus := "invalid"
+ j := json.RawMessage(fmt.Sprintf(`["%s"]`, invalidStatus))
+ err := json.Unmarshal(j, &statuses)
+ expectedErr := fmt.Sprintf("error while unmarshalling, StatusKind=%s not valid", invalidStatus)
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Fatalf("expected error unmarshalling struct, expected: %v, got: %v", expectedErr, err)
+ }
+}
diff --git a/pkg/tuf/testutils.go b/pkg/tuf/testutils.go
new file mode 100644
index 0000000..42c6f39
--- /dev/null
+++ b/pkg/tuf/testutils.go
@@ -0,0 +1,129 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "context"
+ "crypto/x509"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "github.com/sigstore/sigstore/pkg/signature"
+ "github.com/sigstore/sigstore/pkg/signature/options"
+ "github.com/theupdateframework/go-tuf"
+)
+
+type TestSigstoreRoot struct {
+ Rekor signature.Verifier
+ FulcioCertificate *x509.Certificate
+ // TODO: Include a CTFE key if/when cosign verifies SCT.
+}
+
+// This creates a new sigstore TUF repo whose signers can be used to create dynamic
+// signed Rekor entries.
+func NewSigstoreTufRepo(t *testing.T, root TestSigstoreRoot) (tuf.LocalStore, *tuf.Repo) {
+ td := t.TempDir()
+ ctx := context.Background()
+ remote := tuf.FileSystemStore(td, nil)
+ r, err := tuf.NewRepo(remote)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := r.Init(false); err != nil {
+ t.Error(err)
+ }
+
+ for _, role := range []string{"root", "targets", "snapshot", "timestamp"} {
+ if _, err := r.GenKey(role); err != nil {
+ t.Error(err)
+ }
+ }
+ targetsPath := filepath.Join(td, "staged", "targets")
+ if err := os.MkdirAll(filepath.Dir(targetsPath), 0o755); err != nil {
+ t.Error(err)
+ }
+ // Add the rekor key target
+ pk, err := root.Rekor.PublicKey(options.WithContext(ctx))
+ if err != nil {
+ t.Error(err)
+ }
+ b, err := x509.MarshalPKIXPublicKey(pk)
+ if err != nil {
+ t.Error(err)
+ }
+ rekorPath := "rekor.pub"
+ rekorData := cryptoutils.PEMEncode(cryptoutils.PublicKeyPEMType, b)
+ if err := os.WriteFile(filepath.Join(targetsPath, rekorPath), rekorData, 0o600); err != nil {
+ t.Error(err)
+ }
+ scmRekor, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Rekor, Status: Active}})
+ if err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget("rekor.pub", scmRekor); err != nil {
+ t.Error(err)
+ }
+ // Add Fulcio Certificate information.
+ fulcioPath := "fulcio.crt.pem"
+ fulcioData := cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, root.FulcioCertificate.Raw)
+ if err := os.WriteFile(filepath.Join(targetsPath, fulcioPath), fulcioData, 0o600); err != nil {
+ t.Error(err)
+ }
+ scmFulcio, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: customMetadata{Usage: Fulcio, Status: Active}})
+ if err != nil {
+ t.Error(err)
+ }
+ if err := r.AddTarget(fulcioPath, scmFulcio); err != nil {
+ t.Error(err)
+ }
+ if err := r.Snapshot(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Timestamp(); err != nil {
+ t.Error(err)
+ }
+ if err := r.Commit(); err != nil {
+ t.Error(err)
+ }
+ // Serve remote repository.
+ s := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(td, "repository"))))
+ defer s.Close()
+
+ // Initialize with custom root.
+ tufRoot := t.TempDir()
+ t.Setenv("TUF_ROOT", tufRoot)
+ meta, err := remote.GetMeta()
+ if err != nil {
+ t.Error(err)
+ }
+ rootBytes, ok := meta["root.json"]
+ if !ok {
+ t.Error(err)
+ }
+ resetForTests()
+ if err := Initialize(ctx, s.URL, rootBytes); err != nil {
+ t.Error(err)
+ }
+ t.Cleanup(func() {
+ resetForTests()
+ })
+ return remote, r
+}
diff --git a/pkg/tuf/usage_type.go b/pkg/tuf/usage_type.go
new file mode 100644
index 0000000..4ea7ad0
--- /dev/null
+++ b/pkg/tuf/usage_type.go
@@ -0,0 +1,64 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "fmt"
+ "strings"
+)
+
+type UsageKind int
+
+const (
+ UnknownUsage UsageKind = iota
+ Fulcio
+ Rekor
+ CTFE
+)
+
+var toUsageString = map[UsageKind]string{
+ UnknownUsage: "Unknown",
+ Fulcio: "Fulcio",
+ Rekor: "Rekor",
+ CTFE: "CTFE",
+}
+
+func (u UsageKind) String() string {
+ return toUsageString[u]
+}
+
+func (u UsageKind) MarshalText() ([]byte, error) {
+ str := u.String()
+ if len(str) == 0 {
+ return nil, fmt.Errorf("error while marshalling, int(UsageKind)=%d not valid", int(u))
+ }
+ return []byte(u.String()), nil
+}
+
+func (u *UsageKind) UnmarshalText(text []byte) error {
+ switch strings.ToLower(string(text)) {
+ case "unknown":
+ *u = UnknownUsage
+ case "fulcio":
+ *u = Fulcio
+ case "rekor":
+ *u = Rekor
+ case "ctfe":
+ *u = CTFE
+ default:
+ return fmt.Errorf("error while unmarshalling, UsageKind=%v not valid", string(text))
+ }
+ return nil
+}
diff --git a/pkg/tuf/usage_type_test.go b/pkg/tuf/usage_type_test.go
new file mode 100644
index 0000000..9fca0cf
--- /dev/null
+++ b/pkg/tuf/usage_type_test.go
@@ -0,0 +1,84 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestMarshalUsageType(t *testing.T) {
+ usages := []UsageKind{UnknownUsage, Fulcio, Rekor, CTFE}
+ bytes, err := json.Marshal(usages)
+ if err != nil {
+ t.Fatalf("expected no error marshalling struct, got: %v", err)
+ }
+ expected := `["Unknown","Fulcio","Rekor","CTFE"]`
+ if string(bytes) != expected {
+ t.Fatalf("error while marshalling, expected: %s, got: %s", expected, bytes)
+ }
+}
+
+func TestMarshalInvalidUsageType(t *testing.T) {
+ invalidUsage := 42
+ usages := []UsageKind{UsageKind(invalidUsage)}
+ bytes, err := json.Marshal(usages)
+ if bytes != nil {
+ t.Fatalf("expected error marshalling struct, got: %v", bytes)
+ }
+ expectedErr := fmt.Sprintf("error while marshalling, int(UsageKind)=%d not valid", invalidUsage)
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Fatalf("expected error marshalling struct, expected: %v, got: %v", expectedErr, err)
+ }
+}
+
+func TestUnmarshalUsageType(t *testing.T) {
+ var usages []UsageKind
+ j := json.RawMessage(`["fulcio", "rekor", "ctfe", "unknown"]`)
+ err := json.Unmarshal(j, &usages)
+ if err != nil {
+ t.Fatalf("expected no error unmarshalling struct, got: %v", err)
+ }
+ if !reflect.DeepEqual(usages, []UsageKind{Fulcio, Rekor, CTFE, UnknownUsage}) {
+ t.Fatalf("expected [Fulcio, Rekor, CTFE, UnknownUsage], got: %v", usages)
+ }
+}
+
+func TestUnmarshalUsageTypeCapitalization(t *testing.T) {
+ // Any capitalization is allowed.
+ var usages []UsageKind
+ j := json.RawMessage(`["fUlCiO", "rEkOr", "cTfE", "uNkNoWn"]`)
+ err := json.Unmarshal(j, &usages)
+ if err != nil {
+ t.Fatalf("expected no error unmarshalling struct, got: %v", err)
+ }
+ if !reflect.DeepEqual(usages, []UsageKind{Fulcio, Rekor, CTFE, UnknownUsage}) {
+ t.Fatalf("expected [Fulcio, Rekor, CTFE, UnknownUsage], got: %v", usages)
+ }
+}
+
+func TestUnmarshalInvalidUsageType(t *testing.T) {
+ var usages []UsageKind
+ invalidUsage := "invalid"
+ j := json.RawMessage(fmt.Sprintf(`["%s"]`, invalidUsage))
+ err := json.Unmarshal(j, &usages)
+ expectedErr := fmt.Sprintf("error while unmarshalling, UsageKind=%s not valid", invalidUsage)
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Fatalf("expected error unmarshalling struct, expected: %v, got: %v", expectedErr, err)
+ }
+}
diff --git a/test/cert_utils.go b/test/cert_utils.go
new file mode 100644
index 0000000..9e187d1
--- /dev/null
+++ b/test/cert_utils.go
@@ -0,0 +1,183 @@
+// Copyright 2022 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package test contains test utilities
+package test
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "math/big"
+ "net"
+ "net/url"
+ "time"
+)
+
+/*
+To use:
+
+rootCert, rootKey, _ := GenerateRootCa()
+subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey)
+leafCert, _, _ := GenerateLeafCert("subject", "oidc-issuer", subCert, subKey)
+
+roots := x509.NewCertPool()
+subs := x509.NewCertPool()
+roots.AddCert(rootCert)
+subs.AddCert(subCert)
+opts := x509.VerifyOptions{
+ Roots: roots,
+ Intermediates: subs,
+ KeyUsages: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageCodeSigning,
+ },
+}
+_, err := leafCert.Verify(opts)
+*/
+
+func createCertificate(template, parent *x509.Certificate, pub interface{}, priv crypto.Signer) (*x509.Certificate, error) {
+ certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv)
+ if err != nil {
+ return nil, err
+ }
+
+ return x509.ParseCertificate(certBytes)
+}
+
+// GenerateRootCa generates a test root CA
+func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) {
+ rootTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "sigstore",
+ Organization: []string{"sigstore.dev"},
+ },
+ NotBefore: time.Now().Add(-5 * time.Hour),
+ NotAfter: time.Now().Add(5 * time.Hour),
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cert, err := createCertificate(rootTemplate, rootTemplate, &priv.PublicKey, priv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return cert, priv, nil
+}
+
+// GenerateSubordinateCa generates a test intermediate CA
+func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+ subTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "sigstore-sub",
+ Organization: []string{"sigstore.dev"},
+ },
+ NotBefore: time.Now().Add(-2 * time.Minute),
+ NotAfter: time.Now().Add(2 * time.Hour),
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cert, err := createCertificate(subTemplate, rootTemplate, &priv.PublicKey, rootPriv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return cert, priv, nil
+}
+
+// GenerateLeafCert generates a test leaf certificate
+func GenerateLeafCert(subject, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer, exts ...pkix.Extension) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+ exts = append(exts, pkix.Extension{
+ // OID for OIDC Issuer extension
+ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+ Critical: false,
+ Value: []byte(oidcIssuer),
+ })
+ certTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ EmailAddresses: []string{subject},
+ NotBefore: time.Now().Add(-1 * time.Minute),
+ NotAfter: time.Now().Add(time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+ IsCA: false,
+ ExtraExtensions: exts,
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return cert, priv, nil
+}
+
+// GenerateLeafCertWithSubjectAlternateNames generates a test leaf certificate with the specified SANs
+func GenerateLeafCertWithSubjectAlternateNames(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+ certTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ EmailAddresses: emailAddresses,
+ DNSNames: dnsNames,
+ IPAddresses: ipAddresses,
+ URIs: uris,
+ NotBefore: time.Now().Add(-1 * time.Minute),
+ NotAfter: time.Now().Add(time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+ IsCA: false,
+ ExtraExtensions: []pkix.Extension{{
+ // OID for OIDC Issuer extension
+ Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+ Critical: false,
+ Value: []byte(oidcIssuer),
+ }},
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return cert, priv, nil
+}
diff --git a/test/e2e/dex-config.yml b/test/e2e/dex-config.yml
new file mode 100644
index 0000000..7fffca2
--- /dev/null
+++ b/test/e2e/dex-config.yml
@@ -0,0 +1,42 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+issuer: http://127.0.0.1:5556/auth
+
+storage:
+ type: memory
+
+# Configuration for the HTTP endpoints.
+web:
+ http: 0.0.0.0:5556
+
+logger:
+ level: debug
+
+oauth2:
+ responseTypes: [ "code" ]
+ skipApprovalScreen: true
+ alwaysShowLoginScreen: false
+
+staticClients:
+- id: sigstore
+ name: 'Sigstore Mock'
+ public: true
+
+connectors:
+- type: mockCallback
+ id: mock
+ name: Mock
+
diff --git a/test/e2e/dexidp.Dockerfile b/test/e2e/dexidp.Dockerfile
new file mode 100644
index 0000000..b77ede4
--- /dev/null
+++ b/test/e2e/dexidp.Dockerfile
@@ -0,0 +1,16 @@
+#
+# Copyright 2023 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http:# www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM ghcr.io/dexidp/dex:v2.37.0
diff --git a/test/e2e/docker-compose.yml b/test/e2e/docker-compose.yml
new file mode 100644
index 0000000..a05be86
--- /dev/null
+++ b/test/e2e/docker-compose.yml
@@ -0,0 +1,44 @@
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: "3.8"
+services:
+ vault:
+ build:
+ dockerfile: vault.Dockerfile
+ context: '.'
+ environment:
+ VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_TOKEN}
+ ports:
+ - 8200:8200
+ privileged: true
+ localstack:
+ build:
+ dockerfile: localstack.Dockerfile
+ context: '.'
+ ports:
+ - 4566:4566
+ environment:
+ - SERVICES=kms
+ dex:
+ build:
+ dockerfile: dexidp.Dockerfile
+ context: '.'
+ ports:
+ - "5556:5556"
+ volumes:
+ - ./dex-config.yml:/etc/dex/dex-config.yml:z
+ command: ["dex", "serve", "/etc/dex/dex-config.yml"]
+
diff --git a/test/e2e/e2e-test.sh b/test/e2e/e2e-test.sh
new file mode 100755
index 0000000..014e123
--- /dev/null
+++ b/test/e2e/e2e-test.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+#
+# Copyright 2021 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -ex
+
+cleanup() {
+ echo "cleanup"
+ docker-compose down
+}
+
+trap cleanup ERR
+
+export VAULT_TOKEN=testtoken
+export VAULT_ADDR=http://localhost:8200/
+
+echo "starting services"
+docker-compose config
+docker-compose up -d --build
+
+count=0
+
+echo -n "waiting up to 60 sec for system to start"
+until [ $(docker-compose ps vault | grep -c -e "Up" -e "running") == 1 -a $(docker-compose logs localstack | grep -c Ready) == 1 ];
+do
+ if [ $count -eq 12 ]; then
+ echo "! timeout reached"
+ exit 1
+ else
+ echo -n "."
+ sleep 5
+ let 'count+=1'
+ fi
+done
+
+sleep 5
+
+echo
+echo "running tests"
+
+export VAULT_TOKEN=testtoken
+export VAULT_ADDR=http://localhost:8200/
+
+export AWS_ACCESS_KEY_ID=test
+export AWS_SECRET_ACCESS_KEY=test
+export AWS_REGION=us-east-1
+export AWS_ENDPOINT=localhost:4566
+export AWS_TLS_INSECURE_SKIP_VERIFY=1
+
+export OIDC_ISSUER=http://127.0.0.1:5556/auth
+export OIDC_ID=sigstore
+
+go test -tags e2e -count=1 ../../...
+cd ../..
+for dir in $(find pkg/signature/kms/ -name go.mod | sed -e 's/go\.mod//g')
+do
+ cd $dir && go test -tags e2e -count=1 ./...
+ cd ../../../../
+done
+cd test/e2e
+
+cleanup
diff --git a/test/e2e/localstack.Dockerfile b/test/e2e/localstack.Dockerfile
new file mode 100644
index 0000000..0956aae
--- /dev/null
+++ b/test/e2e/localstack.Dockerfile
@@ -0,0 +1,16 @@
+#
+# Copyright 2023 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http:# www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM docker.io/localstack/localstack:3.0.2
diff --git a/test/e2e/vault.Dockerfile b/test/e2e/vault.Dockerfile
new file mode 100644
index 0000000..89b8b8a
--- /dev/null
+++ b/test/e2e/vault.Dockerfile
@@ -0,0 +1,16 @@
+#
+# Copyright 2023 The Sigstore Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http:# www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM docker.io/hashicorp/vault:1.15.4
diff --git a/test/fuzz/README.md b/test/fuzz/README.md
new file mode 100644
index 0000000..37c28b7
--- /dev/null
+++ b/test/fuzz/README.md
@@ -0,0 +1,20 @@
+### Fuzzing
+
+The fuzzing of sigstore uses [go-fuzz](https://github.com/dvyukov/go-fuzz) for fuzzing.
+It is integrated into oss-fuzz https://github.com/google/oss-fuzz/pull/6890 for fuzzing continuously.
+
+#### Why not use go 1.18 fuzzing?
+The go-fuzz can be compatible with `libfuzzer`, which is supported by `oss-fuzz`.
+The go 1.18 doesn't have support for external fuzzer formats yet.
+
+#### What is corpus?
+>A set of inputs for a fuzz target. In most contexts, it refers to a set of minimal test inputs that generate maximal code coverage.
+https://google.github.io/clusterfuzz/reference/glossary/#corpus
+
+#### How do I run the fuzzer?
+1. `make fuzz`
+2. `go-fuzz -bin=signature-fuzz.zip -func FuzzED25529SignerVerfier`
+3. An example to use the `libfuzzer` `go-fuzz-build --libfuzzer -func FuzzRSASignerVerfier ./signature/...`
+and `clang -fsanitize=fuzzer reflect-fuzz.a -o fmt.libfuzzer`
+4. The `libfuzzer` option requires `linux`.
+
diff --git a/test/fuzz/corpus/ecdsa/0 b/test/fuzz/corpus/ecdsa/0
new file mode 100644
index 0000000..5b94388
--- /dev/null
+++ b/test/fuzz/corpus/ecdsa/0
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTnXFzkBUgER/32YRahg+YyJseS9Gwr
+G982a+OzogidAlUuzGXGLPuziHcLcbHQDB3MxLWIiRDSGgjGfYmbjIpOAAAAsGyAIeBsgC
+HgAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOdcXOQFSARH/fZh
+FqGD5jImx5L0bCsb3zZr47OiCJ0CVS7MZcYs+7OIdwtxsdAMHczEtYiJENIaCMZ9iZuMik
+4AAAAhALSGPEUKRuducAVHwVHfqAG3jupDJQnrdcOFeUUW0HmhAAAAFnNhbW15QG5ldy1s
+YXJnZS1kZXZib3gB
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/ecdsa/1 b/test/fuzz/corpus/ecdsa/1
new file mode 100644
index 0000000..0e9deff
--- /dev/null
+++ b/test/fuzz/corpus/ecdsa/1
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQn8+kqeep6yWIiuxiZE46+CIgXQaVZ
+b/drvUj4aimMkqNncvPpYUkEczyJzI8+hpYuIMuXj3TlsK3jwdSHLiUyAAAAsPqPcNH6j3
+DRAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCfz6Sp56nrJYiK7
+GJkTjr4IiBdBpVlv92u9SPhqKYySo2dy8+lhSQRzPInMjz6Gli4gy5ePdOWwrePB1IcuJT
+IAAAAgU2zRLbT+EaaRJdXrugpDhi6rxeli730rLFGPUhJXpsAAAAAWc2FtbXlAbmV3LWxh
+cmdlLWRldmJveAEC
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/ecdsa/2 b/test/fuzz/corpus/ecdsa/2
new file mode 100644
index 0000000..21039f8
--- /dev/null
+++ b/test/fuzz/corpus/ecdsa/2
@@ -0,0 +1,3 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/ed25519/0 b/test/fuzz/corpus/ed25519/0
new file mode 100644
index 0000000..18d85db
--- /dev/null
+++ b/test/fuzz/corpus/ed25519/0
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCv5p8foWdwQ6YKC8BP47vgm4hGKs7a0wEvdLMELWCRtgAAAKBCmyBmQpsg
+ZgAAAAtzc2gtZWQyNTUxOQAAACCv5p8foWdwQ6YKC8BP47vgm4hGKs7a0wEvdLMELWCRtg
+AAAEAKPu7W+HvvKHBE6CkKSeTyCQ7XX18PvCf1iesnYXiHh6/mnx+hZ3BDpgoLwE/ju+Cb
+iEYqztrTAS90swQtYJG2AAAAFnNhbW15QG5ldy1sYXJnZS1kZXZib3gBAgMEBQYH
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/ed25519/1 b/test/fuzz/corpus/ed25519/1
new file mode 100644
index 0000000..9fdd821
--- /dev/null
+++ b/test/fuzz/corpus/ed25519/1
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACClVDDJyLbCLxwTQ6okIn1Wpfv2K4Fn33EW7xXNiwF7EgAAAKB0N5GVdDeR
+lQAAAAtzc2gtZWQyNTUxOQAAACClVDDJyLbCLxwTQ6okIn1Wpfv2K4Fn33EW7xXNiwF7Eg
+AAAECfDvH57XG4lNcADy8kb1f3oAPJDyaGD8hgdTsLtQ4176VUMMnItsIvHBNDqiQifVal
++/YrgWffcRbvFc2LAXsSAAAAFnNhbW15QG5ldy1sYXJnZS1kZXZib3gBAgMEBQYH
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/ed25519/2 b/test/fuzz/corpus/ed25519/2
new file mode 100644
index 0000000..814db21
--- /dev/null
+++ b/test/fuzz/corpus/ed25519/2
@@ -0,0 +1,3 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+A
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/fuzz/corpus/pem/0 b/test/fuzz/corpus/pem/0
new file mode 100644
index 0000000..127f559
--- /dev/null
+++ b/test/fuzz/corpus/pem/0
@@ -0,0 +1,2 @@
+-----BEGIN -----
+-----END -----
diff --git a/test/fuzz/corpus/pem/00aa063b6a11aa6532146400d321a29ac57772df-6 b/test/fuzz/corpus/pem/00aa063b6a11aa6532146400d321a29ac57772df-6
new file mode 100644
index 0000000..0ce4c2d
--- /dev/null
+++ b/test/fuzz/corpus/pem/00aa063b6a11aa6532146400d321a29ac57772df-6
@@ -0,0 +1,2 @@
+-----BEGIN -----
+ÚÀ: \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/039e15380cca84b1e0566d3212c336e5f3d83b07-2 b/test/fuzz/corpus/pem/039e15380cca84b1e0566d3212c336e5f3d83b07-2
new file mode 100644
index 0000000..70a7b38
--- /dev/null
+++ b/test/fuzz/corpus/pem/039e15380cca84b1e0566d3212c336e5f3d83b07-2
@@ -0,0 +1 @@
+-----BEGIN Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/0ad367ffa4f81383a24ef6abe1dc34ea4f77c25f-3 b/test/fuzz/corpus/pem/0ad367ffa4f81383a24ef6abe1dc34ea4f77c25f-3
new file mode 100644
index 0000000..94b1497
--- /dev/null
+++ b/test/fuzz/corpus/pem/0ad367ffa4f81383a24ef6abe1dc34ea4f77c25f-3
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+:¥
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/0b80059edfb2da2efb180e4d86c3975fb0e1eaba-8 b/test/fuzz/corpus/pem/0b80059edfb2da2efb180e4d86c3975fb0e1eaba-8
new file mode 100644
index 0000000..c73a393
--- /dev/null
+++ b/test/fuzz/corpus/pem/0b80059edfb2da2efb180e4d86c3975fb0e1eaba-8
@@ -0,0 +1,2 @@
+-----BEGIN -----
+:⿽ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1 b/test/fuzz/corpus/pem/1
new file mode 100644
index 0000000..9734784
--- /dev/null
+++ b/test/fuzz/corpus/pem/1
@@ -0,0 +1,7 @@
+-----BEGIN foo-----
+:
+aaa: bbb
+foo:
+
+YmFyYmF6
+-----END foo-----
diff --git a/test/fuzz/corpus/pem/126c5ff159d29cb0be86edd93ac59fea84eeb8f9-2 b/test/fuzz/corpus/pem/126c5ff159d29cb0be86edd93ac59fea84eeb8f9-2
new file mode 100644
index 0000000..0dff2b7
--- /dev/null
+++ b/test/fuzz/corpus/pem/126c5ff159d29cb0be86edd93ac59fea84eeb8f9-2
@@ -0,0 +1,9 @@
+--
+--BEGIN foo-----
+:
+aaa: bbb
+fbb
+f
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/143b2870fa376ee37d700e46bad6b95da7cc6bf8-5 b/test/fuzz/corpus/pem/143b2870fa376ee37d700e46bad6b95da7cc6bf8-5
new file mode 100644
index 0000000..b9a012e
--- /dev/null
+++ b/test/fuzz/corpus/pem/143b2870fa376ee37d700e46bad6b95da7cc6bf8-5
@@ -0,0 +1 @@
+-----BEGIN ¿½¿½ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/16bef3faa75283175c0f6ee02820f32cfda7b5d0-4 b/test/fuzz/corpus/pem/16bef3faa75283175c0f6ee02820f32cfda7b5d0-4
new file mode 100644
index 0000000..0eb189c
--- /dev/null
+++ b/test/fuzz/corpus/pem/16bef3faa75283175c0f6ee02820f32cfda7b5d0-4
Binary files differ
diff --git a/test/fuzz/corpus/pem/17dc2d28046108693e97a37eadcd107aa04870e3-3 b/test/fuzz/corpus/pem/17dc2d28046108693e97a37eadcd107aa04870e3-3
new file mode 100644
index 0000000..c5e7029
--- /dev/null
+++ b/test/fuzz/corpus/pem/17dc2d28046108693e97a37eadcd107aa04870e3-3
@@ -0,0 +1,3 @@
+-----BEGIN -----
+¿:Î
+Î:¿ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1b83cfdc173ac48684d838336c7c1a55280e0acf-3 b/test/fuzz/corpus/pem/1b83cfdc173ac48684d838336c7c1a55280e0acf-3
new file mode 100644
index 0000000..20fd431
--- /dev/null
+++ b/test/fuzz/corpus/pem/1b83cfdc173ac48684d838336c7c1a55280e0acf-3
@@ -0,0 +1 @@
+-----BEGIN Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1bb1cb35daa2c2c232ace34d07f6067ac86da2ef-1 b/test/fuzz/corpus/pem/1bb1cb35daa2c2c232ace34d07f6067ac86da2ef-1
new file mode 100644
index 0000000..a99b7b1
--- /dev/null
+++ b/test/fuzz/corpus/pem/1bb1cb35daa2c2c232ace34d07f6067ac86da2ef-1
@@ -0,0 +1,5 @@
+-----BEGIN foo-----
+fooT
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1bfab2d6d235f0e89b0a2c9abb0c3afbb61ac831-1 b/test/fuzz/corpus/pem/1bfab2d6d235f0e89b0a2c9abb0c3afbb61ac831-1
new file mode 100644
index 0000000..39fe8cf
--- /dev/null
+++ b/test/fuzz/corpus/pem/1bfab2d6d235f0e89b0a2c9abb0c3afbb61ac831-1
@@ -0,0 +1,2 @@
+-----BEGIN -----
+-----END \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1c25ea2b045f73b72084eacf83a91a1d4e536194-1 b/test/fuzz/corpus/pem/1c25ea2b045f73b72084eacf83a91a1d4e536194-1
new file mode 100644
index 0000000..2f3ca95
--- /dev/null
+++ b/test/fuzz/corpus/pem/1c25ea2b045f73b72084eacf83a91a1d4e536194-1
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ÿmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/1c293c6c956336a2e08d55087f43d331b571623e-6 b/test/fuzz/corpus/pem/1c293c6c956336a2e08d55087f43d331b571623e-6
new file mode 100644
index 0000000..2f6c9b7
--- /dev/null
+++ b/test/fuzz/corpus/pem/1c293c6c956336a2e08d55087f43d331b571623e-6
@@ -0,0 +1,7 @@
+063349815200
+-----BEGIN -----
+:
+-aa: bbb6u-----
+:
+
+aaa:aaa: bb \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/2264b923a08d8b8784e39896da6d85095075eb2a-2 b/test/fuzz/corpus/pem/2264b923a08d8b8784e39896da6d85095075eb2a-2
new file mode 100644
index 0000000..48d92b5
--- /dev/null
+++ b/test/fuzz/corpus/pem/2264b923a08d8b8784e39896da6d85095075eb2a-2
@@ -0,0 +1,9 @@
+--
+--BEGIN1foo-----
+:
+aaa: bbb6
+
+foo:
+
+YmFyYmF6
+-----END foo--- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/242cc35d953670092808e9d961d5511a818e3b6d-1 b/test/fuzz/corpus/pem/242cc35d953670092808e9d961d5511a818e3b6d-1
new file mode 100644
index 0000000..367bf77
--- /dev/null
+++ b/test/fuzz/corpus/pem/242cc35d953670092808e9d961d5511a818e3b6d-1
@@ -0,0 +1,2 @@
+-----BEGIN -----
+-----END ----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/28023cd89a1827d1e70fe20215bdcf36b71fa499-4 b/test/fuzz/corpus/pem/28023cd89a1827d1e70fe20215bdcf36b71fa499-4
new file mode 100644
index 0000000..b0cf25e
--- /dev/null
+++ b/test/fuzz/corpus/pem/28023cd89a1827d1e70fe20215bdcf36b71fa499-4
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+unK6newline in format does not match input
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/32d7f4d00dbb5c8f962ef8ff8a31629ccc721aa1-4 b/test/fuzz/corpus/pem/32d7f4d00dbb5c8f962ef8ff8a31629ccc721aa1-4
new file mode 100644
index 0000000..a270369
--- /dev/null
+++ b/test/fuzz/corpus/pem/32d7f4d00dbb5c8f962ef8ff8a31629ccc721aa1-4
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+::¥
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/3fb4188486c64a812260611d749dc9fc7ca15da3-1 b/test/fuzz/corpus/pem/3fb4188486c64a812260611d749dc9fc7ca15da3-1
new file mode 100644
index 0000000..e732dc3
--- /dev/null
+++ b/test/fuzz/corpus/pem/3fb4188486c64a812260611d749dc9fc7ca15da3-1
@@ -0,0 +1,2 @@
+-----BEGIN foo-----
+-----END foo------ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/40bdf4011e9fafe0e9b1b4e7cd4eef84a669cb06-2 b/test/fuzz/corpus/pem/40bdf4011e9fafe0e9b1b4e7cd4eef84a669cb06-2
new file mode 100644
index 0000000..8b482e1
--- /dev/null
+++ b/test/fuzz/corpus/pem/40bdf4011e9fafe0e9b1b4e7cd4eef84a669cb06-2
@@ -0,0 +1,6 @@
+-----BEGIN foo-----
+:
+:
+:
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/46c44389cde762038f7ed276eeb5eb16287b41b0-2 b/test/fuzz/corpus/pem/46c44389cde762038f7ed276eeb5eb16287b41b0-2
new file mode 100644
index 0000000..3baa281
--- /dev/null
+++ b/test/fuzz/corpus/pem/46c44389cde762038f7ed276eeb5eb16287b41b0-2
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+:ߧ
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/48feeb9a655d20bfdfd263ba92fdbf6600bba7fa-5 b/test/fuzz/corpus/pem/48feeb9a655d20bfdfd263ba92fdbf6600bba7fa-5
new file mode 100644
index 0000000..5d68f63
--- /dev/null
+++ b/test/fuzz/corpus/pem/48feeb9a655d20bfdfd263ba92fdbf6600bba7fa-5
@@ -0,0 +1,10 @@
+-----BEGIN -----
+
+--
+-7035266621122884
+Gaa: bbbffbb
+
+
+b agW|
+mFyYmF6
+--key has be \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/4a339febca054df203b4627e14d0016fdd518143-7 b/test/fuzz/corpus/pem/4a339febca054df203b4627e14d0016fdd518143-7
new file mode 100644
index 0000000..8b07f71
--- /dev/null
+++ b/test/fuzz/corpus/pem/4a339febca054df203b4627e14d0016fdd518143-7
@@ -0,0 +1,4 @@
+
+-----BEGIN -----
+
+aaa:aaa: bb \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/4bc1c12e85085d1b3e0f0b2551d21422067a8b7b-4 b/test/fuzz/corpus/pem/4bc1c12e85085d1b3e0f0b2551d21422067a8b7b-4
new file mode 100644
index 0000000..730b14c
--- /dev/null
+++ b/test/fuzz/corpus/pem/4bc1c12e85085d1b3e0f0b2551d21422067a8b7b-4
@@ -0,0 +1 @@
+-----BEGIN ï½ï \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/4c1795357af14c6c54d4fa99b6fad04f7a431df9-1 b/test/fuzz/corpus/pem/4c1795357af14c6c54d4fa99b6fad04f7a431df9-1
new file mode 100644
index 0000000..224ffc0
--- /dev/null
+++ b/test/fuzz/corpus/pem/4c1795357af14c6c54d4fa99b6fad04f7a431df9-1
@@ -0,0 +1,7 @@
+-----BEGIN foo-----
+:
+:
+:
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/4d47644faa08a59937b8f013b99ca3b5ac4fffeb-5 b/test/fuzz/corpus/pem/4d47644faa08a59937b8f013b99ca3b5ac4fffeb-5
new file mode 100644
index 0000000..f6a22d6
--- /dev/null
+++ b/test/fuzz/corpus/pem/4d47644faa08a59937b8f013b99ca3b5ac4fffeb-5
@@ -0,0 +1,2 @@
+-----BEGIN -----
+:Έ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/514b139e4ce9d356890d06f4af51df2f1afc3c13-2 b/test/fuzz/corpus/pem/514b139e4ce9d356890d06f4af51df2f1afc3c13-2
new file mode 100644
index 0000000..d5865a9
--- /dev/null
+++ b/test/fuzz/corpus/pem/514b139e4ce9d356890d06f4af51df2f1afc3c13-2
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+fooT
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/53371d74058793bbeea98a1ed896d235647ee54f-2 b/test/fuzz/corpus/pem/53371d74058793bbeea98a1ed896d235647ee54f-2
new file mode 100644
index 0000000..df4eedb
--- /dev/null
+++ b/test/fuzz/corpus/pem/53371d74058793bbeea98a1ed896d235647ee54f-2
@@ -0,0 +1,6 @@
+-----BEGIN foo-----
+aabbb
+foo
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/55e49de57989fda88102051841cc1837835663f9-4 b/test/fuzz/corpus/pem/55e49de57989fda88102051841cc1837835663f9-4
new file mode 100644
index 0000000..2a4e882
--- /dev/null
+++ b/test/fuzz/corpus/pem/55e49de57989fda88102051841cc1837835663f9-4
@@ -0,0 +1,2 @@
+-----BEGIN -----
+ï¿:oÎ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/5727a70cf1fd7c94c5e8b67a644718be30ef4ba1-6 b/test/fuzz/corpus/pem/5727a70cf1fd7c94c5e8b67a644718be30ef4ba1-6
new file mode 100644
index 0000000..febbcbc
--- /dev/null
+++ b/test/fuzz/corpus/pem/5727a70cf1fd7c94c5e8b67a644718be30ef4ba1-6
@@ -0,0 +1,2 @@
+-----BEGIN -----
+N : \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/57d856a3a72f72aa0aff7cef1463125e85ebb717-6 b/test/fuzz/corpus/pem/57d856a3a72f72aa0aff7cef1463125e85ebb717-6
new file mode 100644
index 0000000..50bb771
--- /dev/null
+++ b/test/fuzz/corpus/pem/57d856a3a72f72aa0aff7cef1463125e85ebb717-6
@@ -0,0 +1,12 @@
+-----BEGIN -----
+
+-
+:
+---367035266621122884
+:
+Gaa: bbb
+fbb
+
+b agW|
+mFyYmF6
+--- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/5e41a95a406ec15af97f98b309ced43f926dcdeb-2 b/test/fuzz/corpus/pem/5e41a95a406ec15af97f98b309ced43f926dcdeb-2
new file mode 100644
index 0000000..b7aa481
--- /dev/null
+++ b/test/fuzz/corpus/pem/5e41a95a406ec15af97f98b309ced43f926dcdeb-2
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+aabbb
+fooYmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/6177b5c12494f44da46e86b545f6bf16e6d9a37f-2 b/test/fuzz/corpus/pem/6177b5c12494f44da46e86b545f6bf16e6d9a37f-2
new file mode 100644
index 0000000..4efe09e
--- /dev/null
+++ b/test/fuzz/corpus/pem/6177b5c12494f44da46e86b545f6bf16e6d9a37f-2
@@ -0,0 +1,2 @@
+
+-----BEGIN \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/64a855b2b07b06e17e543094b2fea14fc3684e43-2 b/test/fuzz/corpus/pem/64a855b2b07b06e17e543094b2fea14fc3684e43-2
new file mode 100644
index 0000000..01cf13e
--- /dev/null
+++ b/test/fuzz/corpus/pem/64a855b2b07b06e17e543094b2fea14fc3684e43-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ÿ6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/6ba8ad653a36b5f8d8a45c659af0e5b61ec552d8-1 b/test/fuzz/corpus/pem/6ba8ad653a36b5f8d8a45c659af0e5b61ec552d8-1
new file mode 100644
index 0000000..7b22ee1
--- /dev/null
+++ b/test/fuzz/corpus/pem/6ba8ad653a36b5f8d8a45c659af0e5b61ec552d8-1
@@ -0,0 +1,8 @@
+--
+--BEGIN foo-----
+:
+aaa: bbb
+foo:
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/73f7efa4dc5b350d91f9dec193009afe6089be97-1 b/test/fuzz/corpus/pem/73f7efa4dc5b350d91f9dec193009afe6089be97-1
new file mode 100644
index 0000000..47629f8
--- /dev/null
+++ b/test/fuzz/corpus/pem/73f7efa4dc5b350d91f9dec193009afe6089be97-1
@@ -0,0 +1,3 @@
+:
+
+YmFyYmF6
diff --git a/test/fuzz/corpus/pem/75b35c51db6d069ba0ad6051524168aaf2594bfa-3 b/test/fuzz/corpus/pem/75b35c51db6d069ba0ad6051524168aaf2594bfa-3
new file mode 100644
index 0000000..d6b04fc
--- /dev/null
+++ b/test/fuzz/corpus/pem/75b35c51db6d069ba0ad6051524168aaf2594bfa-3
@@ -0,0 +1,3 @@
+-----BEGIN -----
+¿:Î
+¿ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/77ac6f147fab168ade8c977a5c25f2dc27ae5a52-4 b/test/fuzz/corpus/pem/77ac6f147fab168ade8c977a5c25f2dc27ae5a52-4
new file mode 100644
index 0000000..2bb84f1
--- /dev/null
+++ b/test/fuzz/corpus/pem/77ac6f147fab168ade8c977a5c25f2dc27ae5a52-4
@@ -0,0 +1 @@
+-----BEGIN -¿½Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/78d3194b5b93a6448ebbd539a1f12cb5273b00eb-1 b/test/fuzz/corpus/pem/78d3194b5b93a6448ebbd539a1f12cb5273b00eb-1
new file mode 100644
index 0000000..5dc92f7
--- /dev/null
+++ b/test/fuzz/corpus/pem/78d3194b5b93a6448ebbd539a1f12cb5273b00eb-1
@@ -0,0 +1,2 @@
+-----BEGIN -----
+-----END o---- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/7a1a027faf971e46bc3fc08280e58ea47d46696c-9 b/test/fuzz/corpus/pem/7a1a027faf971e46bc3fc08280e58ea47d46696c-9
new file mode 100644
index 0000000..59a957e
--- /dev/null
+++ b/test/fuzz/corpus/pem/7a1a027faf971e46bc3fc08280e58ea47d46696c-9
@@ -0,0 +1,2 @@
+-----BEGIN -----
+ܮ:⿽ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/800630235c61d15f3d21c1e72c1d4fa9c2452a06-3 b/test/fuzz/corpus/pem/800630235c61d15f3d21c1e72c1d4fa9c2452a06-3
new file mode 100644
index 0000000..b56157f
--- /dev/null
+++ b/test/fuzz/corpus/pem/800630235c61d15f3d21c1e72c1d4fa9c2452a06-3
@@ -0,0 +1,2 @@
+-----BEGIN -----
+½¿ï:fooÎ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/83a880604a0db06142e5c50c06edcc7568c42f34-7 b/test/fuzz/corpus/pem/83a880604a0db06142e5c50c06edcc7568c42f34-7
new file mode 100644
index 0000000..b63b16e
--- /dev/null
+++ b/test/fuzz/corpus/pem/83a880604a0db06142e5c50c06edcc7568c42f34-7
@@ -0,0 +1,2 @@
+-----BEGIN -----
+€ : \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/84d7f874ed4f1821d5188f8bfc5a970af2a8f13c-1 b/test/fuzz/corpus/pem/84d7f874ed4f1821d5188f8bfc5a970af2a8f13c-1
new file mode 100644
index 0000000..c473d1c
--- /dev/null
+++ b/test/fuzz/corpus/pem/84d7f874ed4f1821d5188f8bfc5a970af2a8f13c-1
@@ -0,0 +1,2 @@
+-----BEGIN -----
+fooÎ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/86156bb87353421d7a4dbd22ee8c87da90319c85-3 b/test/fuzz/corpus/pem/86156bb87353421d7a4dbd22ee8c87da90319c85-3
new file mode 100644
index 0000000..7ac295c
--- /dev/null
+++ b/test/fuzz/corpus/pem/86156bb87353421d7a4dbd22ee8c87da90319c85-3
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/864196a44d1c943bc3e3426a550c33836e24114c-4 b/test/fuzz/corpus/pem/864196a44d1c943bc3e3426a550c33836e24114c-4
new file mode 100644
index 0000000..d5cf033
--- /dev/null
+++ b/test/fuzz/corpus/pem/864196a44d1c943bc3e3426a550c33836e24114c-4
@@ -0,0 +1,2 @@
+-----BEGIN -----
+�:oΠ\ No newline at end of file
diff --git a/test/fuzz/corpus/pem/8684efa95476c052138696132158f32eba800ce1-3 b/test/fuzz/corpus/pem/8684efa95476c052138696132158f32eba800ce1-3
new file mode 100644
index 0000000..f9e19f4
--- /dev/null
+++ b/test/fuzz/corpus/pem/8684efa95476c052138696132158f32eba800ce1-3
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+:½¿ï
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/879034c826380a27c9b61e8549decc9c6d080510-3 b/test/fuzz/corpus/pem/879034c826380a27c9b61e8549decc9c6d080510-3
new file mode 100644
index 0000000..00c9e63
--- /dev/null
+++ b/test/fuzz/corpus/pem/879034c826380a27c9b61e8549decc9c6d080510-3
@@ -0,0 +1,2 @@
+-----BEGIN -----
+:Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/8cea0a2091af85c9740e519c9b2bcbbef33223db-3 b/test/fuzz/corpus/pem/8cea0a2091af85c9740e519c9b2bcbbef33223db-3
new file mode 100644
index 0000000..4af6dc3
--- /dev/null
+++ b/test/fuzz/corpus/pem/8cea0a2091af85c9740e519c9b2bcbbef33223db-3
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+:ا
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/8deec9a0749c64dc6541da31b465762e2bf11341-6 b/test/fuzz/corpus/pem/8deec9a0749c64dc6541da31b465762e2bf11341-6
new file mode 100644
index 0000000..6aada8e
--- /dev/null
+++ b/test/fuzz/corpus/pem/8deec9a0749c64dc6541da31b465762e2bf11341-6
@@ -0,0 +1,2 @@
+-----BEGIN -----
+¿:Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/8f51cef2a076c2850e056835015a018e42fd7168-3 b/test/fuzz/corpus/pem/8f51cef2a076c2850e056835015a018e42fd7168-3
new file mode 100644
index 0000000..8752ed4
--- /dev/null
+++ b/test/fuzz/corpus/pem/8f51cef2a076c2850e056835015a018e42fd7168-3
Binary files differ
diff --git a/test/fuzz/corpus/pem/95c694ba9fc0fc5cb833114b9dc4ed6ae7644c79-3 b/test/fuzz/corpus/pem/95c694ba9fc0fc5cb833114b9dc4ed6ae7644c79-3
new file mode 100644
index 0000000..c5db611
--- /dev/null
+++ b/test/fuzz/corpus/pem/95c694ba9fc0fc5cb833114b9dc4ed6ae7644c79-3
@@ -0,0 +1 @@
+-----BEGIN
diff --git a/test/fuzz/corpus/pem/97d56637605bf32936e7344bc0839dc0d396d002-2 b/test/fuzz/corpus/pem/97d56637605bf32936e7344bc0839dc0d396d002-2
new file mode 100644
index 0000000..4390342
--- /dev/null
+++ b/test/fuzz/corpus/pem/97d56637605bf32936e7344bc0839dc0d396d002-2
@@ -0,0 +1,2 @@
+-----BEGIN -----
+f \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/9c65264e4b0b01adedc9ca30fbd20d7a3b6e4686-7 b/test/fuzz/corpus/pem/9c65264e4b0b01adedc9ca30fbd20d7a3b6e4686-7
new file mode 100644
index 0000000..eb97ace
--- /dev/null
+++ b/test/fuzz/corpus/pem/9c65264e4b0b01adedc9ca30fbd20d7a3b6e4686-7
@@ -0,0 +1,2 @@
+-----BEGIN -----
+:� \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/9d6ffa203cf753ab82943975ac34ae76b23a9503-4 b/test/fuzz/corpus/pem/9d6ffa203cf753ab82943975ac34ae76b23a9503-4
new file mode 100644
index 0000000..ace89d0
--- /dev/null
+++ b/test/fuzz/corpus/pem/9d6ffa203cf753ab82943975ac34ae76b23a9503-4
@@ -0,0 +1,2 @@
+-----BEGIN
+-----BEGIN \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a06a30151c284ca655d30170ab5143c9c6c2df8a-4 b/test/fuzz/corpus/pem/a06a30151c284ca655d30170ab5143c9c6c2df8a-4
new file mode 100644
index 0000000..2b96b5a
--- /dev/null
+++ b/test/fuzz/corpus/pem/a06a30151c284ca655d30170ab5143c9c6c2df8a-4
@@ -0,0 +1,12 @@
+-----BEGIN -----
+
+--
+:
+---367035266621122884
+:
+Gaa: bbb
+fbb
+f
+b agW|
+mFyYmF6
+-----END foo \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a2004bc3882e0e0770e3abcf9768fa5c3d642041-2 b/test/fuzz/corpus/pem/a2004bc3882e0e0770e3abcf9768fa5c3d642041-2
new file mode 100644
index 0000000..8ff216f
--- /dev/null
+++ b/test/fuzz/corpus/pem/a2004bc3882e0e0770e3abcf9768fa5c3d642041-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+FyF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a345640366934585f446163074acf57c275b597b-3 b/test/fuzz/corpus/pem/a345640366934585f446163074acf57c275b597b-3
new file mode 100644
index 0000000..84acd68
--- /dev/null
+++ b/test/fuzz/corpus/pem/a345640366934585f446163074acf57c275b597b-3
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a5d8e3d21838da80e7830e4d3f608c64e0cc4a32-5 b/test/fuzz/corpus/pem/a5d8e3d21838da80e7830e4d3f608c64e0cc4a32-5
new file mode 100644
index 0000000..a2c22c6
--- /dev/null
+++ b/test/fuzz/corpus/pem/a5d8e3d21838da80e7830e4d3f608c64e0cc4a32-5
@@ -0,0 +1,12 @@
+-----BEGIN -----
+
+--
+:
+035266621122884
+:
+Gaa: bbb
+fbb
+
+b agW|
+mFyYmF6
+-----END \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a6f71960b5a3fe7787c0792d08963e4272bc76de-2 b/test/fuzz/corpus/pem/a6f71960b5a3fe7787c0792d08963e4272bc76de-2
new file mode 100644
index 0000000..e7b4c74
--- /dev/null
+++ b/test/fuzz/corpus/pem/a6f71960b5a3fe7787c0792d08963e4272bc76de-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+fooTYmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/a8a0c9af9edc2f5aa62fdb4355692d843605a6af-2 b/test/fuzz/corpus/pem/a8a0c9af9edc2f5aa62fdb4355692d843605a6af-2
new file mode 100644
index 0000000..02a3030
--- /dev/null
+++ b/test/fuzz/corpus/pem/a8a0c9af9edc2f5aa62fdb4355692d843605a6af-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+uncated base 128 integermFyYmK6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/ac2fde6a33a2f3dc35cce4f7a02fa2c87f2c09e8-2 b/test/fuzz/corpus/pem/ac2fde6a33a2f3dc35cce4f7a02fa2c87f2c09e8-2
new file mode 100644
index 0000000..b68a7b4
--- /dev/null
+++ b/test/fuzz/corpus/pem/ac2fde6a33a2f3dc35cce4f7a02fa2c87f2c09e8-2
@@ -0,0 +1,3 @@
+-----BEGIN -----
+¿:Î
+:¿ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/afb44221440eed241164b63ef1c14e0b5b0ffaad-3 b/test/fuzz/corpus/pem/afb44221440eed241164b63ef1c14e0b5b0ffaad-3
new file mode 100644
index 0000000..a6ebb78
--- /dev/null
+++ b/test/fuzz/corpus/pem/afb44221440eed241164b63ef1c14e0b5b0ffaad-3
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ted 128 integermFyYmK6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/b9f075ae1a41e19f9e7e6176ab3c0dd1ef967161-5 b/test/fuzz/corpus/pem/b9f075ae1a41e19f9e7e6176ab3c0dd1ef967161-5
new file mode 100644
index 0000000..fb57eff
--- /dev/null
+++ b/test/fuzz/corpus/pem/b9f075ae1a41e19f9e7e6176ab3c0dd1ef967161-5
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+unK6newlineinformatdoes not beEfFgGvmatch input
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/bb55dfd080331ac9fed155558a8a5adb134b4e7f-2 b/test/fuzz/corpus/pem/bb55dfd080331ac9fed155558a8a5adb134b4e7f-2
new file mode 100644
index 0000000..8f76bb9
--- /dev/null
+++ b/test/fuzz/corpus/pem/bb55dfd080331ac9fed155558a8a5adb134b4e7f-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ nFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/cd1b2b5fb3598b0c314a6d73a8e0a7073b77e3da-2 b/test/fuzz/corpus/pem/cd1b2b5fb3598b0c314a6d73a8e0a7073b77e3da-2
new file mode 100644
index 0000000..fa943be
--- /dev/null
+++ b/test/fuzz/corpus/pem/cd1b2b5fb3598b0c314a6d73a8e0a7073b77e3da-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ÿ
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d0663f723b7260b2f16b364dbd67f2dd2c32ff00-2 b/test/fuzz/corpus/pem/d0663f723b7260b2f16b364dbd67f2dd2c32ff00-2
new file mode 100644
index 0000000..424d9e2
--- /dev/null
+++ b/test/fuzz/corpus/pem/d0663f723b7260b2f16b364dbd67f2dd2c32ff00-2
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+ÉÞ
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d13f816660c54829c658000f1b61b7b4b50f10b1-3 b/test/fuzz/corpus/pem/d13f816660c54829c658000f1b61b7b4b50f10b1-3
new file mode 100644
index 0000000..70b0a37
--- /dev/null
+++ b/test/fuzz/corpus/pem/d13f816660c54829c658000f1b61b7b4b50f10b1-3
@@ -0,0 +1,3 @@
+-----BEGIN -----
+ç:Î
+:¿ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d51c3d46b0aef042186906efb3e342bbd37676dc-5 b/test/fuzz/corpus/pem/d51c3d46b0aef042186906efb3e342bbd37676dc-5
new file mode 100644
index 0000000..8dfecc7
--- /dev/null
+++ b/test/fuzz/corpus/pem/d51c3d46b0aef042186906efb3e342bbd37676dc-5
@@ -0,0 +1,2 @@
+-----BEGIN -----
+¿:oÎ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d680b4047f01df32c39741c6b9fda24bcd8b2ee5-3 b/test/fuzz/corpus/pem/d680b4047f01df32c39741c6b9fda24bcd8b2ee5-3
new file mode 100644
index 0000000..92dfccf
--- /dev/null
+++ b/test/fuzz/corpus/pem/d680b4047f01df32c39741c6b9fda24bcd8b2ee5-3
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+unK6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d76412e8a07f0804d7f8b728da50966f4663b728-3 b/test/fuzz/corpus/pem/d76412e8a07f0804d7f8b728da50966f4663b728-3
new file mode 100644
index 0000000..f356b3b
--- /dev/null
+++ b/test/fuzz/corpus/pem/d76412e8a07f0804d7f8b728da50966f4663b728-3
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+uncated base 028 integermFyYmK6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d89ae5c348e4931f4b5267efcdb798e42c248f5d-5 b/test/fuzz/corpus/pem/d89ae5c348e4931f4b5267efcdb798e42c248f5d-5
new file mode 100644
index 0000000..a2f6b68
--- /dev/null
+++ b/test/fuzz/corpus/pem/d89ae5c348e4931f4b5267efcdb798e42c248f5d-5
@@ -0,0 +1,2 @@
+-----BEGIN -----
+ï¿À: \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/d95f213104887b0fe67d3de603a2ec49fde13f31-2 b/test/fuzz/corpus/pem/d95f213104887b0fe67d3de603a2ec49fde13f31-2
new file mode 100644
index 0000000..252a76b
--- /dev/null
+++ b/test/fuzz/corpus/pem/d95f213104887b0fe67d3de603a2ec49fde13f31-2
@@ -0,0 +1,2 @@
+-----BEGIN -----
+:fooÎ \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/dfea2feae7c55cf54c241dafd5f951ee4de74164-5 b/test/fuzz/corpus/pem/dfea2feae7c55cf54c241dafd5f951ee4de74164-5
new file mode 100644
index 0000000..4dc660f
--- /dev/null
+++ b/test/fuzz/corpus/pem/dfea2feae7c55cf54c241dafd5f951ee4de74164-5
@@ -0,0 +1,2 @@
+-----BEGIN -----
+ï:Î \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/e00036d8be7b3b07286b8ce7624c0aeb4e1c1550-4 b/test/fuzz/corpus/pem/e00036d8be7b3b07286b8ce7624c0aeb4e1c1550-4
new file mode 100644
index 0000000..d6176e3
--- /dev/null
+++ b/test/fuzz/corpus/pem/e00036d8be7b3b07286b8ce7624c0aeb4e1c1550-4
@@ -0,0 +1,9 @@
+-BEGIN foo-----
+END -----
+:
+aaa: bbb
+fbb
+f
+
+YmFyYmF6
+-----END fo \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/e6ec65342f830e1780b16840d6a5767cf225553d-2 b/test/fuzz/corpus/pem/e6ec65342f830e1780b16840d6a5767cf225553d-2
new file mode 100644
index 0000000..5a10120
--- /dev/null
+++ b/test/fuzz/corpus/pem/e6ec65342f830e1780b16840d6a5767cf225553d-2
@@ -0,0 +1,3 @@
+-----BEGIN -----
+
+-----END \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/f0811f31ec1c422958fd861786088c9cf71d3fb5-1 b/test/fuzz/corpus/pem/f0811f31ec1c422958fd861786088c9cf71d3fb5-1
new file mode 100644
index 0000000..d096c53
--- /dev/null
+++ b/test/fuzz/corpus/pem/f0811f31ec1c422958fd861786088c9cf71d3fb5-1
@@ -0,0 +1,3 @@
+-----BEGIN foo-----
+foF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/f142d087e3fa84dd0140414b824f3a5398ef7e21-1 b/test/fuzz/corpus/pem/f142d087e3fa84dd0140414b824f3a5398ef7e21-1
new file mode 100644
index 0000000..6e18c3e
--- /dev/null
+++ b/test/fuzz/corpus/pem/f142d087e3fa84dd0140414b824f3a5398ef7e21-1
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+: b
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/f458c0ff1410a9e9b66df63fb530936d44e6c31a-1 b/test/fuzz/corpus/pem/f458c0ff1410a9e9b66df63fb530936d44e6c31a-1
new file mode 100644
index 0000000..f38d284
--- /dev/null
+++ b/test/fuzz/corpus/pem/f458c0ff1410a9e9b66df63fb530936d44e6c31a-1
@@ -0,0 +1,6 @@
+-----BEGIN foo-----
+aabbb
+foo:
+
+YmFyYmF6
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/fab11055c6a34ab2fd0257b4b073eb6a945be42c-3 b/test/fuzz/corpus/pem/fab11055c6a34ab2fd0257b4b073eb6a945be42c-3
new file mode 100644
index 0000000..646b6b4
--- /dev/null
+++ b/test/fuzz/corpus/pem/fab11055c6a34ab2fd0257b4b073eb6a945be42c-3
@@ -0,0 +1,10 @@
+-
+--BEGIN foo-----
+END -----
+:
+aaa: bbb
+fbb
+f
+
+mFyYmF6
+-----END foo \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/fac9f9666c93e22462b864764842be9eb9f17e45-1 b/test/fuzz/corpus/pem/fac9f9666c93e22462b864764842be9eb9f17e45-1
new file mode 100644
index 0000000..cca3eac
--- /dev/null
+++ b/test/fuzz/corpus/pem/fac9f9666c93e22462b864764842be9eb9f17e45-1
@@ -0,0 +1,4 @@
+-----BEGIN foo-----
+
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/fbdd93e11fdd678a319ec8069279d612d02f8df2-1 b/test/fuzz/corpus/pem/fbdd93e11fdd678a319ec8069279d612d02f8df2-1
new file mode 100644
index 0000000..ddcf8f4
--- /dev/null
+++ b/test/fuzz/corpus/pem/fbdd93e11fdd678a319ec8069279d612d02f8df2-1
@@ -0,0 +1,5 @@
+-----BEGIN foo-----
+a: b
+o:
+
+-----END foo----- \ No newline at end of file
diff --git a/test/fuzz/corpus/pem/ffc5d83a112d1776b9a006fcab571901cd61fba3-3 b/test/fuzz/corpus/pem/ffc5d83a112d1776b9a006fcab571901cd61fba3-3
new file mode 100644
index 0000000..f0ec4d0
--- /dev/null
+++ b/test/fuzz/corpus/pem/ffc5d83a112d1776b9a006fcab571901cd61fba3-3
Binary files differ
diff --git a/test/fuzz/corpus/rsa/0 b/test/fuzz/corpus/rsa/0
new file mode 100644
index 0000000..74e77eb
--- /dev/null
+++ b/test/fuzz/corpus/rsa/0
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfCoj9PKxSIpOB
+jVvP7B0l8Q6KXgwSxEBIobMl11nrH2Fv6ufZRWgma7E3rZcjRMygyfia6SB8KBjq
+OBMHnxX78tp5IDxbPWniA7GGTWZyBsXgfLFH7GVGBh8fiJJtfL4TP/xmMzY47rx8
+qvglkQDktdmSEmvfYmof5SIXD/CBI9YDxpXQB9EBcd16QnjwHUKHElOs4lZI9OeP
+8TSV8tWyskq1cO4LxPS8WZVTvbq0jp84OwQTpWtJqG/DUQ1QfMjfixt+uauCDA87
+iIwBC+rC7aCfaXHpqNayHzToUi2Jc34O6LMyfHgowEjQgnKehClY4Vuy0aJXQvKB
+mRDqyjO/AgMBAAECggEBAIHOAs3Gis8+WjRSjXVjh882DG1QsJwXZQYgPT+vpiAl
+YjKdNpOHRkbd9ARgXY5kEuccxDd7p7E6MM3XFpQf7M51ltpZfWboRgAIgD+WOiHw
+eSbdytr95C6tj11twTJBH+naGk1sTokxv7aaVdKfIjL49oeBexBFmVe4pW9gkmrE
+1z1y1a0RohqbZ0kprYPWjz5UhsNqbCzgkdDqS7IrcOwVg6zvKYFjHnqIHqaJXVif
+FgIfoNt7tz+12FTHI+6OkKoN3YCJueaxneBhITXm6RLOpQWa9qhdUPbkJ9vQNfph
+Qqke4faaxKY9UDma+GpEHR016AWufZp92pd9wQkDn0kCgYEA7w/ZizAkefHoZhZ8
+Isn/fYu4fdtUaVgrnGUVZobiGxWrHRU9ikbAwR7UwbgRSfppGiJdAMq1lyH2irmb
+4OHU64rjuYSlIqUWHLQHWmqUbLUvlDojH/vdmH/Zn0AbrLZaimC5UCjK3Eb7sAMq
+G0tGeDX2JraQvx7KrbC6peTaaaMCgYEA7tgZBiRCQJ7+mNu+gX9x6OXtjsDCh516
+vToRLkxWc7LAbC9LKsuEHl4e3vy1PY/nyuv12Ng2dBq4WDXozAmVgz0ok7rRlIFp
+w8Yj8o/9KuGZkD/7tw/pLsVc9Q3Wf0ACrnAAh7+3dAvn3yg+WHwXzqWIbrseDPt9
+ILCfUoNDpzUCgYAKFCX8y0PObFd67lm/cbq2xUw66iNN6ay1BEH5t5gSwkAbksis
+ar03pyAbJrJ75vXFZ0t6fBFZ1NG7GYYr3fmHEKz3JlN7+W/MN/7TXgjx6FWgLy9J
+6ul1w3YeU6qXBn0ctmU5ru6WiNuVmRyOWAcZjFTbXvkNRbQPzJKh6dsXdwKBgA1D
+FIihxMf/zBVCxl48bF/JPJqbm3GaTfFp4wBWHsrH1yVqrtrOeCSTh1VMZOfpMK60
+0W7b+pIR1cCYJbgGpDWoVLN3QSHk2bGUM/TJB/60jilTVC/DA2ikbtfwj8N7E2sK
+Lw1amN4ptxNOEcAqC8xepqe3XiDMahNBm2cigMQtAoGBAKwrXvss2BKz+/6poJQU
+A0c7jhMN8M9Y5S2Ockw07lrQeAgfu4q+/8ztm0NeHJbk01IJvJY5Nt7bSgwgNVlo
+j7vR2BMAc9U73Ju9aeTl/L6GqmZyA+Ojhl5gA5DPZYqNiqi93ydgRaI6n4+o3dI7
+5wnr40AmbuKCDvMOvN7nMybL
+-----END PRIVATE KEY-----
diff --git a/test/fuzz/corpus/rsa/1 b/test/fuzz/corpus/rsa/1
new file mode 100644
index 0000000..9afc9e1
--- /dev/null
+++ b/test/fuzz/corpus/rsa/1
@@ -0,0 +1,3 @@
+-----BEGIN RSA PRIVATE KEY-----
+A
+-----END RSA PRIVATE KEY-----
diff --git a/test/fuzz/corpus/signature/0 b/test/fuzz/corpus/signature/0
new file mode 100644
index 0000000..cdb2ba0
--- /dev/null
+++ b/test/fuzz/corpus/signature/0
@@ -0,0 +1 @@
+4695082086889915666293004
diff --git a/test/fuzz/corpus/signature/1 b/test/fuzz/corpus/signature/1
new file mode 100644
index 0000000..0f46f7f
--- /dev/null
+++ b/test/fuzz/corpus/signature/1
@@ -0,0 +1 @@
+564007835656437916
diff --git a/test/fuzz/corpus/signature/2 b/test/fuzz/corpus/signature/2
new file mode 100644
index 0000000..745f523
--- /dev/null
+++ b/test/fuzz/corpus/signature/2
@@ -0,0 +1 @@
+9735816121518442091
diff --git a/test/fuzz/dsse/fuzz_test.go b/test/fuzz/dsse/fuzz_test.go
new file mode 100644
index 0000000..9bc8117
--- /dev/null
+++ b/test/fuzz/dsse/fuzz_test.go
@@ -0,0 +1,81 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dsse
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+ "unicode/utf8"
+
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/sigstore/sigstore/pkg/signature"
+ ds "github.com/sigstore/sigstore/pkg/signature/dsse"
+)
+
+func FuzzDSSE(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data, payload string) {
+ if !utf8.Valid([]byte(data)) {
+ t.Skip("invalid utf8")
+ }
+ if !utf8.Valid([]byte(payload)) {
+ t.Skip("invalid utf8")
+ }
+ p, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Errorf("failed to generate key: %v", err)
+ }
+
+ sv, err := signature.LoadECDSASignerVerifier(p, crypto.SHA256)
+ if err != nil {
+ t.Errorf("failed to load signer verifier: %v", err)
+ }
+
+ wsv := ds.WrapSignerVerifier(sv, payload)
+
+ sig, err := wsv.SignMessage(strings.NewReader(data))
+ if err != nil {
+ t.Errorf("failed to sign message: %v", err)
+ }
+
+ if err := wsv.VerifySignature(bytes.NewReader(sig), nil); err != nil {
+ t.Errorf("failed to verify signature: %v", err)
+ }
+
+ env := dsse.Envelope{}
+ if err := json.Unmarshal(sig, &env); err != nil {
+ panic(err)
+ }
+ if env.PayloadType != payload {
+ t.Errorf("Expected payloadType %s, got %s", payload, env.PayloadType)
+ }
+
+ got, err := base64.StdEncoding.DecodeString(env.Payload)
+ if err != nil {
+ t.Errorf("failed to decode payload: %v", err)
+ }
+
+ if string(got) != data {
+ t.Errorf("Expected payload %s, got %s", data, env.Payload)
+ }
+ })
+}
diff --git a/test/fuzz/fuzz_password_test.go b/test/fuzz/fuzz_password_test.go
new file mode 100644
index 0000000..d08d8fb
--- /dev/null
+++ b/test/fuzz/fuzz_password_test.go
@@ -0,0 +1,44 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fuzz
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+)
+
+func FuzzGetPassword(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ original := cryptoutils.Read
+ cryptoutils.Read = func() func() ([]byte, error) {
+ return func() ([]byte, error) {
+ return data, nil
+ }
+ }
+ defer func() { cryptoutils.Read = original }()
+ p, err := cryptoutils.GetPasswordFromStdIn(true)
+ if err != nil {
+ t.Errorf("error in getting the password %v", err)
+ }
+ // the password we got back is not what was entered
+ if bytes.Compare(p, data) != 0 {
+ t.Errorf("password %v does not match %v", p, data)
+ }
+ t.Skip("invalid data")
+ })
+}
diff --git a/test/fuzz/go.mod b/test/fuzz/go.mod
new file mode 100644
index 0000000..66b7299
--- /dev/null
+++ b/test/fuzz/go.mod
@@ -0,0 +1,34 @@
+module github.com/sigstore/sigstore/test/fuzz
+
+go 1.20
+
+require (
+ github.com/AdaLogics/go-fuzz-headers v0.0.0-20211102141018-f7be0cbad29c
+ github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1
+ github.com/secure-systems-lab/go-securesystemslib v0.7.0
+ github.com/sigstore/sigstore v1.7.6
+)
+
+require (
+ github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+ github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-containerregistry v0.16.1 // indirect
+ github.com/kr/pretty v0.2.1 // indirect
+ github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/stephens2424/writerset v1.0.2 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/mod v0.10.0 // indirect
+ golang.org/x/net v0.18.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.9.1 // indirect
+ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+ google.golang.org/grpc v1.56.3 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/square/go-jose.v2 v2.6.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/test/fuzz/go.sum b/test/fuzz/go.sum
new file mode 100644
index 0000000..bb20ed5
--- /dev/null
+++ b/test/fuzz/go.sum
@@ -0,0 +1,91 @@
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20211102141018-f7be0cbad29c h1:9K6I0yCgGSneuHCoIlJl0O09UjqqWduCwd+ZL1nHFWc=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20211102141018-f7be0cbad29c/go.mod h1:WpB7kf89yJUETZxQnP1kgYPNwlT2jjdDYUCoxVggM3g=
+github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1 h1:YQOLTC8zvFaNSEuMexG0i7pY26bOksnQFsSJfGclo54=
+github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
+github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
+github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
+github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg=
+github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
+github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc=
+github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc=
+github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8=
+github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
+github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
+github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
+github.com/sigstore/sigstore v1.7.6 h1:zB0woXx+3Bp7dk7AjklHF1VhXBdCs84VXkZbp0IHLv8=
+github.com/sigstore/sigstore v1.7.6/go.mod h1:FJE+NpEZIs4QKqZl4B2RtaVLVDcDtocAwTiNlexeBkY=
+github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=
+github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
+github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
+github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
+golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
+gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/test/fuzz/pem/fuzzcert_test.go b/test/fuzz/pem/fuzzcert_test.go
new file mode 100644
index 0000000..b93c5a4
--- /dev/null
+++ b/test/fuzz/pem/fuzzcert_test.go
@@ -0,0 +1,85 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pem
+
+import (
+ "bytes"
+ "encoding/pem"
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "testing"
+)
+
+func FuzzLoadCertificates(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ b, _ := pem.Decode(data)
+ if b == nil {
+ t.Skip("invalid pem")
+ }
+
+ result, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(data))
+ if err != nil {
+ if result != nil {
+ t.Errorf("result %v should be nil when there is an error %v", result, err)
+ }
+ t.Skip("invalid pem")
+ }
+ for _, cert := range result {
+ if len(cert.Raw) == 0 {
+ t.Errorf("x509 cert raw is empty")
+ }
+ }
+ })
+}
+
+func FuzzUnmarshalCertificatesFromPEM(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ b, _ := pem.Decode(data)
+ if b == nil {
+ t.Skip("invalid pem")
+ }
+ result, err := cryptoutils.UnmarshalCertificatesFromPEM(data)
+ if err != nil {
+ if result != nil {
+ t.Errorf("result %v should be nil when there is an error %v", result, err)
+ }
+ t.Skip("invalid pem")
+ }
+ for _, cert := range result {
+ if len(cert.Raw) == 0 {
+ t.Errorf("x509 cert raw is empty")
+ }
+ }
+ })
+}
+
+func FuzzUnmarshalPEMToPublicKey(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ b, _ := pem.Decode(data)
+ if b == nil {
+ t.Skip("invalid pem")
+ }
+ result, err := cryptoutils.UnmarshalPEMToPublicKey(data)
+ if err != nil {
+ if result != nil {
+ t.Errorf("result %v should be nil when there is an error %v", result, err)
+ }
+ t.Skip("invalid pem")
+ }
+ if result == nil {
+ t.Errorf("result %v should not be nil", result)
+ }
+ })
+}
diff --git a/test/fuzz/signature/fuzz_signature_test.go b/test/fuzz/signature/fuzz_signature_test.go
new file mode 100644
index 0000000..c998d37
--- /dev/null
+++ b/test/fuzz/signature/fuzz_signature_test.go
@@ -0,0 +1,180 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "math/big"
+ "testing"
+
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+
+ fuzz "github.com/AdaLogics/go-fuzz-headers"
+ "github.com/sigstore/sigstore/pkg/signature"
+)
+
+func FuzzECDSASigner(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+
+ x := ecdsa.PrivateKey{}
+ z := new(big.Int)
+ z.SetBytes(data)
+ x.X = z
+ x.Y = z
+ x.D = z
+ x.Curve = elliptic.P384()
+
+ signer, err := signature.LoadECDSASignerVerifier(&x, crypto.SHA512)
+ if err != nil {
+ if signer != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", signer, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ sig, err := signer.SignMessage(bytes.NewReader(data))
+ if err != nil {
+ if sig != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", sig, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ if err = signer.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data)); err != nil {
+ t.Skip("not valid key")
+ }
+ })
+}
+func FuzzComputeDigest(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ hashFuncs := []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA512,
+ crypto.SHA384,
+ crypto.SHA224,
+ crypto.SHA1,
+ }
+ data, _, err := signature.ComputeDigestForSigning(bytes.NewReader(data), crypto.SHA512, hashFuncs)
+ if err != nil {
+ if data != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", data, err)
+ }
+ t.Skip("not valid key")
+ }
+ })
+}
+
+func FuzzComputeVerifying(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ hashFuncs := []crypto.Hash{
+ crypto.SHA256,
+ crypto.SHA512,
+ crypto.SHA384,
+ crypto.SHA224,
+ crypto.SHA1,
+ }
+ data, _, err := signature.ComputeDigestForVerifying(bytes.NewReader(data), crypto.SHA512, hashFuncs)
+ if err != nil {
+ if data != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", data, err)
+ }
+ t.Skip("not valid key")
+ }
+ })
+}
+
+func FuzzED25529SignerVerfier(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ x := ed25519.PrivateKey(data)
+
+ signer, err := signature.LoadED25519SignerVerifier(x)
+ if err != nil {
+ if signer != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", signer, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ sig, err := signer.SignMessage(bytes.NewReader(data))
+ if err != nil {
+ if sig != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", sig, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ signer.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+ })
+}
+
+func FuzzRSAPKCS1v15SignerVerfier(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ f := fuzz.NewConsumer(data)
+ x := rsa.PrivateKey{}
+ f.GenerateStruct(&x)
+
+ signer, err := signature.LoadRSAPKCS1v15Signer(&x, crypto.SHA512)
+ if err != nil {
+ if signer != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", signer, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ sig, err := signer.SignMessage(bytes.NewReader(data))
+ if err != nil {
+ if sig != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", sig, err)
+ }
+ t.Skip("not valid key")
+ }
+ if _, err := signer.Sign(bytes.NewReader(data), data, nil); err != nil {
+ t.Skip("not valid key")
+ }
+ })
+}
+
+func FuzzRSAPSSSignerVerfier(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey(data, cryptoutils.SkipPassword)
+ if err != nil {
+ t.Skip()
+ }
+ signer, err := signature.LoadRSAPSSSignerVerifier(privateKey.(*rsa.PrivateKey), crypto.SHA512, nil)
+ if err != nil {
+ if signer != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", signer, err)
+ }
+ t.Skip("not valid key")
+ }
+
+ sig, err := signer.SignMessage(bytes.NewReader(data))
+ if err != nil {
+ if sig != nil {
+ t.Errorf("key %v is not nil when there is an error %v ", sig, err)
+ }
+ t.Skip("not valid key")
+ }
+ if _, err := signer.Sign(bytes.NewReader(data), data, nil); err != nil {
+ t.Skip("not valid key")
+ }
+ })
+}
diff --git a/test/fuzz/tools.go b/test/fuzz/tools.go
new file mode 100644
index 0000000..37215cf
--- /dev/null
+++ b/test/fuzz/tools.go
@@ -0,0 +1,25 @@
+// +build tools
+
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// This package imports things required by build scripts, to force `go mod` to see them as dependencies
+package fuzz
+
+import (
+ _ "github.com/AdaLogics/go-fuzz-headers"
+ _ "github.com/dvyukov/go-fuzz/go-fuzz"
+ _ "github.com/dvyukov/go-fuzz/go-fuzz-build"
+)