diff --git a/.travis.yml b/.travis.yml index aa593abe..fcf73d2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ env: before_script: - make bootstrap script: - - make + - make travis - make artifacts after_success: - bash <(curl -s https://codecov.io/bash) -t "$CODECOV_TOKEN" || echo "Codecov did diff --git a/Makefile b/Makefile index 9fb552d1..c9b453e1 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ AWSKMS_BINNAME?=step-awskms-init AWSKMS_PKG?=github.com/smallstep/certificates/cmd/step-awskms-init YUBIKEY_BINNAME?=step-yubikey-init YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init +PKCS11_BINNAME?=step-pkcs11-init +PKCS11_PKG?=github.com/smallstep/certificates/cmd/step-pkcs11-init # Set V to 1 for verbose output from the Makefile Q=$(if $V,,@) @@ -16,7 +18,9 @@ OUTPUT_ROOT=output/ all: lint test build -.PHONY: all +travis: lintcgo testcgo build + +.PHONY: all travis ######################################### # Bootstrapping @@ -76,7 +80,7 @@ GOFLAGS := CGO_ENABLED=0 download: $Q go mod download -build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) +build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) $(PREFIX)bin/$(PKCS11_BINNAME) @echo "Build Complete!" $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) @@ -95,6 +99,10 @@ $(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_PKG) +$(PREFIX)bin/$(PKCS11_BINNAME): download $(call rwildcard,*.go) + $Q mkdir -p $(@D) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(PKCS11_BINNAME) $(LDFLAGS) $(PKCS11_PKG) + # Target to force a build of step-ca without running tests simple: build @@ -115,7 +123,10 @@ generate: test: $Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./... -.PHONY: test +testcgo: + $Q go test -short -coverprofile=coverage.out ./... + +.PHONY: test testcgo integrate: integration @@ -132,9 +143,12 @@ fmt: $Q gofmt -l -w $(SRC) lint: + $Q $(GOFLAGS) LOG_LEVEL=error golangci-lint run --timeout=30m + +lintcgo: $Q LOG_LEVEL=error golangci-lint run --timeout=30m -.PHONY: lint fmt +.PHONY: fmt lint lintcgo ######################################### # Install @@ -171,6 +185,9 @@ endif ifneq ($(YUBIKEY_BINNAME),"") $Q rm -f bin/$(YUBIKEY_BINNAME) endif +ifneq ($(PKCS11_BINNAME),"") + $Q rm -f bin/$(PKCS11_BINNAME) +endif .PHONY: clean diff --git a/authority/authority.go b/authority/authority.go index e53144fe..37fd0429 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -387,3 +387,10 @@ func (a *Authority) Shutdown() error { } return a.db.Shutdown() } + +// CloseForReload closes internal services, to allow a safe reload. +func (a *Authority) CloseForReload() { + if err := a.keyManager.Close(); err != nil { + log.Printf("error closing the key manager: %v", err) + } +} diff --git a/authority/authority_test.go b/authority/authority_test.go index 8b003572..e6625d6a 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -306,3 +306,17 @@ func TestNewEmbedded_GetTLSCertificate(t *testing.T) { assert.True(t, cert.Leaf.IPAddresses[0].Equal(net.ParseIP("127.0.0.1"))) assert.True(t, cert.Leaf.IPAddresses[1].Equal(net.ParseIP("::1"))) } + +func TestAuthority_CloseForReload(t *testing.T) { + tests := []struct { + name string + auth *Authority + }{ + {"ok", testAuthority(t)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.auth.CloseForReload() + }) + } +} diff --git a/ca/ca.go b/ca/ca.go index edd8bd38..a061fa02 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -250,9 +250,11 @@ func (ca *CA) Reload() error { } // 1. Stop previous renewer - // 2. Replace ca properties + // 2. Safely shutdown any internal resources (e.g. key manager) + // 3. Replace ca properties // Do not replace ca.srv ca.renewer.Stop() + ca.auth.CloseForReload() ca.auth = newCA.auth ca.config = newCA.config ca.opts = newCA.opts diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 096935af..dad9cdbe 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -31,6 +31,7 @@ import ( _ "github.com/smallstep/certificates/kms/sshagentkms" // Experimental kms interfaces. + _ "github.com/smallstep/certificates/kms/pkcs11" _ "github.com/smallstep/certificates/kms/yubikey" // Enabled cas interfaces. diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go index d23a31e9..69573c5d 100644 --- a/cmd/step-cloudkms-init/main.go +++ b/cmd/step-cloudkms-init/main.go @@ -234,7 +234,7 @@ func createSSH(c *cloudkms.CloudKMS, project, location, keyRing string, protecti resp, err = c.CreateKey(&apiv1.CreateKeyRequest{ Name: parent + "/ssh-host-key", SignatureAlgorithm: apiv1.ECDSAWithSHA256, - ProtectionLevel: apiv1.Software, + ProtectionLevel: protectionLevel, }) if err != nil { return err diff --git a/cmd/step-pkcs11-init/main.go b/cmd/step-pkcs11-init/main.go new file mode 100644 index 00000000..0dd431ad --- /dev/null +++ b/cmd/step-pkcs11-init/main.go @@ -0,0 +1,455 @@ +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "runtime" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/uri" + "go.step.sm/cli-utils/fileutil" + "go.step.sm/cli-utils/ui" + "go.step.sm/crypto/pemutil" + + // Enable pkcs11. + _ "github.com/smallstep/certificates/kms/pkcs11" +) + +// Config is a mapping of the cli flags. +type Config struct { + KMS string + RootOnly bool + RootObject string + RootKeyObject string + CrtObject string + CrtKeyObject string + SSHHostKeyObject string + SSHUserKeyObject string + RootFile string + KeyFile string + Pin string + NoCerts bool + EnableSSH bool + Force bool +} + +// Validate checks the flags in the config. +func (c *Config) Validate() error { + switch { + case c.KMS == "": + return errors.New("flag `--kms` is required") + case c.RootFile != "" && c.KeyFile == "": + return errors.New("flag `--root` requires flag `--key`") + case c.KeyFile != "" && c.RootFile == "": + return errors.New("flag `--key` requires flag `--root`") + case c.RootOnly && c.RootFile != "": + return errors.New("flag `--root-only` is incompatible with flag `--root`") + case c.RootFile == "" && c.RootObject == "": + return errors.New("one of flag `--root` or `--root-cert` is required") + case c.RootFile == "" && c.RootKeyObject == "": + return errors.New("one of flag `--root` or `--root-key` is required") + default: + if c.RootFile != "" { + c.RootObject = "" + c.RootKeyObject = "" + } + if c.RootOnly { + c.CrtObject = "" + c.CrtKeyObject = "" + } + if !c.EnableSSH { + c.SSHHostKeyObject = "" + c.SSHUserKeyObject = "" + } + return nil + } +} + +func main() { + var kmsuri string + switch runtime.GOOS { + case "darwin": + kmsuri = "pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM" + case "linux": + kmsuri = "pkcs11:module-path=/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so;token=YubiHSM" + case "windows": + if home, err := os.UserHomeDir(); err == nil { + kmsuri = "pkcs11:module-path=" + home + "\\yubihsm2-sdk\\bin\\yubihsm_pkcs11.dll" + ";token=YubiHSM" + } + } + + var c Config + flag.StringVar(&c.KMS, "kms", kmsuri, "PKCS #11 URI with the module-path and token to connect to the module.") + flag.StringVar(&c.RootObject, "root-cert", "pkcs11:id=7330;object=root-cert", "PKCS #11 URI with object id and label to store the root certificate.") + flag.StringVar(&c.RootKeyObject, "root-key", "pkcs11:id=7330;object=root-key", "PKCS #11 URI with object id and label to store the root key.") + flag.StringVar(&c.CrtObject, "crt-cert", "pkcs11:id=7331;object=intermediate-cert", "PKCS #11 URI with object id and label to store the intermediate certificate.") + flag.StringVar(&c.CrtKeyObject, "crt-key", "pkcs11:id=7331;object=intermediate-key", "PKCS #11 URI with object id and label to store the intermediate certificate.") + flag.StringVar(&c.SSHHostKeyObject, "ssh-host-key", "pkcs11:id=7332;object=ssh-host-key", "PKCS #11 URI with object id and label to store the key used to sign SSH host certificates.") + flag.StringVar(&c.SSHUserKeyObject, "ssh-user-key", "pkcs11:id=7333;object=ssh-user-key", "PKCS #11 URI with object id and label to store the key used to sign SSH user certificates.") + flag.BoolVar(&c.RootOnly, "root-only", false, "Store only only the root certificate and sign and intermediate.") + flag.StringVar(&c.RootFile, "root", "", "Path to the root certificate to use.") + flag.StringVar(&c.KeyFile, "key", "", "Path to the root key to use.") + flag.BoolVar(&c.EnableSSH, "ssh", false, "Enable the creation of ssh keys.") + flag.BoolVar(&c.NoCerts, "no-certs", false, "Do not store certificates in the module.") + flag.BoolVar(&c.Force, "force", false, "Force the delete of previous keys.") + flag.Usage = usage + flag.Parse() + + if err := c.Validate(); err != nil { + fatal(err) + } + + u, err := uri.ParseWithScheme("pkcs11", c.KMS) + if err != nil { + fatal(err) + } + + if u.Pin() == "" { + pin, err := ui.PromptPassword("What is the PKCS#11 PIN?") + if err != nil { + fatal(err) + } + c.Pin = string(pin) + } + + k, err := kms.New(context.Background(), apiv1.Options{ + Type: string(apiv1.PKCS11), + URI: c.KMS, + Pin: c.Pin, + }) + if err != nil { + fatal(err) + } + + defer func() { + _ = k.Close() + }() + + // Check if the slots are empty, fail if they are not + certUris := []string{ + c.RootObject, c.CrtObject, + } + keyUris := []string{ + c.RootKeyObject, c.CrtKeyObject, + c.SSHHostKeyObject, c.SSHUserKeyObject, + } + if !c.Force { + for _, u := range certUris { + if u != "" && !c.NoCerts { + checkObject(k, u) + checkCertificate(k, u) + } + } + for _, u := range keyUris { + if u != "" { + checkObject(k, u) + } + } + } else { + deleter, ok := k.(interface { + DeleteKey(uri string) error + DeleteCertificate(uri string) error + }) + if ok { + for _, u := range certUris { + if u != "" && !c.NoCerts { + // Some HSMs like Nitrokey will overwrite the key with the + // certificate label. + if err := deleter.DeleteKey(u); err != nil { + fatalClose(err, k) + } + if err := deleter.DeleteCertificate(u); err != nil { + fatalClose(err, k) + } + } + } + for _, u := range keyUris { + if u != "" { + if err := deleter.DeleteKey(u); err != nil { + fatalClose(err, k) + } + } + } + } + } + + if err := createPKI(k, c); err != nil { + fatalClose(err, k) + } +} + +func fatal(err error) { + if os.Getenv("STEPDEBUG") == "1" { + fmt.Fprintf(os.Stderr, "%+v\n", err) + } else { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) +} + +func fatalClose(err error, k kms.KeyManager) { + _ = k.Close() + fatal(err) +} + +func usage() { + fmt.Fprintln(os.Stderr, "Usage: step-pkcs11-init") + fmt.Fprintln(os.Stderr, ` +The step-pkcs11-init command initializes a public key infrastructure (PKI) +to be used by step-ca. + +This tool is experimental and in the future it will be integrated in step cli. + +OPTIONS`) + fmt.Fprintln(os.Stderr) + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, ` +COPYRIGHT + + (c) 2018-2021 Smallstep Labs, Inc.`) + os.Exit(1) +} + +func checkCertificate(k kms.KeyManager, rawuri string) { + if cm, ok := k.(kms.CertificateManager); ok { + if _, err := cm.LoadCertificate(&apiv1.LoadCertificateRequest{ + Name: rawuri, + }); err == nil { + fmt.Fprintf(os.Stderr, "⚠️ Your PKCS #11 module already has a certificate on %s.\n", rawuri) + fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.") + _ = k.Close() + os.Exit(1) + } + } +} + +func checkObject(k kms.KeyManager, rawuri string) { + if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: rawuri, + }); err == nil { + fmt.Fprintf(os.Stderr, "⚠️ Your PKCS #11 module already has a key on %s.\n", rawuri) + fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.") + _ = k.Close() + os.Exit(1) + } +} + +func createPKI(k kms.KeyManager, c Config) error { + var err error + ui.Println("Creating PKI ...") + now := time.Now() + + // Root Certificate + var signer crypto.Signer + var root *x509.Certificate + if c.RootFile != "" && c.KeyFile != "" { + root, err = pemutil.ReadCertificate(c.RootFile) + if err != nil { + return err + } + + key, err := pemutil.Read(c.KeyFile) + if err != nil { + return err + } + + var ok bool + if signer, ok = key.(crypto.Signer); !ok { + return errors.Errorf("key type '%T' does not implement a signer", key) + } + } else { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.RootKeyObject, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + signer, err = k.CreateSigner(&resp.CreateSignerRequest) + if err != nil { + return err + } + + template := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 1, + MaxPathLenZero: false, + Issuer: pkix.Name{CommonName: "PKCS #11 Smallstep Root"}, + Subject: pkix.Name{CommonName: "PKCS #11 Smallstep Root"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + AuthorityKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, template, template, resp.PublicKey, signer) + if err != nil { + return err + } + + root, err = x509.ParseCertificate(b) + if err != nil { + return errors.Wrap(err, "error parsing root certificate") + } + + if cm, ok := k.(kms.CertificateManager); ok && !c.NoCerts { + if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.RootObject, + Certificate: root, + }); err != nil { + return err + } + } + + if err = fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + ui.PrintSelected("Root Key", resp.Name) + ui.PrintSelected("Root Certificate", "root_ca.crt") + } + + // Intermediate Certificate + var keyName string + var publicKey crypto.PublicKey + if c.RootOnly { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return errors.Wrap(err, "error creating intermediate key") + } + + pass, err := ui.PromptPasswordGenerate("What do you want your password to be? [leave empty and we'll generate one]", + ui.WithRichPrompt()) + if err != nil { + return err + } + + _, err = pemutil.Serialize(priv, pemutil.WithPassword(pass), pemutil.ToFile("intermediate_ca_key", 0600)) + if err != nil { + return err + } + + publicKey = priv.Public() + } else { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.CrtKeyObject, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + publicKey = resp.PublicKey + keyName = resp.Name + } + + template := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 0, + MaxPathLenZero: true, + Issuer: root.Subject, + Subject: pkix.Name{CommonName: "YubiKey Smallstep Intermediate"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(publicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, signer) + if err != nil { + return err + } + + intermediate, err := x509.ParseCertificate(b) + if err != nil { + return errors.Wrap(err, "error parsing intermediate certificate") + } + + if cm, ok := k.(kms.CertificateManager); ok && !c.NoCerts { + if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.CrtObject, + Certificate: intermediate, + }); err != nil { + return err + } + } + + if err = fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + if c.RootOnly { + ui.PrintSelected("Intermediate Key", "intermediate_ca_key") + } else { + ui.PrintSelected("Intermediate Key", keyName) + } + + ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt") + + if c.SSHHostKeyObject != "" { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.SSHHostKeyObject, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + ui.PrintSelected("SSH Host Key", resp.Name) + } + + if c.SSHUserKeyObject != "" { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.SSHUserKeyObject, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + ui.PrintSelected("SSH User Key", resp.Name) + } + + return nil +} + +func mustSerialNumber() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + panic(err) + } + return sn +} + +func mustSubjectKeyID(key crypto.PublicKey) []byte { + b, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + panic(err) + } + hash := sha1.Sum(b) + return hash[:] +} diff --git a/distribution.md b/distribution.md index eb2ff3d2..5e3e4727 100644 --- a/distribution.md +++ b/distribution.md @@ -16,42 +16,20 @@ e.g. `v1.0.2` `-rc*` suffix. e.g. `v1.0.2-rc` or `v1.0.2-rc.4` --- -1. **Release `cli` first** - If you plan to release [`cli`](https://github.com/smallstep/cli) as part of - this release, `cli` must be released first. The `certificates` docker container - depends on the `cli` container. Make certain to wait until the `cli` travis - build has completed. +1. **Tag it!** -2. **Update the version of step/cli** + 1. Find the most recent tag. -
- $ go get -u github.com/smallstep/cli
-
-
-3. **Commit all changes.**
-
- Make sure that the local checkout is up to date with the remote origin and
- that all local changes have been pushed.
-
-
- $ git pull --rebase origin master
- $ git push
-
-
-4. **Tag it!**
-
- 1. **Find the most recent tag.**
-
-
- $ git fetch --tags
- $ git tag
-
+ ```
+ $> git fetch --tags
+ $> git tag
+ ```
The new tag needs to be the logical successor of the most recent existing tag.
See [versioning](#versioning) section for more information on version numbers.
- 2. **Select the type and value of the next tag.**
+ 2. Select the type and value of the next tag.
Is the new release a *release candidate* or a *standard release*?
@@ -62,7 +40,7 @@ e.g. `v1.0.2`
is a release candidate, say `v1.0.2-rc.3`, then the version of the next
release candidate should be `v1.0.2-rc.4`.
- 2. **Standard Release**
+ 2. Standard Release
If the most recent tag is a standard release, say `v1.0.2`, then the version
of the next standard release should be `v1.0.3`. If the most recent tag
@@ -70,27 +48,27 @@ e.g. `v1.0.2`
standard release should be `v1.0.3`.
- 3. **Create a local tag.**
+ 3. Create a local tag.
-
+ ```
# standard release
- $ git tag v1.0.3
+ $> git tag v1.0.3
...or
# release candidate
- $ git tag v1.0.3-rc.1
-
+ $> git tag v1.0.3-rc.1
+ ```
- 4. **Push the new tag to the remote origin.**
+ 4. Push the new tag to the remote origin.
-
+ ```
# standard release
- $ git push origin tag v1.0.3
+ $> git push origin tag v1.0.3
...or
# release candidate
- $ git push origin tag v1.0.3-rc.1
-
+ $> git push origin tag v1.0.3-rc.1
+ ```
-5. **Check the build status at**
+2. **Check the build status at**
[Travis-CI](https://travis-ci.com/smallstep/certificates/builds/).
Travis will begin by verifying that there are no compilation or linting errors
@@ -105,7 +83,7 @@ e.g. `v1.0.2`
* **step-certificates_1.0.3_darwin_amd64.tar.gz**: tarball containing a statically compiled darwin binary.
* **step-certificates.tar.gz**: tarball containing a git archive of the full repo.
-6. **Update the AUR Arch Linux package**
+3. **Update the AUR Arch Linux package**
> **NOTE**: if you plan to release `cli` next then you can skip this step.
@@ -119,7 +97,7 @@ e.g. `v1.0.2`
$ ./update --ca v1.0.3
-7. **Update the Helm packages**
+4. **Update the Helm packages**
> **NOTE**: This is an optional step, only necessary if we want to release a
> new helm package.
diff --git a/go.mod b/go.mod
index c81bf9dc..8ef1b088 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.14
require (
cloud.google.com/go v0.70.0
github.com/Masterminds/sprig/v3 v3.1.0
+ github.com/ThalesIgnite/crypto11 v1.2.4
github.com/aws/aws-sdk-go v1.30.29
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-piv/piv-go v1.7.0
@@ -18,12 +19,12 @@ require (
github.com/rs/xid v1.2.1
github.com/sirupsen/logrus v1.4.2
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
- github.com/smallstep/nosql v0.3.0
+ github.com/smallstep/nosql v0.3.6
github.com/urfave/cli v1.22.4
go.step.sm/cli-utils v0.1.0
go.step.sm/crypto v0.7.3
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
- golang.org/x/net v0.0.0-20201021035429-f5854403a974
+ golang.org/x/net v0.0.0-20210119194325-5f4716e94777
google.golang.org/api v0.33.0
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154
google.golang.org/grpc v1.32.0
diff --git a/go.sum b/go.sum
index 25fef498..2d212432 100644
--- a/go.sum
+++ b/go.sum
@@ -34,13 +34,15 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
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/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4=
-github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
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/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
+github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
+github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
@@ -49,6 +51,8 @@ github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TN
github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8=
+github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0=
github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
@@ -75,14 +79,17 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
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/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6Sts=
-github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
-github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200413122845-09dd2e1a4195 h1:n8KbImHW5qZCXv1y3tHjz5yz418/eTxeRJZ2ZuDm1ZU=
-github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200413122845-09dd2e1a4195/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM=
-github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3 h1:MQLRM35Pp0yAyBYksjbj1nZI/w6eyRY/mWoM1sFf4kU=
-github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
+github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
+github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658 h1:/WBjuutuivOA02gpDtrvrWKw01ugkyt3QnimB7enbtI=
+github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658/go.mod h1:2uGEvGm+JSDLd5UAaKIFSbXDcYyeH0fWJP4N2HMMYMI=
+github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc=
+github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -98,8 +105,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U=
github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
-github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
-github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@@ -139,6 +144,8 @@ github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
+github.com/golang/snappy v0.0.2/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 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -197,6 +204,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/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=
@@ -216,18 +225,17 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/micromdm/scep v1.0.0 h1:ai//kcZnxZPq1YE/MatiE2bIRD94KOAwZRpN1fhVQXY=
github.com/micromdm/scep v1.0.0/go.mod h1:CID2SixSr5FvoauZdAFUSpQkn5MAuSy9oyURMGOJbag=
+github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ=
+github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
-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/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU=
github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
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=
@@ -247,12 +255,11 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk=
github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
-github.com/smallstep/nosql v0.3.0 h1:V1X5vfDsDt89499h3jZFUlR4VnnsYYs5tXaQZ0w8z5U=
-github.com/smallstep/nosql v0.3.0/go.mod h1:QG7gNOpidifn99MjZaiNbm7HPesIyBd97F/OfacNz8Q=
+github.com/smallstep/nosql v0.3.6 h1:cq6a3NwjFJxkVlWU1T4qGskcfEXr0fO1WqQrraDO1Po=
+github.com/smallstep/nosql v0.3.6/go.mod h1:h1zC/Z54uNHc8euquLED4qJNCrMHd3nytA141ZZh4qQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -267,11 +274,13 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
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/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 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
+github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -280,20 +289,17 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
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=
-go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
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 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
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 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.step.sm/cli-utils v0.1.0 h1:uuQ73MuAh5P5Eg+3zfqwrtlTLx5DWSfGqGCrSSjYqdk=
go.step.sm/cli-utils v0.1.0/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y=
-go.step.sm/crypto v0.6.1 h1:nJoRFGrGNf/mKVVMdWnfLbBfIFt/z4NdJlSL5nipQMQ=
go.step.sm/crypto v0.6.1/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
go.step.sm/crypto v0.7.3 h1:uWkT0vsaZVixgn5x6Ojqittry9PiyVn2ihEYG/qOxV8=
go.step.sm/crypto v0.7.3/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
@@ -302,10 +308,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/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 h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -366,8 +370,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
@@ -416,8 +420,11 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
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-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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=
diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go
index b7845109..705f3633 100644
--- a/kms/apiv1/options.go
+++ b/kms/apiv1/options.go
@@ -26,16 +26,29 @@ type CertificateManager interface {
// ErrNotImplemented is the type of error returned if an operation is not
// implemented.
type ErrNotImplemented struct {
- msg string
+ Message string
}
func (e ErrNotImplemented) Error() string {
- if e.msg != "" {
- return e.msg
+ if e.Message != "" {
+ return e.Message
}
return "not implemented"
}
+// ErrAlreadyExists is the type of error returned if a key already exists. This
+// is currently only implmented on pkcs11.
+type ErrAlreadyExists struct {
+ Message string
+}
+
+func (e ErrAlreadyExists) Error() string {
+ if e.Message != "" {
+ return e.Message
+ }
+ return "key already exists"
+}
+
// Type represents the KMS type used.
type Type string
@@ -64,10 +77,15 @@ type Options struct {
// Path to the credentials file used in CloudKMS and AmazonKMS.
CredentialsFile string `json:"credentialsFile"`
- // Path to the module used with PKCS11 KMS.
- Module string `json:"module"`
+ // URI is based on the PKCS #11 URI Scheme defined in
+ // https://tools.ietf.org/html/rfc7512 and represents the configuration used
+ // to connect to the KMS.
+ //
+ // Used by: pkcs11
+ URI string `json:"uri"`
- // Pin used to access the PKCS11 module.
+ // Pin used to access the PKCS11 module. It can be defined in the URI using
+ // the pin-value or pin-source properties.
Pin string `json:"pin"`
// ManagementKey used in YubiKeys. Default management key is the hexadecimal
@@ -93,10 +111,9 @@ func (o *Options) Validate() error {
}
switch Type(strings.ToLower(o.Type)) {
- case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS, SSHAgentKMS:
- case YubiKey:
- case PKCS11:
- return ErrNotImplemented{"support for PKCS11 is not yet implemented"}
+ case DefaultKMS, SoftKMS: // Go crypto based kms.
+ case CloudKMS, AmazonKMS, SSHAgentKMS: // Cloud based kms.
+ case YubiKey, PKCS11: // Hardware based kms.
default:
return errors.Errorf("unsupported kms type %s", o.Type)
}
diff --git a/kms/apiv1/options_test.go b/kms/apiv1/options_test.go
index 150dd17b..5405954f 100644
--- a/kms/apiv1/options_test.go
+++ b/kms/apiv1/options_test.go
@@ -15,7 +15,7 @@ func TestOptions_Validate(t *testing.T) {
{"cloudkms", &Options{Type: "cloudkms"}, false},
{"awskms", &Options{Type: "awskms"}, false},
{"sshagentkms", &Options{Type: "sshagentkms"}, false},
- {"pkcs11", &Options{Type: "pkcs11"}, true},
+ {"pkcs11", &Options{Type: "pkcs11"}, false},
{"unsupported", &Options{Type: "unsupported"}, true},
}
for _, tt := range tests {
@@ -42,7 +42,7 @@ func TestErrNotImplemented_Error(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := ErrNotImplemented{
- msg: tt.fields.msg,
+ Message: tt.fields.msg,
}
if got := e.Error(); got != tt.want {
t.Errorf("ErrNotImplemented.Error() = %v, want %v", got, tt.want)
@@ -50,3 +50,27 @@ func TestErrNotImplemented_Error(t *testing.T) {
})
}
}
+
+func TestErrAlreadyExists_Error(t *testing.T) {
+ type fields struct {
+ msg string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {"default", fields{}, "key already exists"},
+ {"custom", fields{"custom message: key already exists"}, "custom message: key already exists"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := ErrAlreadyExists{
+ Message: tt.fields.msg,
+ }
+ if got := e.Error(); got != tt.want {
+ t.Errorf("ErrAlreadyExists.Error() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go
index bbee4cfc..e58c4546 100644
--- a/kms/apiv1/requests.go
+++ b/kms/apiv1/requests.go
@@ -98,9 +98,16 @@ type GetPublicKeyRequest struct {
// CreateKeyRequest is the parameter used in the kms.CreateKey method.
type CreateKeyRequest struct {
- Name string
+ // Name represents the key name or label used to identify a key.
+ //
+ // Used by: awskms, cloudkms, pkcs11, yubikey.
+ Name string
+
+ // SignatureAlgorithm represents the type of key to create.
SignatureAlgorithm SignatureAlgorithm
- Bits int
+
+ // Bits is the number of bits on RSA keys.
+ Bits int
// ProtectionLevel specifies how cryptographic operations are performed.
// Used by: cloudkms
diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go
index 5e88eb80..da392989 100644
--- a/kms/awskms/awskms.go
+++ b/kms/awskms/awskms.go
@@ -17,6 +17,9 @@ import (
"go.step.sm/crypto/pemutil"
)
+// Scheme is the scheme used in uris.
+const Scheme = "awskms"
+
// KMS implements a KMS using AWS Key Management Service.
type KMS struct {
session *session.Session
@@ -69,7 +72,24 @@ var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{
// AWS sessions can also be configured with environment variables, see docs at
// https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for all the options.
func New(ctx context.Context, opts apiv1.Options) (*KMS, error) {
- o := session.Options{}
+ var o session.Options
+
+ if opts.URI != "" {
+ u, err := uri.ParseWithScheme(Scheme, opts.URI)
+ if err != nil {
+ return nil, err
+ }
+ o.Profile = u.Get("profile")
+ if v := u.Get("region"); v != "" {
+ o.Config.Region = new(string)
+ *o.Config.Region = v
+ }
+ if f := u.Get("credentials-file"); f != "" {
+ o.SharedConfigFiles = []string{opts.CredentialsFile}
+ }
+ }
+
+ // Deprecated way to set configuration parameters.
if opts.Region != "" {
o.Config.Region = &opts.Region
}
diff --git a/kms/awskms/awskms_test.go b/kms/awskms/awskms_test.go
index c86645e2..3c99fc4c 100644
--- a/kms/awskms/awskms_test.go
+++ b/kms/awskms/awskms_test.go
@@ -60,7 +60,13 @@ func TestNew(t *testing.T) {
Profile: "smallstep",
CredentialsFile: "~/aws/credentials",
}}, expected, false},
+ {"ok with uri", args{ctx, apiv1.Options{
+ URI: "awskms:region=us-east-1;profile=smallstep;credentials-file=/var/run/aws/credentials",
+ }}, expected, false},
{"fail", args{ctx, apiv1.Options{}}, nil, true},
+ {"fail uri", args{ctx, apiv1.Options{
+ URI: "pkcs11:region=us-east-1;profile=smallstep;credentials-file=/var/run/aws/credentials",
+ }}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go
index 547bfc62..cc533702 100644
--- a/kms/cloudkms/cloudkms.go
+++ b/kms/cloudkms/cloudkms.go
@@ -14,11 +14,15 @@ import (
gax "github.com/googleapis/gax-go/v2"
"github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
+ "github.com/smallstep/certificates/kms/uri"
"go.step.sm/crypto/pemutil"
"google.golang.org/api/option"
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
)
+// Scheme is the scheme used in uris.
+const Scheme = "cloudkms"
+
const pendingGenerationRetries = 10
// protectionLevelMapping maps step protection levels with cloud kms ones.
@@ -71,6 +75,10 @@ type KeyManagementClient interface {
CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error)
}
+var newKeyManagementClient = func(ctx context.Context, opts ...option.ClientOption) (KeyManagementClient, error) {
+ return cloudkms.NewKeyManagementClient(ctx, opts...)
+}
+
// CloudKMS implements a KMS using Google's Cloud apiv1.
type CloudKMS struct {
client KeyManagementClient
@@ -79,11 +87,23 @@ type CloudKMS struct {
// New creates a new CloudKMS configured with a new client.
func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) {
var cloudOpts []option.ClientOption
+
+ if opts.URI != "" {
+ u, err := uri.ParseWithScheme(Scheme, opts.URI)
+ if err != nil {
+ return nil, err
+ }
+ if f := u.Get("credentials-file"); f != "" {
+ cloudOpts = append(cloudOpts, option.WithCredentialsFile(f))
+ }
+ }
+
+ // Deprecated way to set configuration parameters.
if opts.CredentialsFile != "" {
cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile))
}
- client, err := cloudkms.NewKeyManagementClient(ctx, cloudOpts...)
+ client, err := newKeyManagementClient(ctx, cloudOpts...)
if err != nil {
return nil, err
}
diff --git a/kms/cloudkms/cloudkms_test.go b/kms/cloudkms/cloudkms_test.go
index 1038432a..e04e0198 100644
--- a/kms/cloudkms/cloudkms_test.go
+++ b/kms/cloudkms/cloudkms_test.go
@@ -5,13 +5,13 @@ import (
"crypto"
"fmt"
"io/ioutil"
- "os"
"reflect"
"testing"
gax "github.com/googleapis/gax-go/v2"
"github.com/smallstep/certificates/kms/apiv1"
"go.step.sm/crypto/pemutil"
+ "google.golang.org/api/option"
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -50,26 +50,63 @@ func TestParent(t *testing.T) {
}
func TestNew(t *testing.T) {
+ tmp := newKeyManagementClient
+ t.Cleanup(func() {
+ newKeyManagementClient = tmp
+ })
+ newKeyManagementClient = func(ctx context.Context, opts ...option.ClientOption) (KeyManagementClient, error) {
+ if len(opts) > 0 {
+ return nil, fmt.Errorf("test error")
+ }
+ return &MockClient{}, nil
+ }
+
type args struct {
ctx context.Context
opts apiv1.Options
}
tests := []struct {
- name string
- skipOnCI bool
- args args
- want *CloudKMS
- wantErr bool
+ name string
+ args args
+ want *CloudKMS
+ wantErr bool
}{
- {"fail authentication", true, args{context.Background(), apiv1.Options{}}, nil, true},
- {"fail credentials", false, args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true},
+ {"ok", args{context.Background(), apiv1.Options{}}, &CloudKMS{client: &MockClient{}}, false},
+ {"ok with uri", args{context.Background(), apiv1.Options{URI: "cloudkms:"}}, &CloudKMS{client: &MockClient{}}, false},
+ {"fail credentials", args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true},
+ {"fail with uri", args{context.Background(), apiv1.Options{URI: "cloudkms:credentials-file=testdata/missing"}}, nil, true},
+ {"fail schema", args{context.Background(), apiv1.Options{URI: "pkcs11:"}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if tt.skipOnCI && os.Getenv("CI") == "true" {
- t.SkipNow()
+ got, err := New(tt.args.ctx, tt.args.opts)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
+ return
}
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("New() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+func TestNew_real(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ opts apiv1.Options
+ }
+ tests := []struct {
+ name string
+ args args
+ want *CloudKMS
+ wantErr bool
+ }{
+ {"fail credentials", args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true},
+ {"fail with uri", args{context.Background(), apiv1.Options{URI: "cloudkms:credentials-file=testdata/missing"}}, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
got, err := New(tt.args.ctx, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/kms/pkcs11/benchmark_test.go b/kms/pkcs11/benchmark_test.go
new file mode 100644
index 00000000..30e21117
--- /dev/null
+++ b/kms/pkcs11/benchmark_test.go
@@ -0,0 +1,82 @@
+// +build cgo
+
+package pkcs11
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "testing"
+
+ "github.com/smallstep/certificates/kms/apiv1"
+)
+
+func benchmarkSign(b *testing.B, signer crypto.Signer, opts crypto.SignerOpts) {
+ hash := opts.HashFunc()
+ h := hash.New()
+ h.Write([]byte("buggy-coheir-RUBRIC-rabbet-liberal-eaglet-khartoum-stagger"))
+ digest := h.Sum(nil)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ signer.Sign(rand.Reader, digest, opts)
+ }
+ b.StopTimer()
+}
+
+func BenchmarkSignRSA(b *testing.B) {
+ k := setupPKCS11(b)
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7371;object=rsa-key",
+ })
+ if err != nil {
+ b.Fatalf("PKCS11.CreateSigner() error = %v", err)
+ }
+ benchmarkSign(b, signer, crypto.SHA256)
+}
+
+func BenchmarkSignRSAPSS(b *testing.B) {
+ k := setupPKCS11(b)
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7372;object=rsa-pss-key",
+ })
+ if err != nil {
+ b.Fatalf("PKCS11.CreateSigner() error = %v", err)
+ }
+ benchmarkSign(b, signer, &rsa.PSSOptions{
+ SaltLength: rsa.PSSSaltLengthEqualsHash,
+ Hash: crypto.SHA256,
+ })
+}
+
+func BenchmarkSignP256(b *testing.B) {
+ k := setupPKCS11(b)
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7373;object=ecdsa-p256-key",
+ })
+ if err != nil {
+ b.Fatalf("PKCS11.CreateSigner() error = %v", err)
+ }
+ benchmarkSign(b, signer, crypto.SHA256)
+}
+
+func BenchmarkSignP384(b *testing.B) {
+ k := setupPKCS11(b)
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7374;object=ecdsa-p384-key",
+ })
+ if err != nil {
+ b.Fatalf("PKCS11.CreateSigner() error = %v", err)
+ }
+ benchmarkSign(b, signer, crypto.SHA384)
+}
+
+func BenchmarkSignP521(b *testing.B) {
+ k := setupPKCS11(b)
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7375;object=ecdsa-p521-key",
+ })
+ if err != nil {
+ b.Fatalf("PKCS11.CreateSigner() error = %v", err)
+ }
+ benchmarkSign(b, signer, crypto.SHA512)
+}
diff --git a/kms/pkcs11/opensc_test.go b/kms/pkcs11/opensc_test.go
new file mode 100644
index 00000000..f3b61932
--- /dev/null
+++ b/kms/pkcs11/opensc_test.go
@@ -0,0 +1,60 @@
+// +build opensc
+
+package pkcs11
+
+import (
+ "runtime"
+ "sync"
+
+ "github.com/ThalesIgnite/crypto11"
+)
+
+var softHSM2Once sync.Once
+
+// mustPKCS11 configures a *PKCS11 KMS to be used with OpenSC, using for example
+// a Nitrokey HSM. To initialize these tests we should run:
+// sc-hsm-tool --initialize --so-pin 3537363231383830 --pin 123456
+// Or:
+// pkcs11-tool --module /usr/local/lib/opensc-pkcs11.so \
+// --init-token --init-pin \
+// --so-pin=3537363231383830 --new-pin=123456 --pin=123456 \
+// --label="pkcs11-test"
+func mustPKCS11(t TBTesting) *PKCS11 {
+ t.Helper()
+ testModule = "OpenSC"
+ if runtime.GOARCH != "amd64" {
+ t.Fatalf("opensc test skipped on %s:%s", runtime.GOOS, runtime.GOARCH)
+ }
+
+ var path string
+ switch runtime.GOOS {
+ case "darwin":
+ path = "/usr/local/lib/opensc-pkcs11.so"
+ case "linux":
+ path = "/usr/local/lib/opensc-pkcs11.so"
+ default:
+ t.Skipf("opensc test skipped on %s", runtime.GOOS)
+ return nil
+ }
+ var zero int
+ p11, err := crypto11.Configure(&crypto11.Config{
+ Path: path,
+ SlotNumber: &zero,
+ Pin: "123456",
+ })
+ if err != nil {
+ t.Fatalf("failed to configure opensc on %s: %v", runtime.GOOS, err)
+ }
+
+ k := &PKCS11{
+ p11: p11,
+ }
+
+ // Setup
+ softHSM2Once.Do(func() {
+ teardown(t, k)
+ setup(t, k)
+ })
+
+ return k
+}
diff --git a/kms/pkcs11/other_test.go b/kms/pkcs11/other_test.go
new file mode 100644
index 00000000..835587f7
--- /dev/null
+++ b/kms/pkcs11/other_test.go
@@ -0,0 +1,174 @@
+// +build cgo,!softhsm2,!yubihsm2,!opensc
+
+package pkcs11
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "io"
+ "math/big"
+
+ "github.com/ThalesIgnite/crypto11"
+ "github.com/pkg/errors"
+)
+
+func mustPKCS11(t TBTesting) *PKCS11 {
+ t.Helper()
+ testModule = "Golang crypto"
+ k := &PKCS11{
+ p11: &stubPKCS11{
+ signerIndex: make(map[keyType]int),
+ certIndex: make(map[keyType]int),
+ },
+ }
+ for i := range testCerts {
+ testCerts[i].Certificates = nil
+ }
+ teardown(t, k)
+ setup(t, k)
+ return k
+}
+
+type keyType struct {
+ id string
+ label string
+ serial string
+}
+
+func newKey(id, label []byte, serial *big.Int) keyType {
+ var serialString string
+ if serial != nil {
+ serialString = serial.String()
+ }
+ return keyType{
+ id: string(id),
+ label: string(label),
+ serial: serialString,
+ }
+}
+
+type stubPKCS11 struct {
+ signers []crypto11.Signer
+ certs []*x509.Certificate
+ signerIndex map[keyType]int
+ certIndex map[keyType]int
+}
+
+func (s *stubPKCS11) FindKeyPair(id, label []byte) (crypto11.Signer, error) {
+ if id == nil && label == nil {
+ return nil, errors.New("id and label cannot both be nil")
+ }
+ if i, ok := s.signerIndex[newKey(id, label, nil)]; ok {
+ return s.signers[i], nil
+ }
+ return nil, nil
+}
+
+func (s *stubPKCS11) FindCertificate(id, label []byte, serial *big.Int) (*x509.Certificate, error) {
+ if id == nil && label == nil && serial == nil {
+ return nil, errors.New("id, label and serial cannot both be nil")
+ }
+ if i, ok := s.certIndex[newKey(id, label, serial)]; ok {
+ return s.certs[i], nil
+ }
+ return nil, nil
+
+}
+
+func (s *stubPKCS11) ImportCertificateWithLabel(id, label []byte, cert *x509.Certificate) error {
+ switch {
+ case id == nil && label == nil:
+ return errors.New("id and label cannot both be nil")
+ case cert == nil:
+ return errors.New("certificate cannot be nil")
+ }
+
+ i := len(s.certs)
+ s.certs = append(s.certs, cert)
+ s.certIndex[newKey(id, label, cert.SerialNumber)] = i
+ s.certIndex[newKey(id, nil, nil)] = i
+ s.certIndex[newKey(nil, label, nil)] = i
+ s.certIndex[newKey(nil, nil, cert.SerialNumber)] = i
+ s.certIndex[newKey(id, label, nil)] = i
+ s.certIndex[newKey(id, nil, cert.SerialNumber)] = i
+ s.certIndex[newKey(nil, label, cert.SerialNumber)] = i
+
+ return nil
+}
+
+func (s *stubPKCS11) DeleteCertificate(id, label []byte, serial *big.Int) error {
+ if id == nil && label == nil && serial == nil {
+ return errors.New("id, label and serial cannot both be nil")
+ }
+ if i, ok := s.certIndex[newKey(id, label, serial)]; ok {
+ s.certs[i] = nil
+ }
+ return nil
+}
+
+func (s *stubPKCS11) GenerateRSAKeyPairWithLabel(id, label []byte, bits int) (crypto11.SignerDecrypter, error) {
+ if id == nil && label == nil {
+ return nil, errors.New("id and label cannot both be nil")
+ }
+ p, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ return nil, err
+ }
+ k := &privateKey{
+ Signer: p,
+ index: len(s.signers),
+ stub: s,
+ }
+ s.signers = append(s.signers, k)
+ s.signerIndex[newKey(id, label, nil)] = k.index
+ s.signerIndex[newKey(id, nil, nil)] = k.index
+ s.signerIndex[newKey(nil, label, nil)] = k.index
+ return k, nil
+}
+
+func (s *stubPKCS11) GenerateECDSAKeyPairWithLabel(id, label []byte, curve elliptic.Curve) (crypto11.Signer, error) {
+ if id == nil && label == nil {
+ return nil, errors.New("id and label cannot both be nil")
+ }
+ p, err := ecdsa.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+ k := &privateKey{
+ Signer: p,
+ index: len(s.signers),
+ stub: s,
+ }
+ s.signers = append(s.signers, k)
+ s.signerIndex[newKey(id, label, nil)] = k.index
+ s.signerIndex[newKey(id, nil, nil)] = k.index
+ s.signerIndex[newKey(nil, label, nil)] = k.index
+ return k, nil
+}
+
+func (s *stubPKCS11) Close() error {
+ return nil
+}
+
+type privateKey struct {
+ crypto.Signer
+ index int
+ stub *stubPKCS11
+}
+
+func (s *privateKey) Delete() error {
+ s.stub.signers[s.index] = nil
+ return nil
+}
+
+func (s *privateKey) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) {
+ k, ok := s.Signer.(*rsa.PrivateKey)
+ if !ok {
+ return nil, errors.New("key is not an rsa key")
+ }
+ return k.Decrypt(rand, msg, opts)
+}
diff --git a/kms/pkcs11/pkcs11.go b/kms/pkcs11/pkcs11.go
new file mode 100644
index 00000000..47c298a5
--- /dev/null
+++ b/kms/pkcs11/pkcs11.go
@@ -0,0 +1,354 @@
+// +build cgo
+
+package pkcs11
+
+import (
+ "context"
+ "crypto"
+ "crypto/elliptic"
+ "crypto/x509"
+ "encoding/hex"
+ "fmt"
+ "math/big"
+ "strconv"
+ "sync"
+
+ "github.com/ThalesIgnite/crypto11"
+ "github.com/pkg/errors"
+ "github.com/smallstep/certificates/kms/apiv1"
+ "github.com/smallstep/certificates/kms/uri"
+)
+
+// Scheme is the scheme used in uris.
+const Scheme = "pkcs11"
+
+// DefaultRSASize is the number of bits of a new RSA key if no size has been
+// specified.
+const DefaultRSASize = 3072
+
+// P11 defines the methods on crypto11.Context that this package will use. This
+// interface will be used for unit testing.
+type P11 interface {
+ FindKeyPair(id, label []byte) (crypto11.Signer, error)
+ FindCertificate(id, label []byte, serial *big.Int) (*x509.Certificate, error)
+ ImportCertificateWithLabel(id, label []byte, cert *x509.Certificate) error
+ DeleteCertificate(id, label []byte, serial *big.Int) error
+ GenerateRSAKeyPairWithLabel(id, label []byte, bits int) (crypto11.SignerDecrypter, error)
+ GenerateECDSAKeyPairWithLabel(id, label []byte, curve elliptic.Curve) (crypto11.Signer, error)
+ Close() error
+}
+
+var p11Configure = func(config *crypto11.Config) (P11, error) {
+ return crypto11.Configure(config)
+}
+
+// PKCS11 is the implementation of a KMS using the PKCS #11 standard.
+type PKCS11 struct {
+ p11 P11
+ closed sync.Once
+}
+
+// New returns a new PKCS11 KMS.
+func New(ctx context.Context, opts apiv1.Options) (*PKCS11, error) {
+ var config crypto11.Config
+ if opts.URI != "" {
+ u, err := uri.ParseWithScheme(Scheme, opts.URI)
+ if err != nil {
+ return nil, err
+ }
+
+ config.Pin = u.Pin()
+ config.Path = u.Get("module-path")
+ config.TokenLabel = u.Get("token")
+ config.TokenSerial = u.Get("serial")
+ if v := u.Get("slot-id"); v != "" {
+ n, err := strconv.Atoi(v)
+ if err != nil {
+ return nil, errors.Wrap(err, "kms uri 'slot-id' is not valid")
+ }
+ config.SlotNumber = &n
+ }
+ }
+ if config.Pin == "" && opts.Pin != "" {
+ config.Pin = opts.Pin
+ }
+
+ switch {
+ case config.Path == "":
+ return nil, errors.New("kms uri 'module-path' are required")
+ case config.TokenLabel == "" && config.TokenSerial == "" && config.SlotNumber == nil:
+ return nil, errors.New("kms uri 'token', 'serial' or 'slot-id' are required")
+ case config.Pin == "":
+ return nil, errors.New("kms 'pin' cannot be empty")
+ case config.TokenLabel != "" && config.TokenSerial != "":
+ return nil, errors.New("kms uri 'token' and 'serial' are mutually exclusive")
+ case config.TokenLabel != "" && config.SlotNumber != nil:
+ return nil, errors.New("kms uri 'token' and 'slot-id' are mutually exclusive")
+ case config.TokenSerial != "" && config.SlotNumber != nil:
+ return nil, errors.New("kms uri 'serial' and 'slot-id' are mutually exclusive")
+ }
+
+ p11, err := p11Configure(&config)
+ if err != nil {
+ return nil, errors.Wrap(err, "error initializing PKCS#11")
+ }
+
+ return &PKCS11{
+ p11: p11,
+ }, nil
+}
+
+func init() {
+ apiv1.Register(apiv1.PKCS11, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
+ return New(ctx, opts)
+ })
+}
+
+// GetPublicKey returns the public key ....
+func (k *PKCS11) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
+ if req.Name == "" {
+ return nil, errors.New("getPublicKeyRequest 'name' cannot be empty")
+ }
+
+ signer, err := findSigner(k.p11, req.Name)
+ if err != nil {
+ return nil, errors.Wrap(err, "getPublicKey failed")
+ }
+
+ return signer.Public(), nil
+}
+
+// CreateKey generates a new key in the PKCS#11 module and returns the public key.
+func (k *PKCS11) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
+ switch {
+ case req.Name == "":
+ return nil, errors.New("createKeyRequest 'name' cannot be empty")
+ case req.Bits < 0:
+ return nil, errors.New("createKeyRequest 'bits' cannot be negative")
+ }
+
+ signer, err := generateKey(k.p11, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "createKey failed")
+ }
+
+ return &apiv1.CreateKeyResponse{
+ Name: req.Name,
+ PublicKey: signer.Public(),
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: req.Name,
+ },
+ }, nil
+}
+
+// CreateSigner creates a signer using the key present in the PKCS#11 MODULE signature
+// slot.
+func (k *PKCS11) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
+ switch {
+ case req.SigningKey == "":
+ return nil, errors.New("createSignerRequest 'signingKey' cannot be empty")
+ }
+
+ signer, err := findSigner(k.p11, req.SigningKey)
+ if err != nil {
+ return nil, errors.Wrap(err, "createSigner failed")
+ }
+
+ return signer, nil
+}
+
+// LoadCertificate implements kms.CertificateManager and loads a certificate
+// from the YubiKey.
+func (k *PKCS11) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) {
+ if req.Name == "" {
+ return nil, errors.New("loadCertificateRequest 'name' cannot be nil")
+ }
+ cert, err := findCertificate(k.p11, req.Name)
+ if err != nil {
+ return nil, errors.Wrap(err, "loadCertificate failed")
+ }
+ return cert, nil
+}
+
+// StoreCertificate implements kms.CertificateManager and stores a certificate
+// in the YubiKey.
+func (k *PKCS11) StoreCertificate(req *apiv1.StoreCertificateRequest) error {
+ switch {
+ case req.Name == "":
+ return errors.New("storeCertificateRequest 'name' cannot be empty")
+ case req.Certificate == nil:
+ return errors.New("storeCertificateRequest 'Certificate' cannot be nil")
+ }
+
+ id, object, err := parseObject(req.Name)
+ if err != nil {
+ return errors.Wrap(err, "storeCertificate failed")
+ }
+
+ cert, err := k.p11.FindCertificate(id, object, nil)
+ if err != nil {
+ return errors.Wrap(err, "storeCertificate failed")
+ }
+ if cert != nil {
+ return errors.Wrap(apiv1.ErrAlreadyExists{
+ Message: req.Name + " already exists",
+ }, "storeCertificate failed")
+ }
+
+ if err := k.p11.ImportCertificateWithLabel(id, object, req.Certificate); err != nil {
+ return errors.Wrap(err, "storeCertificate failed")
+ }
+
+ return nil
+}
+
+// DeleteKey is a utility function to delete a key given an uri.
+func (k *PKCS11) DeleteKey(uri string) error {
+ id, object, err := parseObject(uri)
+ if err != nil {
+ return errors.Wrap(err, "deleteKey failed")
+ }
+ signer, err := k.p11.FindKeyPair(id, object)
+ if err != nil {
+ return errors.Wrap(err, "deleteKey failed")
+ }
+ if signer == nil {
+ return nil
+ }
+ if err := signer.Delete(); err != nil {
+ return errors.Wrap(err, "deleteKey failed")
+ }
+ return nil
+}
+
+// DeleteCertificate is a utility function to delete a certificate given an uri.
+func (k *PKCS11) DeleteCertificate(uri string) error {
+ id, object, err := parseObject(uri)
+ if err != nil {
+ return errors.Wrap(err, "deleteCertificate failed")
+ }
+ if err := k.p11.DeleteCertificate(id, object, nil); err != nil {
+ return errors.Wrap(err, "deleteCertificate failed")
+ }
+ return nil
+}
+
+// Close releases the connection to the PKCS#11 module.
+func (k *PKCS11) Close() (err error) {
+ k.closed.Do(func() {
+ err = errors.Wrap(k.p11.Close(), "error closing pkcs#11 context")
+ })
+ return
+}
+
+func toByte(s string) []byte {
+ if s == "" {
+ return nil
+ }
+ return []byte(s)
+}
+
+func parseObject(rawuri string) ([]byte, []byte, error) {
+ u, err := uri.ParseWithScheme(Scheme, rawuri)
+ if err != nil {
+ return nil, nil, err
+ }
+ id := u.GetEncoded("id")
+ object := u.Get("object")
+ if len(id) == 0 && object == "" {
+ return nil, nil, errors.Errorf("key with uri %s is not valid, id or object are required", rawuri)
+ }
+
+ return id, toByte(object), nil
+}
+
+func generateKey(ctx P11, req *apiv1.CreateKeyRequest) (crypto11.Signer, error) {
+ id, object, err := parseObject(req.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ signer, err := ctx.FindKeyPair(id, object)
+ if err != nil {
+ return nil, err
+ }
+ if signer != nil {
+ return nil, apiv1.ErrAlreadyExists{
+ Message: req.Name + " already exists",
+ }
+ }
+
+ // Enforce the use of both id and labels. This is not strictly necessary in
+ // PKCS #11, but it's a good practice.
+ if len(id) == 0 || len(object) == 0 {
+ return nil, errors.Errorf("key with uri %s is not valid, id and object are required", req.Name)
+ }
+
+ bits := req.Bits
+ if bits == 0 {
+ bits = DefaultRSASize
+ }
+
+ switch req.SignatureAlgorithm {
+ case apiv1.UnspecifiedSignAlgorithm:
+ return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P256())
+ case apiv1.SHA256WithRSA, apiv1.SHA384WithRSA, apiv1.SHA512WithRSA:
+ return ctx.GenerateRSAKeyPairWithLabel(id, object, bits)
+ case apiv1.SHA256WithRSAPSS, apiv1.SHA384WithRSAPSS, apiv1.SHA512WithRSAPSS:
+ return ctx.GenerateRSAKeyPairWithLabel(id, object, bits)
+ case apiv1.ECDSAWithSHA256:
+ return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P256())
+ case apiv1.ECDSAWithSHA384:
+ return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P384())
+ case apiv1.ECDSAWithSHA512:
+ return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P521())
+ case apiv1.PureEd25519:
+ return nil, fmt.Errorf("signature algorithm %s is not supported", req.SignatureAlgorithm)
+ default:
+ return nil, fmt.Errorf("signature algorithm %s is not supported", req.SignatureAlgorithm)
+ }
+}
+
+func findSigner(ctx P11, rawuri string) (crypto11.Signer, error) {
+ id, object, err := parseObject(rawuri)
+ if err != nil {
+ return nil, err
+ }
+ signer, err := ctx.FindKeyPair(id, object)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error finding key with uri %s", rawuri)
+ }
+ if signer == nil {
+ return nil, errors.Errorf("key with uri %s not found", rawuri)
+ }
+ return signer, nil
+}
+
+func findCertificate(ctx P11, rawuri string) (*x509.Certificate, error) {
+ u, err := uri.ParseWithScheme(Scheme, rawuri)
+ if err != nil {
+ return nil, err
+ }
+ id, object, serial := u.GetEncoded("id"), u.Get("object"), u.Get("serial")
+ if len(id) == 0 && object == "" && serial == "" {
+ return nil, errors.Errorf("key with uri %s is not valid, id, object or serial are required", rawuri)
+ }
+
+ var serialNumber *big.Int
+ if serial != "" {
+ b, err := hex.DecodeString(serial)
+ if err != nil {
+ return nil, errors.Errorf("key with uri %s is not valid, failed to decode serial", rawuri)
+ }
+ serialNumber = new(big.Int).SetBytes(b)
+ }
+
+ cert, err := ctx.FindCertificate(id, toByte(object), serialNumber)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error finding certificate with uri %s", rawuri)
+ }
+ if cert == nil {
+ return nil, errors.Errorf("certificate with uri %s not found", rawuri)
+ }
+ return cert, nil
+}
diff --git a/kms/pkcs11/pkcs11_no_cgo.go b/kms/pkcs11/pkcs11_no_cgo.go
new file mode 100644
index 00000000..87c9a36b
--- /dev/null
+++ b/kms/pkcs11/pkcs11_no_cgo.go
@@ -0,0 +1,57 @@
+// +build !cgo
+
+package pkcs11
+
+import (
+ "context"
+ "crypto"
+ "os"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/certificates/kms/apiv1"
+)
+
+var errUnsupported error
+
+func init() {
+ name := filepath.Base(os.Args[0])
+ errUnsupported = errors.Errorf("unsupported kms type 'pkcs11': %s is compiled without cgo support", name)
+
+ apiv1.Register(apiv1.PKCS11, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
+ return nil, errUnsupported
+ })
+}
+
+// PKCS11 is the implementation of a KMS using the PKCS #11 standard.
+type PKCS11 struct{}
+
+// New implements the kms.KeyManager interface and without CGO will always
+// return an error.
+func New(ctx context.Context, opts apiv1.Options) (*PKCS11, error) {
+ return nil, errUnsupported
+}
+
+// GetPublicKey implements the kms.KeyManager interface and without CGO will always
+// return an error.
+func (*PKCS11) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
+ return nil, errUnsupported
+}
+
+// CreateKey implements the kms.KeyManager interface and without CGO will always
+// return an error.
+func (*PKCS11) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
+ return nil, errUnsupported
+}
+
+// CreateSigner implements the kms.KeyManager interface and without CGO will always
+// return an error.
+func (*PKCS11) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
+ return nil, errUnsupported
+}
+
+// Close implements the kms.KeyManager interface and without CGO will always
+// return an error.
+func (*PKCS11) Close() error {
+ return errUnsupported
+}
diff --git a/kms/pkcs11/pkcs11_test.go b/kms/pkcs11/pkcs11_test.go
new file mode 100644
index 00000000..77277366
--- /dev/null
+++ b/kms/pkcs11/pkcs11_test.go
@@ -0,0 +1,729 @@
+// +build cgo
+
+package pkcs11
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "math/big"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/ThalesIgnite/crypto11"
+ "github.com/pkg/errors"
+ "github.com/smallstep/certificates/kms/apiv1"
+ "golang.org/x/crypto/cryptobyte"
+ "golang.org/x/crypto/cryptobyte/asn1"
+)
+
+func TestNew(t *testing.T) {
+ tmp := p11Configure
+ t.Cleanup(func() {
+ p11Configure = tmp
+ })
+
+ k := mustPKCS11(t)
+ t.Cleanup(func() {
+ k.Close()
+ })
+ p11Configure = func(config *crypto11.Config) (P11, error) {
+ if strings.Contains(config.Path, "fail") {
+ return nil, errors.New("an error")
+ }
+ return k.p11, nil
+ }
+
+ type args struct {
+ ctx context.Context
+ opts apiv1.Options
+ }
+ tests := []struct {
+ name string
+ args args
+ want *PKCS11
+ wantErr bool
+ }{
+ {"ok", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test?pin-value=password",
+ }}, k, false},
+ {"ok with serial", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;serial=0123456789?pin-value=password",
+ }}, k, false},
+ {"ok with slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;slot-id=0?pin-value=password",
+ }}, k, false},
+ {"ok with pin", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test",
+ Pin: "passowrd",
+ }}, k, false},
+ {"fail missing module", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:token=pkcs11-test",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail missing pin", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test",
+ }}, nil, true},
+ {"fail missing token/serial/slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail token+serial+slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test;serial=0123456789;slot-id=0",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail token+serial", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test;serial=0123456789",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail token+slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test;slot-id=0",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail serial+slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;serial=0123456789;slot-id=0",
+ Pin: "passowrd",
+ }}, nil, true},
+ {"fail slot-id", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;slot-id=x?pin-value=password",
+ }}, nil, true},
+ {"fail scheme", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "foo:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=pkcs11-test?pin-value=password",
+ }}, nil, true},
+ {"fail configure", args{context.Background(), apiv1.Options{
+ Type: "pkcs11",
+ URI: "pkcs11:module-path=/usr/local/lib/fail.so;token=pkcs11-test?pin-value=password",
+ }}, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := New(tt.args.ctx, tt.args.opts)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("New() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestPKCS11_GetPublicKey(t *testing.T) {
+ k := setupPKCS11(t)
+ type args struct {
+ req *apiv1.GetPublicKeyRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ want crypto.PublicKey
+ wantErr bool
+ }{
+ {"RSA", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:id=7371;object=rsa-key",
+ }}, &rsa.PublicKey{}, false},
+ {"RSA by id", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:id=7371",
+ }}, &rsa.PublicKey{}, false},
+ {"RSA by label", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:object=rsa-key",
+ }}, &rsa.PublicKey{}, false},
+ {"ECDSA", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:id=7373;object=ecdsa-p256-key",
+ }}, &ecdsa.PublicKey{}, false},
+ {"ECDSA by id", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:id=7373",
+ }}, &ecdsa.PublicKey{}, false},
+ {"ECDSA by label", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:object=ecdsa-p256-key",
+ }}, &ecdsa.PublicKey{}, false},
+ {"fail name", args{&apiv1.GetPublicKeyRequest{
+ Name: "",
+ }}, nil, true},
+ {"fail uri", args{&apiv1.GetPublicKeyRequest{
+ Name: "https:id=9999;object=https",
+ }}, nil, true},
+ {"fail missing", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:id=9999;object=rsa-key",
+ }}, nil, true},
+ {"fail FindKeyPair", args{&apiv1.GetPublicKeyRequest{
+ Name: "pkcs11:foo=bar",
+ }}, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := k.GetPublicKey(tt.args.req)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
+ t.Errorf("PKCS11.GetPublicKey() = %T, want %T", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestPKCS11_CreateKey(t *testing.T) {
+ k := setupPKCS11(t)
+
+ // Make sure to delete the created key
+ k.DeleteKey(testObject)
+
+ type args struct {
+ req *apiv1.CreateKeyRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ want *apiv1.CreateKeyResponse
+ wantErr bool
+ }{
+ {"default", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &ecdsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA256WithRSA", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA256WithRSA,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA384WithRSA", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA384WithRSA,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA512WithRSA", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA512WithRSA,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA256WithRSAPSS", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA256WithRSAPSS,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA384WithRSAPSS", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA384WithRSAPSS,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA SHA512WithRSAPSS", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA512WithRSAPSS,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA 2048", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA256WithRSA,
+ Bits: 2048,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"RSA 4096", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.SHA256WithRSA,
+ Bits: 4096,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &rsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"ECDSA P256", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.ECDSAWithSHA256,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &ecdsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"ECDSA P384", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.ECDSAWithSHA384,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &ecdsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"ECDSA P521", args{&apiv1.CreateKeyRequest{
+ Name: testObject,
+ SignatureAlgorithm: apiv1.ECDSAWithSHA512,
+ }}, &apiv1.CreateKeyResponse{
+ Name: testObject,
+ PublicKey: &ecdsa.PublicKey{},
+ CreateSignerRequest: apiv1.CreateSignerRequest{
+ SigningKey: testObject,
+ },
+ }, false},
+ {"fail name", args{&apiv1.CreateKeyRequest{
+ Name: "",
+ }}, nil, true},
+ {"fail no id", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:object=create-key",
+ }}, nil, true},
+ {"fail no object", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:id=9999",
+ }}, nil, true},
+ {"fail schema", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs12:id=9999;object=create-key",
+ }}, nil, true},
+ {"fail bits", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:id=9999;object=create-key",
+ Bits: -1,
+ SignatureAlgorithm: apiv1.SHA256WithRSAPSS,
+ }}, nil, true},
+ {"fail ed25519", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:id=9999;object=create-key",
+ SignatureAlgorithm: apiv1.PureEd25519,
+ }}, nil, true},
+ {"fail unknown", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:id=9999;object=create-key",
+ SignatureAlgorithm: apiv1.SignatureAlgorithm(100),
+ }}, nil, true},
+ {"fail FindKeyPair", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:foo=bar",
+ SignatureAlgorithm: apiv1.SHA256WithRSAPSS,
+ }}, nil, true},
+ {"fail already exists", args{&apiv1.CreateKeyRequest{
+ Name: "pkcs11:id=7373;object=ecdsa-p256-key",
+ SignatureAlgorithm: apiv1.ECDSAWithSHA256,
+ }}, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := k.CreateKey(tt.args.req)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != nil {
+ got.PublicKey = tt.want.PublicKey
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("PKCS11.CreateKey() = %v, want %v", got, tt.want)
+ }
+ if got != nil {
+ if err := k.DeleteKey(got.Name); err != nil {
+ t.Errorf("PKCS11.DeleteKey() error = %v", err)
+ }
+ }
+ })
+ }
+}
+
+func TestPKCS11_CreateSigner(t *testing.T) {
+ k := setupPKCS11(t)
+ data := []byte("buggy-coheir-RUBRIC-rabbet-liberal-eaglet-khartoum-stagger")
+
+ // VerifyASN1 verifies the ASN.1 encoded signature, sig, of hash using the
+ // public key, pub. Its return value records whether the signature is valid.
+ verifyASN1 := func(pub *ecdsa.PublicKey, hash, sig []byte) bool {
+ var (
+ r, s = &big.Int{}, &big.Int{}
+ inner cryptobyte.String
+ )
+ input := cryptobyte.String(sig)
+ if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
+ !input.Empty() ||
+ !inner.ReadASN1Integer(r) ||
+ !inner.ReadASN1Integer(s) ||
+ !inner.Empty() {
+ return false
+ }
+ return ecdsa.Verify(pub, hash, r, s)
+ }
+
+ type args struct {
+ req *apiv1.CreateSignerRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ algorithm apiv1.SignatureAlgorithm
+ signerOpts crypto.SignerOpts
+ wantErr bool
+ }{
+ // SoftHSM2
+ {"RSA", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7371;object=rsa-key",
+ }}, apiv1.SHA256WithRSA, crypto.SHA256, false},
+ {"RSA PSS", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7372;object=rsa-pss-key",
+ }}, apiv1.SHA256WithRSAPSS, &rsa.PSSOptions{
+ SaltLength: rsa.PSSSaltLengthEqualsHash,
+ Hash: crypto.SHA256,
+ }, false},
+ {"ECDSA P256", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7373;object=ecdsa-p256-key",
+ }}, apiv1.ECDSAWithSHA256, crypto.SHA256, false},
+ {"ECDSA P384", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7374;object=ecdsa-p384-key",
+ }}, apiv1.ECDSAWithSHA384, crypto.SHA384, false},
+ {"ECDSA P521", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:id=7375;object=ecdsa-p521-key",
+ }}, apiv1.ECDSAWithSHA512, crypto.SHA512, false},
+ {"fail SigningKey", args{&apiv1.CreateSignerRequest{
+ SigningKey: "",
+ }}, 0, nil, true},
+ {"fail uri", args{&apiv1.CreateSignerRequest{
+ SigningKey: "https:id=7375;object=ecdsa-p521-key",
+ }}, 0, nil, true},
+ {"fail FindKeyPair", args{&apiv1.CreateSignerRequest{
+ SigningKey: "pkcs11:foo=bar",
+ }}, 0, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := k.CreateSigner(tt.args.req)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.CreateSigner() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ if got != nil {
+ hash := tt.signerOpts.HashFunc()
+ h := hash.New()
+ h.Write(data)
+ digest := h.Sum(nil)
+ sig, err := got.Sign(rand.Reader, digest, tt.signerOpts)
+ if err != nil {
+ t.Errorf("cyrpto.Signer.Sign() error = %v", err)
+ }
+
+ switch tt.algorithm {
+ case apiv1.SHA256WithRSA, apiv1.SHA384WithRSA, apiv1.SHA512WithRSA:
+ pub := got.Public().(*rsa.PublicKey)
+ if err := rsa.VerifyPKCS1v15(pub, hash, digest, sig); err != nil {
+ t.Errorf("rsa.VerifyPKCS1v15() error = %v", err)
+ }
+ case apiv1.UnspecifiedSignAlgorithm, apiv1.SHA256WithRSAPSS, apiv1.SHA384WithRSAPSS, apiv1.SHA512WithRSAPSS:
+ pub := got.Public().(*rsa.PublicKey)
+ if err := rsa.VerifyPSS(pub, hash, digest, sig, tt.signerOpts.(*rsa.PSSOptions)); err != nil {
+ t.Errorf("rsa.VerifyPSS() error = %v", err)
+ }
+ case apiv1.ECDSAWithSHA256, apiv1.ECDSAWithSHA384, apiv1.ECDSAWithSHA512:
+ pub := got.Public().(*ecdsa.PublicKey)
+ if !verifyASN1(pub, digest, sig) {
+ t.Error("ecdsa.VerifyASN1() failed")
+ }
+ default:
+ t.Errorf("signature algorithm %s is not supported", tt.algorithm)
+ }
+
+ }
+
+ })
+ }
+}
+
+func TestPKCS11_LoadCertificate(t *testing.T) {
+ k := setupPKCS11(t)
+
+ getCertFn := func(i, j int) func() *x509.Certificate {
+ return func() *x509.Certificate {
+ return testCerts[i].Certificates[j]
+ }
+ }
+
+ type args struct {
+ req *apiv1.LoadCertificateRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ wantFn func() *x509.Certificate
+ wantErr bool
+ }{
+ {"load", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:id=7376;object=test-root",
+ }}, getCertFn(0, 0), false},
+ {"load by id", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:id=7376",
+ }}, getCertFn(0, 0), false},
+ {"load by label", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:object=test-root",
+ }}, getCertFn(0, 0), false},
+ {"load by serial", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:serial=64",
+ }}, getCertFn(0, 0), false},
+ {"fail missing", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:id=9999;object=test-root",
+ }}, nil, true},
+ {"fail name", args{&apiv1.LoadCertificateRequest{
+ Name: "",
+ }}, nil, true},
+ {"fail scheme", args{&apiv1.LoadCertificateRequest{
+ Name: "foo:id=7376;object=test-root",
+ }}, nil, true},
+ {"fail serial", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:serial=foo",
+ }}, nil, true},
+ {"fail FindCertificate", args{&apiv1.LoadCertificateRequest{
+ Name: "pkcs11:foo=bar",
+ }}, nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := k.LoadCertificate(tt.args.req)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.LoadCertificate() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ var want *x509.Certificate
+ if tt.wantFn != nil {
+ want = tt.wantFn()
+ got.Raw, got.RawIssuer, got.RawSubject, got.RawTBSCertificate, got.RawSubjectPublicKeyInfo = nil, nil, nil, nil, nil
+ want.Raw, want.RawIssuer, want.RawSubject, want.RawTBSCertificate, want.RawSubjectPublicKeyInfo = nil, nil, nil, nil, nil
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("PKCS11.LoadCertificate() = %v, want %v", got, want)
+ }
+ })
+ }
+}
+
+func TestPKCS11_StoreCertificate(t *testing.T) {
+ k := setupPKCS11(t)
+
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey() error = %v", err)
+ }
+
+ cert, err := generateCertificate(pub, priv)
+ if err != nil {
+ t.Fatalf("x509.CreateCertificate() error = %v", err)
+ }
+
+ // Make sure to delete the created certificate
+ t.Cleanup(func() {
+ k.DeleteCertificate(testObject)
+ })
+
+ type args struct {
+ req *apiv1.StoreCertificateRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {"ok", args{&apiv1.StoreCertificateRequest{
+ Name: testObject,
+ Certificate: cert,
+ }}, false},
+ {"fail already exists", args{&apiv1.StoreCertificateRequest{
+ Name: testObject,
+ Certificate: cert,
+ }}, true},
+ {"fail name", args{&apiv1.StoreCertificateRequest{
+ Name: "",
+ Certificate: cert,
+ }}, true},
+ {"fail certificate", args{&apiv1.StoreCertificateRequest{
+ Name: testObject,
+ Certificate: nil,
+ }}, true},
+ {"fail uri", args{&apiv1.StoreCertificateRequest{
+ Name: "http:id=7770;object=create-cert",
+ Certificate: cert,
+ }}, true},
+ {"fail ImportCertificateWithLabel", args{&apiv1.StoreCertificateRequest{
+ Name: "pkcs11:foo=bar",
+ Certificate: cert,
+ }}, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := k.StoreCertificate(tt.args.req); (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.StoreCertificate() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if !tt.wantErr {
+ got, err := k.LoadCertificate(&apiv1.LoadCertificateRequest{
+ Name: tt.args.req.Name,
+ })
+ if err != nil {
+ t.Errorf("PKCS11.LoadCertificate() error = %v", err)
+ }
+ if !reflect.DeepEqual(got, cert) {
+ t.Errorf("PKCS11.LoadCertificate() = %v, want %v", got, cert)
+ }
+ }
+ })
+ }
+}
+
+func TestPKCS11_DeleteKey(t *testing.T) {
+ k := setupPKCS11(t)
+
+ type args struct {
+ uri string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {"delete", args{testObject}, false},
+ {"delete by id", args{testObjectByID}, false},
+ {"delete by label", args{testObjectByLabel}, false},
+ {"delete missing", args{"pkcs11:id=9999;object=missing-key"}, false},
+ {"fail name", args{""}, true},
+ {"fail FindKeyPair", args{"pkcs11:foo=bar"}, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if _, err := k.CreateKey(&apiv1.CreateKeyRequest{
+ Name: testObject,
+ }); err != nil {
+ t.Fatalf("PKCS1.CreateKey() error = %v", err)
+ }
+ if err := k.DeleteKey(tt.args.uri); (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.DeleteKey() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{
+ Name: tt.args.uri,
+ }); err == nil {
+ t.Error("PKCS11.GetPublicKey() public key found and not expected")
+ }
+ // Make sure to delete the created one.
+ if err := k.DeleteKey(testObject); err != nil {
+ t.Errorf("PKCS11.DeleteKey() error = %v", err)
+ }
+ })
+ }
+}
+
+func TestPKCS11_DeleteCertificate(t *testing.T) {
+ k := setupPKCS11(t)
+
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("ed25519.GenerateKey() error = %v", err)
+ }
+
+ cert, err := generateCertificate(pub, priv)
+ if err != nil {
+ t.Fatalf("x509.CreateCertificate() error = %v", err)
+ }
+
+ type args struct {
+ uri string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {"delete", args{testObject}, false},
+ {"delete by id", args{testObjectByID}, false},
+ {"delete by label", args{testObjectByLabel}, false},
+ {"delete missing", args{"pkcs11:id=9999;object=missing-key"}, false},
+ {"fail name", args{""}, true},
+ {"fail DeleteCertificate", args{"pkcs11:foo=bar"}, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := k.StoreCertificate(&apiv1.StoreCertificateRequest{
+ Name: testObject,
+ Certificate: cert,
+ }); err != nil {
+ t.Fatalf("PKCS11.StoreCertificate() error = %v", err)
+ }
+ if err := k.DeleteCertificate(tt.args.uri); (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.DeleteCertificate() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if _, err := k.LoadCertificate(&apiv1.LoadCertificateRequest{
+ Name: tt.args.uri,
+ }); err == nil {
+ t.Error("PKCS11.LoadCertificate() certificate found and not expected")
+ }
+ // Make sure to delete the created one.
+ if err := k.DeleteCertificate(testObject); err != nil {
+ t.Errorf("PKCS11.DeleteCertificate() error = %v", err)
+ }
+ })
+ }
+}
+
+func TestPKCS11_Close(t *testing.T) {
+ k := mustPKCS11(t)
+ tests := []struct {
+ name string
+ wantErr bool
+ }{
+ {"ok", false},
+ {"second", false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := k.Close(); (err != nil) != tt.wantErr {
+ t.Errorf("PKCS11.Close() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/kms/pkcs11/setup_test.go b/kms/pkcs11/setup_test.go
new file mode 100644
index 00000000..c9ff9311
--- /dev/null
+++ b/kms/pkcs11/setup_test.go
@@ -0,0 +1,143 @@
+// +build cgo
+
+package pkcs11
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/certificates/kms/apiv1"
+)
+
+var (
+ testModule = ""
+ testObject = "pkcs11:id=7370;object=test-name"
+ testObjectByID = "pkcs11:id=7370"
+ testObjectByLabel = "pkcs11:object=test-name"
+ testKeys = []struct {
+ Name string
+ SignatureAlgorithm apiv1.SignatureAlgorithm
+ Bits int
+ }{
+ {"pkcs11:id=7371;object=rsa-key", apiv1.SHA256WithRSA, 2048},
+ {"pkcs11:id=7372;object=rsa-pss-key", apiv1.SHA256WithRSAPSS, DefaultRSASize},
+ {"pkcs11:id=7373;object=ecdsa-p256-key", apiv1.ECDSAWithSHA256, 0},
+ {"pkcs11:id=7374;object=ecdsa-p384-key", apiv1.ECDSAWithSHA384, 0},
+ {"pkcs11:id=7375;object=ecdsa-p521-key", apiv1.ECDSAWithSHA512, 0},
+ }
+
+ testCerts = []struct {
+ Name string
+ Key string
+ Certificates []*x509.Certificate
+ }{
+ {"pkcs11:id=7376;object=test-root", "pkcs11:id=7373;object=ecdsa-p256-key", nil},
+ }
+)
+
+type TBTesting interface {
+ Helper()
+ Cleanup(f func())
+ Log(args ...interface{})
+ Errorf(format string, args ...interface{})
+ Fatalf(format string, args ...interface{})
+ Skipf(format string, args ...interface{})
+}
+
+func generateCertificate(pub crypto.PublicKey, signer crypto.Signer) (*x509.Certificate, error) {
+ now := time.Now()
+ template := &x509.Certificate{
+ Subject: pkix.Name{CommonName: "Test Root Certificate"},
+ Issuer: pkix.Name{CommonName: "Test Root Certificate"},
+ IsCA: true,
+ MaxPathLen: 1,
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ NotBefore: now,
+ NotAfter: now.Add(time.Hour),
+ SerialNumber: big.NewInt(100),
+ }
+
+ b, err := x509.CreateCertificate(rand.Reader, template, template, pub, signer)
+ if err != nil {
+ return nil, err
+ }
+
+ return x509.ParseCertificate(b)
+}
+
+func setup(t TBTesting, k *PKCS11) {
+ t.Log("Running using", testModule)
+ for _, tk := range testKeys {
+ _, err := k.CreateKey(&apiv1.CreateKeyRequest{
+ Name: tk.Name,
+ SignatureAlgorithm: tk.SignatureAlgorithm,
+ Bits: tk.Bits,
+ })
+ if err != nil && !errors.Is(errors.Cause(err), apiv1.ErrAlreadyExists{
+ Message: tk.Name + " already exists",
+ }) {
+ t.Errorf("PKCS11.GetPublicKey() error = %v", err)
+ }
+ }
+
+ for i, c := range testCerts {
+ signer, err := k.CreateSigner(&apiv1.CreateSignerRequest{
+ SigningKey: c.Key,
+ })
+ if err != nil {
+ t.Errorf("PKCS11.CreateSigner() error = %v", err)
+ continue
+ }
+ cert, err := generateCertificate(signer.Public(), signer)
+ if err != nil {
+ t.Errorf("x509.CreateCertificate() error = %v", err)
+ continue
+ }
+ if err := k.StoreCertificate(&apiv1.StoreCertificateRequest{
+ Name: c.Name,
+ Certificate: cert,
+ }); err != nil && !errors.Is(errors.Cause(err), apiv1.ErrAlreadyExists{
+ Message: c.Name + " already exists",
+ }) {
+ t.Errorf("PKCS1.StoreCertificate() error = %+v", err)
+ continue
+ }
+ testCerts[i].Certificates = append(testCerts[i].Certificates, cert)
+ }
+}
+
+func teardown(t TBTesting, k *PKCS11) {
+ testObjects := []string{testObject, testObjectByID, testObjectByLabel}
+ for _, name := range testObjects {
+ if err := k.DeleteKey(name); err != nil {
+ t.Errorf("PKCS11.DeleteKey() error = %v", err)
+ }
+ if err := k.DeleteCertificate(name); err != nil {
+ t.Errorf("PKCS11.DeleteCertificate() error = %v", err)
+ }
+ }
+ for _, tk := range testKeys {
+ if err := k.DeleteKey(tk.Name); err != nil {
+ t.Errorf("PKCS11.DeleteKey() error = %v", err)
+ }
+ }
+ for _, tc := range testCerts {
+ if err := k.DeleteCertificate(tc.Name); err != nil {
+ t.Errorf("PKCS11.DeleteCertificate() error = %v", err)
+ }
+ }
+}
+
+func setupPKCS11(t TBTesting) *PKCS11 {
+ t.Helper()
+ k := mustPKCS11(t)
+ t.Cleanup(func() {
+ k.Close()
+ })
+ return k
+}
diff --git a/kms/pkcs11/softhsm2_test.go b/kms/pkcs11/softhsm2_test.go
new file mode 100644
index 00000000..37aa667d
--- /dev/null
+++ b/kms/pkcs11/softhsm2_test.go
@@ -0,0 +1,59 @@
+// +build cgo,softhsm2
+
+package pkcs11
+
+import (
+ "runtime"
+ "sync"
+
+ "github.com/ThalesIgnite/crypto11"
+)
+
+var softHSM2Once sync.Once
+
+// mustPKCS11 configures a *PKCS11 KMS to be used with SoftHSM2. To initialize
+// these tests, we should run:
+// softhsm2-util --init-token --free \
+// --token pkcs11-test --label pkcs11-test \
+// --so-pin password --pin password
+//
+// To delete we should run:
+// softhsm2-util --delete-token --token pkcs11-test
+func mustPKCS11(t TBTesting) *PKCS11 {
+ t.Helper()
+ testModule = "SoftHSM2"
+ if runtime.GOARCH != "amd64" {
+ t.Fatalf("softHSM2 test skipped on %s:%s", runtime.GOOS, runtime.GOARCH)
+ }
+
+ var path string
+ switch runtime.GOOS {
+ case "darwin":
+ path = "/usr/local/lib/softhsm/libsofthsm2.so"
+ case "linux":
+ path = "/usr/lib/softhsm/libsofthsm2.so"
+ default:
+ t.Skipf("softHSM2 test skipped on %s", runtime.GOOS)
+ return nil
+ }
+ p11, err := crypto11.Configure(&crypto11.Config{
+ Path: path,
+ TokenLabel: "pkcs11-test",
+ Pin: "password",
+ })
+ if err != nil {
+ t.Fatalf("failed to configure softHSM2 on %s: %v", runtime.GOOS, err)
+ }
+
+ k := &PKCS11{
+ p11: p11,
+ }
+
+ // Setup
+ softHSM2Once.Do(func() {
+ teardown(t, k)
+ setup(t, k)
+ })
+
+ return k
+}
diff --git a/kms/pkcs11/yubihsm2_test.go b/kms/pkcs11/yubihsm2_test.go
new file mode 100644
index 00000000..6d02a420
--- /dev/null
+++ b/kms/pkcs11/yubihsm2_test.go
@@ -0,0 +1,54 @@
+// +build cgo,yubihsm2
+
+package pkcs11
+
+import (
+ "runtime"
+ "sync"
+
+ "github.com/ThalesIgnite/crypto11"
+)
+
+var yubiHSM2Once sync.Once
+
+// mustPKCS11 configures a *PKCS11 KMS to be used with YubiHSM2. To initialize
+// these tests, we should run:
+// yubihsm-connector -d
+func mustPKCS11(t TBTesting) *PKCS11 {
+ t.Helper()
+ testModule = "YubiHSM2"
+ if runtime.GOARCH != "amd64" {
+ t.Skipf("yubiHSM2 test skipped on %s:%s", runtime.GOOS, runtime.GOARCH)
+ }
+
+ var path string
+ switch runtime.GOOS {
+ case "darwin":
+ path = "/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib"
+ case "linux":
+ path = "/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so"
+ default:
+ t.Skipf("yubiHSM2 test skipped on %s", runtime.GOOS)
+ return nil
+ }
+ p11, err := crypto11.Configure(&crypto11.Config{
+ Path: path,
+ TokenLabel: "YubiHSM",
+ Pin: "0001password",
+ })
+ if err != nil {
+ t.Fatalf("failed to configure YubiHSM2 on %s: %v", runtime.GOOS, err)
+ }
+
+ k := &PKCS11{
+ p11: p11,
+ }
+
+ // Setup
+ yubiHSM2Once.Do(func() {
+ teardown(t, k)
+ setup(t, k)
+ })
+
+ return k
+}
diff --git a/kms/uri/testdata/pin.txt b/kms/uri/testdata/pin.txt
new file mode 100644
index 00000000..2fca70ea
--- /dev/null
+++ b/kms/uri/testdata/pin.txt
@@ -0,0 +1 @@
+trim-this-pin
diff --git a/kms/uri/uri.go b/kms/uri/uri.go
index 02bec42c..94009c47 100644
--- a/kms/uri/uri.go
+++ b/kms/uri/uri.go
@@ -1,8 +1,12 @@
package uri
import (
+ "bytes"
+ "encoding/hex"
+ "io/ioutil"
"net/url"
"strings"
+ "unicode"
"github.com/pkg/errors"
)
@@ -79,8 +83,54 @@ func ParseWithScheme(scheme, rawuri string) (*URI, error) {
return u, nil
}
-// Get returns the first value in the uri with the give n key, it will return
+// Get returns the first value in the uri with the given key, it will return
// empty string if that field is not present.
func (u *URI) Get(key string) string {
- return u.Values.Get(key)
+ v := u.Values.Get(key)
+ if v == "" {
+ v = u.URL.Query().Get(key)
+ }
+ return v
+}
+
+// GetEncoded returns the first value in the uri with the given key, it will
+// return empty nil if that field is not present or is empty. If the return
+// value is hex encoded it will decode it and return it.
+func (u *URI) GetEncoded(key string) []byte {
+ v := u.Get(key)
+ if v == "" {
+ return nil
+ }
+ if len(v)%2 == 0 {
+ if b, err := hex.DecodeString(v); err == nil {
+ return b
+ }
+ }
+ return []byte(v)
+}
+
+// Pin returns the pin encoded in the url. It will read the pin from the
+// pin-value or the pin-source attributes.
+func (u *URI) Pin() string {
+ if value := u.Get("pin-value"); value != "" {
+ return value
+ }
+ if path := u.Get("pin-source"); path != "" {
+ if b, err := readFile(path); err == nil {
+ return string(bytes.TrimRightFunc(b, unicode.IsSpace))
+ }
+ }
+ return ""
+}
+
+func readFile(path string) ([]byte, error) {
+ u, err := url.Parse(path)
+ if err == nil && (u.Scheme == "" || u.Scheme == "file") && u.Path != "" {
+ path = u.Path
+ }
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading %s", path)
+ }
+ return b, nil
}
diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go
index a2b69b65..aa420db4 100644
--- a/kms/uri/uri_test.go
+++ b/kms/uri/uri_test.go
@@ -99,6 +99,10 @@ func TestParse(t *testing.T) {
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"},
Values: url.Values{"slot-id": []string{"9a"}},
}, false},
+ {"ok schema", args{"cloudkms:"}, &URI{
+ URL: &url.URL{Scheme: "cloudkms"},
+ Values: url.Values{},
+ }, false},
{"ok query", args{"yubikey:slot-id=9a;foo=bar?pin=123456&foo=bar"}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a;foo=bar", RawQuery: "pin=123456&foo=bar"},
Values: url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}},
@@ -115,6 +119,7 @@ func TestParse(t *testing.T) {
URL: &url.URL{Scheme: "file", Host: "tmp", Path: "/ca.cert"},
Values: url.Values{},
}, false},
+ {"fail schema", args{"cloudkms"}, nil, true},
{"fail parse", args{"yubi%key:slot-id=9a"}, nil, true},
{"fail scheme", args{"yubikey"}, nil, true},
{"fail parse opaque", args{"yubikey:slot-id=%ZZ"}, nil, true},
@@ -148,12 +153,17 @@ func TestParseWithScheme(t *testing.T) {
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"},
Values: url.Values{"slot-id": []string{"9a"}},
}, false},
+ {"ok schema", args{"cloudkms", "cloudkms:"}, &URI{
+ URL: &url.URL{Scheme: "cloudkms"},
+ Values: url.Values{},
+ }, false},
{"ok file", args{"file", "file:///tmp/ca.cert"}, &URI{
URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"},
Values: url.Values{},
}, false},
{"fail parse", args{"yubikey", "yubikey"}, nil, true},
{"fail scheme", args{"yubikey", "awskms:slot-id=9a"}, nil, true},
+ {"fail schema", args{"cloudkms", "cloudkms"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -189,8 +199,9 @@ func TestURI_Get(t *testing.T) {
{"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, "9a"},
{"ok first", mustParse("yubikey:slot-id=9a;slot-id=9b"), args{"slot-id"}, "9a"},
{"ok multiple", mustParse("yubikey:slot-id=9a;foo=bar"), args{"foo"}, "bar"},
+ {"ok in query", mustParse("yubikey:slot-id=9a?foo=bar"), args{"foo"}, "bar"},
{"fail missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, ""},
- {"fail in query", mustParse("yubikey:slot-id=9a?foo=bar"), args{"foo"}, ""},
+ {"fail missing query", mustParse("yubikey:slot-id=9a?bar=zar"), args{"foo"}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -200,3 +211,66 @@ func TestURI_Get(t *testing.T) {
})
}
}
+
+func TestURI_GetEncoded(t *testing.T) {
+ mustParse := func(s string) *URI {
+ u, err := Parse(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return u
+ }
+ type args struct {
+ key string
+ }
+ tests := []struct {
+ name string
+ uri *URI
+ args args
+ want []byte
+ }{
+ {"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, []byte{0x9a}},
+ {"ok first", mustParse("yubikey:slot-id=9a9b;slot-id=9b"), args{"slot-id"}, []byte{0x9a, 0x9b}},
+ {"ok percent", mustParse("yubikey:slot-id=9a;foo=%9a%9b%9c"), args{"foo"}, []byte{0x9a, 0x9b, 0x9c}},
+ {"ok in query", mustParse("yubikey:slot-id=9a?foo=9a"), args{"foo"}, []byte{0x9a}},
+ {"ok in query percent", mustParse("yubikey:slot-id=9a?foo=%9a"), args{"foo"}, []byte{0x9a}},
+ {"ok missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, nil},
+ {"ok missing query", mustParse("yubikey:slot-id=9a?bar=zar"), args{"foo"}, nil},
+ {"ok no hex", mustParse("yubikey:slot-id=09a?bar=zar"), args{"slot-id"}, []byte{'0', '9', 'a'}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.uri.GetEncoded(tt.args.key)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("URI.GetEncoded() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestURI_Pin(t *testing.T) {
+ mustParse := func(s string) *URI {
+ u, err := Parse(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return u
+ }
+ tests := []struct {
+ name string
+ uri *URI
+ want string
+ }{
+ {"from value", mustParse("pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
+ {"from source", mustParse("pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
+ {"from missing", mustParse("pkcs11:id=%72%73"), ""},
+ {"from source missing", mustParse("pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.uri.Pin(); got != tt.want {
+ t.Errorf("URI.Pin() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go
index 3349ea63..19cef55e 100644
--- a/kms/yubikey/yubikey.go
+++ b/kms/yubikey/yubikey.go
@@ -13,8 +13,12 @@ import (
"github.com/go-piv/piv-go/piv"
"github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
+ "github.com/smallstep/certificates/kms/uri"
)
+// Scheme is the scheme used in uris.
+const Scheme = "yubikey"
+
// YubiKey implements the KMS interface on a YubiKey.
type YubiKey struct {
yk *piv.YubiKey
@@ -26,6 +30,21 @@ type YubiKey struct {
// TODO(mariano): only one card is currently supported.
func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) {
managementKey := piv.DefaultManagementKey
+
+ if opts.URI != "" {
+ u, err := uri.ParseWithScheme(Scheme, opts.URI)
+ if err != nil {
+ return nil, err
+ }
+ if v := u.Pin(); v != "" {
+ opts.Pin = v
+ }
+ if v := u.Get("management-key"); v != "" {
+ opts.ManagementKey = v
+ }
+ }
+
+ // Deprecated way to set configuration parameters.
if opts.ManagementKey != "" {
b, err := hex.DecodeString(opts.ManagementKey)
if err != nil {
@@ -106,12 +125,12 @@ func (k *YubiKey) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey
return nil, err
}
- cert, err := k.yk.Certificate(slot)
+ pub, err := k.getPublicKey(slot)
if err != nil {
- return nil, errors.Wrap(err, "error retrieving certificate")
+ return nil, err
}
- return cert.PublicKey, nil
+ return pub, nil
}
// CreateKey generates a new key in the YubiKey and returns the public key.
@@ -150,12 +169,12 @@ func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, e
return nil, err
}
- cert, err := k.yk.Certificate(slot)
+ pub, err := k.getPublicKey(slot)
if err != nil {
- return nil, errors.Wrap(err, "error retrieving certificate")
+ return nil, err
}
- priv, err := k.yk.PrivateKey(slot, cert.PublicKey, piv.KeyAuth{
+ priv, err := k.yk.PrivateKey(slot, pub, piv.KeyAuth{
PIN: k.pin,
PINPolicy: piv.PINPolicyAlways,
})
@@ -175,6 +194,20 @@ func (k *YubiKey) Close() error {
return errors.Wrap(k.yk.Close(), "error closing yubikey")
}
+// getPublicKey returns the public key on a slot. First it attempts to do
+// attestation to get a certificate with the public key in it, if this succeeds
+// means that the key was generated in the device. If not we'll try to get the
+// key from a stored certificate in the same slot.
+func (k *YubiKey) getPublicKey(slot piv.Slot) (crypto.PublicKey, error) {
+ cert, err := k.yk.Attest(slot)
+ if err != nil {
+ if cert, err = k.yk.Certificate(slot); err != nil {
+ return nil, errors.Wrap(err, "error retrieving public key")
+ }
+ }
+ return cert.PublicKey, nil
+}
+
// signatureAlgorithmMapping is a mapping between the step signature algorithm,
// and bits for RSA keys, with yubikey ones.
var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{
@@ -228,6 +261,26 @@ var slotMapping = map[string]piv.Slot{
"9c": piv.SlotSignature,
"9e": piv.SlotCardAuthentication,
"9d": piv.SlotKeyManagement,
+ "82": {Key: 0x82, Object: 0x5FC10D},
+ "83": {Key: 0x83, Object: 0x5FC10E},
+ "84": {Key: 0x84, Object: 0x5FC10F},
+ "85": {Key: 0x85, Object: 0x5FC110},
+ "86": {Key: 0x86, Object: 0x5FC111},
+ "87": {Key: 0x87, Object: 0x5FC112},
+ "88": {Key: 0x88, Object: 0x5FC113},
+ "89": {Key: 0x89, Object: 0x5FC114},
+ "8a": {Key: 0x8a, Object: 0x5FC115},
+ "8b": {Key: 0x8b, Object: 0x5FC116},
+ "8c": {Key: 0x8c, Object: 0x5FC117},
+ "8d": {Key: 0x8d, Object: 0x5FC118},
+ "8e": {Key: 0x8e, Object: 0x5FC119},
+ "8f": {Key: 0x8f, Object: 0x5FC11A},
+ "90": {Key: 0x90, Object: 0x5FC11B},
+ "91": {Key: 0x91, Object: 0x5FC11C},
+ "92": {Key: 0x92, Object: 0x5FC11D},
+ "93": {Key: 0x93, Object: 0x5FC11E},
+ "94": {Key: 0x94, Object: 0x5FC11F},
+ "95": {Key: 0x95, Object: 0x5FC120},
}
func getSlot(name string) (piv.Slot, error) {
diff --git a/systemd/README.md b/systemd/README.md
new file mode 100644
index 00000000..97aa18dd
--- /dev/null
+++ b/systemd/README.md
@@ -0,0 +1,5 @@
+### Systemd unit files for `step-ca`
+
+For documentation on `step-ca.service`, see [Running `step-ca` As A Daemon](https://smallstep.com/docs/step-ca/certificate-authority-server-production#running-step-ca-as-a-daemon).
+
+For documentation on `cert-renewer@.*`, see [Automating Certificate Renewal](https://smallstep.com/docs/step-ca/certificate-authority-server-production#automate-x509-certificate-lifecycle-management)
diff --git a/systemd/cert-renewer@.service b/systemd/cert-renewer@.service
new file mode 100644
index 00000000..29e5ec8a
--- /dev/null
+++ b/systemd/cert-renewer@.service
@@ -0,0 +1,31 @@
+[Unit]
+Description=Certificate renewer for %I
+After=network-online.target
+Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
+StartLimitIntervalSec=0
+
+[Service]
+Type=oneshot
+User=root
+
+Environment=STEPPATH=/etc/step-ca \
+ CERT_LOCATION=/etc/step/certs/%i.crt \
+ KEY_LOCATION=/etc/step/certs/%i.key
+
+; ExecStartPre checks if the certificate is ready for renewal,
+; based on the exit status of the command.
+; (In systemd 243 and above, you can use ExecCondition= here.)
+ExecStartPre=/usr/bin/bash -c \
+ 'step certificate inspect $CERT_LOCATION --format json --roots "$STEPPATH/certs/root_ca.crt" | \
+ jq -e "(((.validity.start | fromdate) + \
+ ((.validity.end | fromdate) - (.validity.start | fromdate)) * 0.66) \
+ - now) <= 0" > /dev/null'
+
+; ExecStart renews the certificate, if ExecStartPre was successful.
+ExecStart=/usr/bin/step ca renew --force $CERT_LOCATION $KEY_LOCATION
+
+; Try to reload or restart the systemd service that relies on this cert-renewer
+ExecStartPost=/usr/bin/bash -c 'systemctl --quiet is-enabled %i && systemctl try-reload-or-restart %i'
+
+[Install]
+WantedBy=multi-user.target
diff --git a/systemd/cert-renewer@.timer b/systemd/cert-renewer@.timer
new file mode 100644
index 00000000..806f3407
--- /dev/null
+++ b/systemd/cert-renewer@.timer
@@ -0,0 +1,18 @@
+[Unit]
+Description=Certificate renewal timer for %I
+Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
+
+[Timer]
+Persistent=true
+
+; Run the timer unit every 5 minutes.
+OnCalendar=*:1/5
+
+; Always run the timer on time.
+AccuracySec=1us
+
+; Add jitter to prevent a "thundering hurd" of simultaneous certificate renewals.
+RandomizedDelaySec=5m
+
+[Install]
+WantedBy=timers.target
diff --git a/systemd/step-ca.service b/systemd/step-ca.service
new file mode 100644
index 00000000..11fdffae
--- /dev/null
+++ b/systemd/step-ca.service
@@ -0,0 +1,56 @@
+[Unit]
+Description=step-ca service
+Documentation=https://smallstep.com/docs/step-ca
+Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
+After=network-online.target
+Wants=network-online.target
+StartLimitIntervalSec=30
+StartLimitBurst=3
+ConditionFileNotEmpty=/etc/step-ca/config/ca.json
+ConditionFileNotEmpty=/etc/step-ca/password.txt
+
+[Service]
+Type=simple
+User=step
+Group=step
+Environment=STEPPATH=/etc/step-ca
+WorkingDirectory=/etc/step-ca
+ExecStart=/usr/local/bin/step-ca config/ca.json --password-file password.txt
+ExecReload=/bin/kill --signal HUP $MAINPID
+Restart=on-failure
+RestartSec=5
+TimeoutStopSec=30
+StartLimitInterval=30
+StartLimitBurst=3
+
+; Process capabilities & privileges
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+SecureBits=keep-caps
+NoNewPrivileges=yes
+
+; Sandboxing
+; This sandboxing works with YubiKey PIV (via pcscd HTTP API), but it is likely
+; too restrictive for PKCS#11 HSMs.
+ProtectSystem=full
+ProtectHome=true
+RestrictNamespaces=true
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+PrivateTmp=true
+ProtectClock=true
+ProtectControlGroups=true
+ProtectKernelTunables=true
+ProtectKernelLogs=true
+ProtectKernelModules=true
+LockPersonality=true
+RestrictSUIDSGID=true
+RemoveIPC=true
+RestrictRealtime=true
+PrivateDevices=true
+SystemCallFilter=@system-service
+SystemCallArchitectures=native
+MemoryDenyWriteExecute=true
+ReadWriteDirectories=/etc/step-ca/db
+
+[Install]
+WantedBy=multi-user.target