Merge pull request #457 from smallstep/pkcs11

Add support for PKCS #11 KMS.
pull/474/head
Mariano Cano 3 years ago committed by GitHub
commit ddd6bc16d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
@ -113,7 +119,7 @@ generate:
# Test
#########################################
test:
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
$Q go test -short -coverprofile=coverage.out ./...
.PHONY: test
@ -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

@ -382,3 +382,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)
}
}

@ -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()
})
}
}

@ -227,9 +227,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

@ -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.

@ -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

@ -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[:]
}

@ -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

@ -51,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=
@ -221,6 +223,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=
@ -272,9 +276,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=

@ -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)
}

@ -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)
}
})
}
}

@ -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

@ -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)
}

@ -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
}

@ -0,0 +1,174 @@
// +build !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)
}

@ -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
}

@ -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
}

@ -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)
}
})
}
}

@ -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
}

@ -0,0 +1,59 @@
// +build 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
}

@ -0,0 +1,54 @@
// +build 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
}

@ -0,0 +1 @@
trim-this-pin

@ -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
}

@ -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)
}
})
}
}

Loading…
Cancel
Save