diff --git a/.github/workflows/test-pki.yml b/.github/workflows/test-pki.yml new file mode 100644 index 00000000..b0df853b --- /dev/null +++ b/.github/workflows/test-pki.yml @@ -0,0 +1,19 @@ +name: Test PKI +on: + pull_request: + +jobs: + test-pki: + name: test-pki + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4.1.0 + with: + go-version: '~1.19' + - name: Clone the code + uses: actions/checkout@v4 + - name: Setup test deps + run: sudo make install-test-pki-deps + - name: Run tests + run: make test-pki diff --git a/Makefile b/Makefile index cd749baf..fa65b18a 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,10 @@ build: fioctl-linux-amd64 fioctl-linux-arm64 fioctl-windows-amd64 fioctl-darwin- fioctl-static: CGO_ENABLED=0 go build -a -ldflags '-w -extldflags "-static"' -o ./bin/fioctl-static ./main.go +# Allows building a dyn-linked fioctl on platforms without pkcs11-tool (not built by default) +fioctl-cgo-pkcs11: + CGO_ENABLED=1 go build -tags cgopki $(LDFLAGS) -o bin/$@ ./main.go + fioctl-linux-amd64: fioctl-linux-arm64: fioctl-linux-armv7: @@ -33,6 +37,9 @@ has-linter: linter-check: has-linter $(linter) run ${EXTRA_LINTER_FLAGS} + $(linter) run --build-tags bashpki ${EXTRA_LINTER_FLAGS} + $(linter) run --build-tags cgopki ${EXTRA_LINTER_FLAGS} + $(linter) run --build-tags testhsm ${EXTRA_LINTER_FLAGS} linter: has-linter $(linter) run --fix ${EXTRA_LINTER_FLAGS} @@ -46,6 +53,15 @@ format: check: format-check linter-check @true +install-test-pki-deps: + apt install openssl softhsm2 opensc libengine-pkcs11-openssl + +# This needs the following packages on Ubuntu: openssl softhsm2 opensc libengine-pkcs11-openssl +test-pki: + go test ./x509/... -v -tags testhsm + go test ./x509/... -v -tags testhsm,bashpki + go test ./x509/... -v -tags testhsm,cgopki + # Use the image for Dockerfile.build to build and install the tool. container-init: docker build -t fioctl-build -f Dockerfile.build . diff --git a/README.md b/README.md index a8affd9f..48df9ff8 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,9 @@ export PATH=$PATH:`pwd`/bin After making changes be sure to run `make format` which will run the go-fmt tool against the source code. + +## HSM support + +The HSM support (for some commands) is provided via the OpenSC pkcs11-tool application, +which needs to be installed separately. +It is not needed if you do not plan to use HSM devices. diff --git a/client/foundries_pki.go b/client/foundries_pki.go index 453a5847..ca6c67ac 100644 --- a/client/foundries_pki.go +++ b/client/foundries_pki.go @@ -19,11 +19,6 @@ type CaCerts struct { TlsCsr string `json:"tls-csr"` ChangeMeta ChangeMeta `json:"change-meta"` - - CreateCaScript *string `json:"create_ca"` - CreateDeviceCaScript *string `json:"create_device_ca"` - SignCaScript *string `json:"sign_ca_csr"` - SignTlsScript *string `json:"sign_tls_csr"` } func (a *Api) FactoryGetCA(factory string) (CaCerts, error) { diff --git a/go.mod b/go.mod index 0e60ceed..730c5d2a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( cloud.google.com/go/pubsub v1.33.0 + github.com/ThalesIgnite/crypto11 v1.2.5 github.com/cheynewallace/tabby v1.1.1 github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c github.com/fatih/color v1.15.0 @@ -16,6 +17,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 github.com/theupdateframework/go-tuf v0.6.1 github.com/theupdateframework/notary v0.7.0 golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb @@ -30,6 +32,7 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -42,8 +45,11 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/spf13/afero v1.9.5 // indirect @@ -51,6 +57,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect + github.com/thales-e-security/pool v0.0.2 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/net v0.10.0 // indirect diff --git a/go.sum b/go.sum index d3cc24c6..270d8ad8 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= +github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= @@ -228,6 +230,8 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +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/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -246,6 +250,7 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -300,8 +305,11 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +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/theupdateframework/go-tuf v0.6.1 h1:6J89fGjQf7s0mLmTG7p7pO/MbKOg+bIXhaLyQdmbKuE= github.com/theupdateframework/go-tuf v0.6.1/go.mod h1:LAFusuQsFNBnEyYoTuA5zZrF7iaQ4TEgBXm8lb6Vj18= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= diff --git a/subcommands/keys/ca_create.go b/subcommands/keys/ca_create.go index 06a4183a..c5e181f1 100644 --- a/subcommands/keys/ca_create.go +++ b/subcommands/keys/ca_create.go @@ -1,6 +1,7 @@ package keys import ( + "errors" "fmt" "os" @@ -60,14 +61,7 @@ This is optional.`, cmd.Flags().BoolVarP(&createLocalCA, "local-ca", "", true, "Create a local CA that you can use for signing your own device certificates") cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Create key on an PKCS#11 compatible HSM using this module") cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to set up on the HSM, if using one") - cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "device-gateway-root", "The label of the HSM token created for this") -} - -func writeFile(filename, contents string, mode os.FileMode) { - if err := os.WriteFile(filename, []byte(contents), mode); err != nil { - fmt.Printf("ERROR: Creating %s: %s", filename, err) - os.Exit(1) - } + cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token created for this") } func getDeviceCaCommonName(factory string) string { @@ -84,35 +78,30 @@ func doCreateCA(cmd *cobra.Command, args []string) { if len(hsmModule) > 0 { if len(hsmPin) == 0 { - fmt.Println("ERROR: --hsm-pin is required with --hsm-module") - os.Exit(1) + subcommands.DieNotNil(errors.New("--hsm-pin is required with --hsm-module")) } - os.Setenv("HSM_MODULE", hsmModule) - os.Setenv("HSM_PIN", hsmPin) - os.Setenv("HSM_TOKEN_LABEL", hsmTokenLabel) + if len(hsmTokenLabel) == 0 { + subcommands.DieNotNil(errors.New("--hsm-token-label is required with --hsm-module")) + } + x509.InitHsm(x509.HsmInfo{ + Module: hsmModule, + Pin: hsmPin, + TokenLabel: hsmTokenLabel, + }) } resp, err := api.FactoryCreateCA(factory) subcommands.DieNotNil(err) - writeFile(x509.OnlineCaCsrFile, resp.CaCsr, 0400) - writeFile(x509.TlsCsrFile, resp.TlsCsr, 0400) - writeFile(x509.CreateCaScript, *resp.CreateCaScript, 0700) - writeFile(x509.CreateDeviceCaScript, *resp.CreateDeviceCaScript, 0700) - writeFile(x509.SignCaScript, *resp.SignCaScript, 0700) - writeFile(x509.SignTlsScript, *resp.SignTlsScript, 0700) - fmt.Println("Creating offline root CA for Factory") resp.RootCrt = x509.CreateFactoryCa(factory) fmt.Println("Signing Foundries TLS CSR") resp.TlsCrt = x509.SignTlsCsr(resp.TlsCsr) - writeFile(x509.TlsCertFile, resp.TlsCrt, 0400) if createOnlineCA { fmt.Println("Signing Foundries CSR for online use") resp.CaCrt = x509.SignCaCsr(resp.CaCsr) - writeFile(x509.OnlineCaCertFile, resp.CaCrt, 0400) } if createLocalCA { diff --git a/x509/bash.go b/x509/bash.go index 64acec55..2a4f0a8e 100644 --- a/x509/bash.go +++ b/x509/bash.go @@ -1,4 +1,7 @@ -//go:build !windows +//go:build bashpki + +// A reference implementation for those who want to customize their PKI. +// This is turned off in vanilla Fioctl builds, and can be enabled in a fork. package x509 @@ -9,38 +12,221 @@ import ( "github.com/foundriesio/fioctl/subcommands" ) -func run(name string, arg ...string) string { - cmd := exec.Command(name, arg...) +type KeyStorage interface { + configure() +} + +func (s *fileStorage) configure() {} + +func (s *hsmStorage) configure() { + os.Setenv("HSM_MODULE", s.Module) + os.Setenv("HSM_PIN", s.Pin) + os.Setenv("HSM_TOKEN_LABEL", s.TokenLabel) +} + +func run(script string, arg ...string) string { + factoryCaKeyStorage.configure() + arg = append([]string{"-s"}, arg...) + cmd := exec.Command("/bin/sh", arg...) cmd.Stderr = os.Stderr + in, err := cmd.StdinPipe() + subcommands.DieNotNil(err, "Failed to start the shell") + + go func() { + defer in.Close() + _, err := in.Write([]byte(script)) + subcommands.DieNotNil(err, "Failed to pass the script to the shell") + }() + out, err := cmd.Output() - subcommands.DieNotNil(err) + subcommands.DieNotNil(err, "Failed to execute the shell script") return string(out) } func CreateFactoryCa(ou string) string { - run("./" + CreateCaScript) + const script = `#!/bin/sh -e +## This script creates the offline private key, factory_ca.key, and x509 certficate, factory_ca.pem, +## owned by the customer that provides a chain of trust for all other certficates used by this factory. + +if [ $# -ne 1 ] ; then + echo "ERROR: $0 " + exit 1 +fi + +ou=$1 + +cat >ca.cnf < " + exit 1 +fi +key=$1 +cn=$2 +ou=$3 + +cat >ca.cnf < " + exit 1 +fi +csr=$1 +crt=$2 + +dns=$(openssl req -text -noout -verify -in $csr | grep DNS:) +echo "signing with dns name: $dns" 1>&2 + +cat >server.ext < " + exit 1 +fi +csr=$1 +crt=$2 + +cat >ca.ext < 0 { + subcommands.DieNotNil(errors.New("Malformed PEM data")) + } + return first +} + func parsePemCertificateRequest(csrPem string) *x509.CertificateRequest { - pemBlock, _ := pem.Decode([]byte(csrPem)) + pemBlock := parseOnePemBlock(csrPem) clientCSR, err := x509.ParseCertificateRequest(pemBlock.Bytes) subcommands.DieNotNil(err) err = clientCSR.CheckSignature() @@ -67,20 +61,6 @@ func parsePemCertificateRequest(csrPem string) *x509.CertificateRequest { return clientCSR } -func parsePemPrivateKey(keyPem string) *ecdsa.PrivateKey { - caPrivateKeyPemBlock, _ := pem.Decode([]byte(keyPem)) - caPrivateKey, err := x509.ParseECPrivateKey(caPrivateKeyPemBlock.Bytes) - subcommands.DieNotNil(err) - return caPrivateKey -} - -func parsePemCertificate(crtPem string) *x509.Certificate { - caCrtPemBlock, _ := pem.Decode([]byte(crtPem)) - crt, err := x509.ParseCertificate(caCrtPemBlock.Bytes) - subcommands.DieNotNil(err) - return crt -} - func marshalSubject(cn string, ou string) pkix.Name { // In it's simpler form, this function would be replaced by // pkix.Name{CommonName: cn, OrganizationalUnit: []string{ou}} @@ -113,10 +93,10 @@ func marshalSubject(cn string, ou string) pkix.Name { } func CreateFactoryCa(ou string) string { - priv := genAndSaveKey(FactoryCaKeyFile) + priv := factoryCaKeyStorage.genAndSaveKey() crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), - Subject: marshalSubject("Factory-CA", ou), + Subject: marshalSubject(factoryCaName, ou), NotBefore: time.Now(), NotAfter: time.Now().AddDate(20, 0, 0), @@ -125,15 +105,15 @@ func CreateFactoryCa(ou string) string { KeyUsage: x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } - factoryCaString := genCertificate(&crtTemplate, &crtTemplate, &priv.PublicKey, priv) - writeFile(FactoryCaCertFile, factoryCaString, 0400) + factoryCaString := genCertificate(&crtTemplate, &crtTemplate, priv.Public(), priv) + writeFile(FactoryCaCertFile, factoryCaString) return factoryCaString } func CreateDeviceCa(cn string, ou string) string { - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) - factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) - priv := genAndSaveKey(DeviceCaKeyFile) + factoryKey := factoryCaKeyStorage.loadKey() + factoryCa := loadCertFromFile(FactoryCaCertFile) + priv := genAndSaveKeyToFile(DeviceCaKeyFile) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), Subject: marshalSubject(cn, ou), @@ -146,15 +126,15 @@ func CreateDeviceCa(cn string, ou string) string { MaxPathLenZero: true, KeyUsage: x509.KeyUsageCertSign, } - crtPem := genCertificate(&crtTemplate, factoryCa, &priv.PublicKey, factoryKey) - writeFile(DeviceCaCertFile, crtPem, 0400) + crtPem := genCertificate(&crtTemplate, factoryCa, priv.Public(), factoryKey) + writeFile(DeviceCaCertFile, crtPem) return crtPem } func SignTlsCsr(csrPem string) string { csr := parsePemCertificateRequest(csrPem) - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) - factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) + factoryKey := factoryCaKeyStorage.loadKey() + factoryCa := loadCertFromFile(FactoryCaCertFile) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), Subject: csr.Subject, @@ -162,19 +142,20 @@ func SignTlsCsr(csrPem string) string { NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, + IsCA: false, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: csr.DNSNames, } crtPem := genCertificate(&crtTemplate, factoryCa, csr.PublicKey, factoryKey) + writeFile(TlsCertFile, crtPem) return crtPem } func SignCaCsr(csrPem string) string { csr := parsePemCertificateRequest(csrPem) - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) - factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) + factoryKey := factoryCaKeyStorage.loadKey() + factoryCa := loadCertFromFile(FactoryCaCertFile) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), Subject: csr.Subject, @@ -188,6 +169,7 @@ func SignCaCsr(csrPem string) string { KeyUsage: x509.KeyUsageCertSign, } crtPem := genCertificate(&crtTemplate, factoryCa, csr.PublicKey, factoryKey) + writeFile(OnlineCaCertFile, crtPem) return crtPem } diff --git a/x509/hsm_test.go b/x509/hsm_test.go new file mode 100644 index 00000000..bb603203 --- /dev/null +++ b/x509/hsm_test.go @@ -0,0 +1,99 @@ +//go:build testhsm + +package x509 + +import ( + "crypto/x509" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testHsmModule = "/usr/lib/softhsm/libsofthsm2.so" + testHsmPin = "1234" + testHsmTokenLabel = "fioctl-test" +) + +func TestHsm(t *testing.T) { + softHsmTokenDir, err := os.MkdirTemp("", "softhsm-tokens-*") + require.Nil(t, err) + defer os.RemoveAll(softHsmTokenDir) + + softHsmConfig, err := os.CreateTemp("", "softhsm-config.cfg") + require.Nil(t, err) + defer os.Remove(softHsmConfig.Name()) + func() { + defer softHsmConfig.Close() + _, err := softHsmConfig.Write([]byte("directories.tokendir = " + softHsmTokenDir)) + require.Nil(t, err) + }() + os.Setenv("SOFTHSM2_CONF", softHsmConfig.Name()) + + cmd := exec.Command( + "softhsm2-util", "--init-token", "--slot", "0", + "--label", testHsmTokenLabel, "--so-pin", testHsmPin, "--pin", testHsmPin) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + require.Nil(t, cmd.Run()) + + InitHsm(HsmInfo{Module: testHsmModule, Pin: testHsmPin, TokenLabel: testHsmTokenLabel}) + + runTest(t, func(factoryCa, tlsCert, onlineCa, offlineCa *x509.Certificate) { + for _, fn := range []string{ + FactoryCaCertFile, + TlsCertFile, + OnlineCaCertFile, + DeviceCaCertFile, + DeviceCaKeyFile, + } { + stat, err := os.Lstat(fn) + require.Nil(t, err) + assert.Equal(t, fn, stat.Name()) + assert.Equal(t, false, stat.IsDir()) + assert.Equal(t, os.FileMode(0400), stat.Mode()) + } + for _, fn := range []string{ + FactoryCaKeyFile, + } { + _, err := os.Lstat(fn) + require.NotNil(t, err) + require.Equal(t, true, os.IsNotExist(err)) + } + + factoryCaOnDisk, err := x509.ParseCertificate(pemToDer(t, readFile(FactoryCaCertFile))) + require.Nil(t, err) + factoryCaPubeyOnHsm, err := x509.ParsePKIXPublicKey(readPubkeyFromHsm(t, FactoryCaKeyLabel)) + require.Nil(t, err) + assert.Equal(t, factoryCa, factoryCaOnDisk) + assert.Equal(t, factoryCa.PublicKey, factoryCaPubeyOnHsm) + + tlsCertOnDisk, err := x509.ParseCertificate(pemToDer(t, readFile(TlsCertFile))) + require.Nil(t, err) + assert.Equal(t, tlsCert, tlsCertOnDisk) + + onlineCaOnDisk, err := x509.ParseCertificate(pemToDer(t, readFile(OnlineCaCertFile))) + require.Nil(t, err) + assert.Equal(t, onlineCa, onlineCaOnDisk) + + offlineCaOnDisk, err := x509.ParseCertificate(pemToDer(t, readFile(DeviceCaCertFile))) + require.Nil(t, err) + offlineCaKeyOnDisk, err := x509.ParseECPrivateKey(pemToDer(t, readFile(DeviceCaKeyFile))) + require.Nil(t, err) + assert.Equal(t, offlineCa, offlineCaOnDisk) + assert.Equal(t, offlineCa.PublicKey, offlineCaKeyOnDisk.Public()) + }) +} + +func readPubkeyFromHsm(t *testing.T, label string) []byte { + cmd := exec.Command( + "pkcs11-tool", "--module", testHsmModule, "--read-object", "--type", "pubkey", + "--token-label", testHsmTokenLabel, "--pin", testHsmPin, "--label", label) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + require.Nil(t, err) + return out +} diff --git a/x509/storage_filesystem.go b/x509/storage_filesystem.go new file mode 100644 index 00000000..af6db27b --- /dev/null +++ b/x509/storage_filesystem.go @@ -0,0 +1,49 @@ +//go:build !bashpki + +package x509 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + + "github.com/foundriesio/fioctl/subcommands" +) + +func genAndSaveKeyToFile(fn string) crypto.Signer { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + subcommands.DieNotNil(err) + + keyRaw, err := x509.MarshalECPrivateKey(priv) + subcommands.DieNotNil(err) + + keyBlock := &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyRaw} + keyBytes := pem.EncodeToMemory(keyBlock) + writeFile(fn, string(keyBytes)) + return priv +} + +func loadKeyFromFile(fn string) crypto.Signer { + keyPem := parseOnePemBlock(readFile(fn)) + key, err := x509.ParseECPrivateKey(keyPem.Bytes) + subcommands.DieNotNil(err) + return key +} + +func loadCertFromFile(fn string) *x509.Certificate { + crtPem := parseOnePemBlock(readFile(fn)) + crt, err := x509.ParseCertificate(crtPem.Bytes) + subcommands.DieNotNil(err) + return crt +} + +func (s *fileStorage) genAndSaveKey() crypto.Signer { + return genAndSaveKeyToFile(s.Filename) +} + +func (s *fileStorage) loadKey() crypto.Signer { + return loadKeyFromFile(s.Filename) +} diff --git a/x509/storage_pkcs11_cgo.go b/x509/storage_pkcs11_cgo.go new file mode 100644 index 00000000..c17106cf --- /dev/null +++ b/x509/storage_pkcs11_cgo.go @@ -0,0 +1,60 @@ +//go:build !bashpki && cgopki + +package x509 + +import ( + "crypto" + "crypto/elliptic" + "fmt" + + "github.com/ThalesIgnite/crypto11" + + "github.com/foundriesio/fioctl/subcommands" +) + +const hsmObjectId = "1" + +func newPkcs11Session(hsm HsmInfo) *crypto11.Context { + cfg := crypto11.Config{ + Path: hsm.Module, + TokenLabel: hsm.TokenLabel, + Pin: hsm.Pin, + MaxSessions: 0, + } + + ctx, err := crypto11.Configure(&cfg) + subcommands.DieNotNil(err) + return ctx +} + +func genAndSaveKeyToHsm(hsm HsmInfo, id, label string) crypto.Signer { + // See storage_pkcs11_tool.go why we need to first check for the key existence. + ctx := newPkcs11Session(hsm) + key, err := ctx.FindKeyPair([]byte(id), []byte(label)) + subcommands.DieNotNil(err) + if key != nil { + subcommands.DieNotNil(fmt.Errorf("Key %s already exists on the HSM device", label)) + } + + key, err = ctx.GenerateECDSAKeyPairWithLabel([]byte(id), []byte(label), elliptic.P256()) + subcommands.DieNotNil(err) + return key +} + +func loadKeyFromHsm(hsm HsmInfo, id, label string) crypto.Signer { + ctx := newPkcs11Session(hsm) + key, err := ctx.FindKeyPair([]byte(id), []byte(label)) + subcommands.DieNotNil(err) + if key == nil { + subcommands.DieNotNil(fmt.Errorf("Key %s not found on the HSM device", label)) + } + return key +} + +func (s *hsmStorage) genAndSaveKey() crypto.Signer { + return genAndSaveKeyToHsm(s.HsmInfo, hsmObjectId, s.Label) +} + +func (s *hsmStorage) loadKey() crypto.Signer { + return loadKeyFromHsm(s.HsmInfo, hsmObjectId, s.Label) +} diff --git a/x509/storage_pkcs11_tool.go b/x509/storage_pkcs11_tool.go new file mode 100644 index 00000000..0f7187d2 --- /dev/null +++ b/x509/storage_pkcs11_tool.go @@ -0,0 +1,110 @@ +//go:build !bashpki && !cgopki + +package x509 + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/foundriesio/fioctl/subcommands" +) + +const hsmObjectId = "1" + +type hsmSigner struct { + hsm HsmInfo + id string + label string + pub crypto.PublicKey +} + +func (s *hsmSigner) keyArgs() []string { + return []string{ + "--module", + s.hsm.Module, + "--token-label", + s.hsm.TokenLabel, + "--pin", + s.hsm.Pin, + "--id", + s.id, + "--label", + s.label, + } +} + +func (s *hsmSigner) Public() crypto.PublicKey { + if s.pub != nil { + return s.pub + } + args := append(s.keyArgs(), "--read-object", "--type=pubkey") + cmd := exec.Command("pkcs11-tool", args...) + out, err := cmd.Output() + var ex *exec.ExitError + if errors.As(err, &ex) { + if strings.HasPrefix(string(ex.Stderr), "error: object not found") { + return nil + } + fmt.Println(string(ex.Stderr)) + } + subcommands.DieNotNil(err) + key, err := x509.ParsePKIXPublicKey(out) + subcommands.DieNotNil(err) + s.pub = key + return key +} + +func (s *hsmSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // By default pkcs11-tool returns raw signature, X509 needs it wrapped into the ASN.1 sequence + args := append(s.keyArgs(), "--sign", "--mechanism=ECDSA", "--signature-format=sequence") + cmd := exec.Command("pkcs11-tool", args...) + cmd.Stderr = os.Stderr + in, err := cmd.StdinPipe() + subcommands.DieNotNil(err) + + go func() { + defer in.Close() + _, err := in.Write(digest) + subcommands.DieNotNil(err) + }() + + return cmd.Output() +} + +func genAndSaveKeyToHsm(hsm HsmInfo, id, label string) crypto.Signer { + // The pkcs11-tool allows creating many objects with the same ID and label, potentially corrupting the storage. + // For now, allow to create only one object. + // In the future we will use the ID field to allow key rotation. + key := &hsmSigner{hsm, id, label, nil} + if key.Public() != nil { + subcommands.DieNotNil(fmt.Errorf("Key %s already exists on the HSM device", label)) + } + + args := append(key.keyArgs(), "--keypairgen", "--key-type=EC:prime256v1") + cmd := exec.Command("pkcs11-tool", args...) + cmd.Stderr = os.Stderr + subcommands.DieNotNil(cmd.Run()) + return key +} + +func loadKeyFromHsm(hsm HsmInfo, id, label string) crypto.Signer { + key := &hsmSigner{hsm, id, label, nil} + if key.Public() == nil { + subcommands.DieNotNil(fmt.Errorf("Key %s not found on the HSM device", label)) + } + return key +} + +func (s *hsmStorage) genAndSaveKey() crypto.Signer { + return genAndSaveKeyToHsm(s.HsmInfo, hsmObjectId, s.Label) +} + +func (s *hsmStorage) loadKey() crypto.Signer { + return loadKeyFromHsm(s.HsmInfo, hsmObjectId, s.Label) +}