From 8dca652bc72cbca1b9203ea49ad97db5e99b6161 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 Jan 2021 20:03:53 -0800 Subject: [PATCH] Add support for PKCS #11 KMS. The implementation works with YubiHSM2. Unit tests are still pending. Fixes #301 --- Makefile | 11 +- cmd/step-ca/main.go | 1 + cmd/step-pkcs11-init/main.go | 430 +++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 7 + kms/apiv1/options.go | 20 +- kms/apiv1/options_test.go | 2 +- kms/apiv1/requests.go | 11 +- kms/uri/uri.go | 130 ++++++++++- kms/uri/uri_test.go | 3 +- 10 files changed, 603 insertions(+), 14 deletions(-) create mode 100644 cmd/step-pkcs11-init/main.go diff --git a/Makefile b/Makefile index 9fb552d1..6f37e121 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,,@) @@ -76,7 +78,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 +97,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 @@ -171,6 +177,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/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-pkcs11-init/main.go b/cmd/step-pkcs11-init/main.go new file mode 100644 index 00000000..174eed78 --- /dev/null +++ b/cmd/step-pkcs11-init/main.go @@ -0,0 +1,430 @@ +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 + EnableSSH bool + Force bool +} + +// Validate checks the flags in the config. +func (c *Config) Validate() error { + switch { + case c.KMS == "": + return errors.New("one of 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" + } + default: + + } + + 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.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) + } + + // 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 != "" { + checkObject(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 != "" { + if err := deleter.DeleteCertificate(u); err != nil { + fatal(err) + } + } + } + for _, u := range keyUris { + if u != "" { + if err := deleter.DeleteKey(u); err != nil { + fatal(err) + } + } + } + } + } + + if err := createPKI(k, c); err != nil { + fatal(err) + } + + defer func() { + _ = k.Close() + }() +} + +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 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 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`.") + 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 { + 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 { + 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/go.mod b/go.mod index 74873ea8..33782eb9 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.3 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 @@ -33,3 +34,4 @@ require ( // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto // replace github.com/smallstep/nosql => ../nosql +replace github.com/ThalesIgnite/crypto11 => github.com/maraino/crypto11 v1.2.4-0.20210127032225-7ed5319b45a1 diff --git a/go.sum b/go.sum index f4a18362..ae64c5f4 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,8 @@ github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/maraino/crypto11 v1.2.4-0.20210127032225-7ed5319b45a1 h1:aj2ASiF6u9p576GdvGsRW5SiwiE/Hp5BeU2c/ldlYTI= +github.com/maraino/crypto11 v1.2.4-0.20210127032225-7ed5319b45a1/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= @@ -222,6 +224,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +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= @@ -273,9 +277,12 @@ 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= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index b7845109..07d7df0d 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -64,10 +64,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"` - - // Pin used to access the PKCS11 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. 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 +98,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..5a8307eb 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 { 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/uri/uri.go b/kms/uri/uri.go index 02bec42c..85d512db 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" ) @@ -82,5 +86,129 @@ func ParseWithScheme(scheme, rawuri string) (*URI, error) { // Get returns the first value in the uri with the give n 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 StringDecode(v) +} + +// GetHex returns the first value in the uri with the give n key, it will return +// empty nil if that field is not present. +func (u *URI) GetHex(key string) ([]byte, error) { + v := u.Values.Get(key) + if v == "" { + v = u.URL.Query().Get(key) + } + return HexDecode(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 StringDecode(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 +} + +// PercentEncode encodes the given bytes using the percent encoding described in +// RFC3986 (https://tools.ietf.org/html/rfc3986). +func PercentEncode(b []byte) string { + buf := new(strings.Builder) + for _, v := range b { + buf.WriteString("%" + hex.EncodeToString([]byte{v})) + } + return buf.String() +} + +// PercentDecode decodes the given string using the percent encoding described +// in RFC3986 (https://tools.ietf.org/html/rfc3986). +func PercentDecode(s string) ([]byte, error) { + if len(s)%3 != 0 { + return nil, errors.Errorf("error parsing %s: wrong length", s) + } + + var first string + buf := new(bytes.Buffer) + for i, r := range s { + mod := i % 3 + rr := string(r) + switch mod { + case 0: + if r != '%' { + return nil, errors.Errorf("error parsing %s: expected %% and found %s in position %d", s, rr, i) + } + case 1: + if !isHex(r) { + return nil, errors.Errorf("error parsing %s: %s in position %d is not an hexadecimal number", s, rr, i) + } + first = string(r) + case 2: + if !isHex(r) { + return nil, errors.Errorf("error parsing %s: %s in position %d is not an hexadecimal number", s, rr, i) + } + b, err := hex.DecodeString(first + rr) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %s", s) + } + buf.Write(b) + } + } + return buf.Bytes(), nil +} + +// StringDecode returns the string given, but it will use Percent-Encoding if +// the string is percent encoded. +func StringDecode(s string) string { + if strings.HasPrefix(s, "%") { + if b, err := PercentDecode(s); err == nil { + return string(b) + } + } + return s +} + +// HexDecode deocdes the string s using Percent-Encoding or regular hex +// encoding. +func HexDecode(s string) ([]byte, error) { + if strings.HasPrefix(s, "%") { + return PercentDecode(s) + } + + b, err := hex.DecodeString(s) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %s", s) + } + return b, nil +} + +func isHex(r rune) bool { + switch { + case r >= '0' && r <= '9': + return true + case r >= 'a' && r <= 'f': + return true + case r >= 'A' && r <= 'F': + return true + default: + return false + } } diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go index a2b69b65..3f001afc 100644 --- a/kms/uri/uri_test.go +++ b/kms/uri/uri_test.go @@ -189,8 +189,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) {