From 8e3b8d970f18fd7cee17f731d00e196a0c611737 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Wed, 27 Sep 2023 22:07:59 +0300 Subject: [PATCH] PoC: pkcs11-tool based HSM support An idea is to continue building Fioctl statically, and provide HSM support by wrapping pkcs11-tool. The tool is available for Linux, Windows, and Darwin; a user can also build from source. Signed-off-by: Volodymyr Khoroz --- subcommands/el2g/gateway.go | 2 +- subcommands/keys/ca_create.go | 15 ++++--- x509/bash.go | 37 ++++++++++----- x509/common.go | 12 +++++ x509/golang.go | 84 ++++++++++++++++++----------------- x509/hsm_test.go | 14 ++++++ x509/storage_fs.go | 38 ++++++++++++++++ x509/storage_hsm.go | 74 ++++++++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 61 deletions(-) create mode 100644 x509/hsm_test.go create mode 100644 x509/storage_fs.go create mode 100644 x509/storage_hsm.go diff --git a/subcommands/el2g/gateway.go b/subcommands/el2g/gateway.go index 77446887..e8dac69a 100644 --- a/subcommands/el2g/gateway.go +++ b/subcommands/el2g/gateway.go @@ -46,7 +46,7 @@ func doDeviceGateway(cmd *cobra.Command, args []string) { subcommands.DieNotNil(err) fmt.Println("Signing CSR") - generatedCa := x509.SignEl2GoCsr(csr.Value) + generatedCa := x509.SignEl2GoCsr(&x509.KeyStorageFiles{}, csr.Value) fmt.Println("Uploading signed certificate") errPrefix := "Unable to upload certificate:\n" + generatedCa diff --git a/subcommands/keys/ca_create.go b/subcommands/keys/ca_create.go index 06a4183a..62b8c059 100644 --- a/subcommands/keys/ca_create.go +++ b/subcommands/keys/ca_create.go @@ -82,14 +82,15 @@ func doCreateCA(cmd *cobra.Command, args []string) { logrus.Debugf("Create CA for %s under %s", factory, certsDir) subcommands.DieNotNil(os.Chdir(certsDir)) + var storage x509.KeyStorage if len(hsmModule) > 0 { if len(hsmPin) == 0 { fmt.Println("ERROR: --hsm-pin is required with --hsm-module") os.Exit(1) } - os.Setenv("HSM_MODULE", hsmModule) - os.Setenv("HSM_PIN", hsmPin) - os.Setenv("HSM_TOKEN_LABEL", hsmTokenLabel) + storage = &x509.KeyStorageHsm{Hsm: x509.HsmInfo{hsmModule, hsmPin, hsmTokenLabel}} + } else { + storage = &x509.KeyStorageFiles{} } resp, err := api.FactoryCreateCA(factory) @@ -103,15 +104,15 @@ func doCreateCA(cmd *cobra.Command, args []string) { writeFile(x509.SignTlsScript, *resp.SignTlsScript, 0700) fmt.Println("Creating offline root CA for Factory") - resp.RootCrt = x509.CreateFactoryCa(factory) + resp.RootCrt = x509.CreateFactoryCa(storage, factory) fmt.Println("Signing Foundries TLS CSR") - resp.TlsCrt = x509.SignTlsCsr(resp.TlsCsr) + resp.TlsCrt = x509.SignTlsCsr(storage, resp.TlsCsr) writeFile(x509.TlsCertFile, resp.TlsCrt, 0400) if createOnlineCA { fmt.Println("Signing Foundries CSR for online use") - resp.CaCrt = x509.SignCaCsr(resp.CaCsr) + resp.CaCrt = x509.SignCaCsr(storage, resp.CaCsr) writeFile(x509.OnlineCaCertFile, resp.CaCrt, 0400) } @@ -121,7 +122,7 @@ func doCreateCA(cmd *cobra.Command, args []string) { resp.CaCrt += "\n" } commonName := getDeviceCaCommonName(factory) - resp.CaCrt += x509.CreateDeviceCa(commonName, factory) + resp.CaCrt += x509.CreateDeviceCa(storage, commonName, factory) } fmt.Println("Uploading signed certs to Foundries") diff --git a/x509/bash.go b/x509/bash.go index 6a8d2d45..29f89af5 100644 --- a/x509/bash.go +++ b/x509/bash.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && bashPKI package x509 @@ -9,7 +9,8 @@ import ( "github.com/foundriesio/fioctl/subcommands" ) -func run(name string, arg ...string) string { +func run(storage KeyStorage, name string, arg ...string) string { + storage.setEnvVariables() cmd := exec.Command(name, arg...) cmd.Stderr = os.Stderr out, err := cmd.Output() @@ -17,30 +18,42 @@ func run(name string, arg ...string) string { return string(out) } -func CreateFactoryCa(ou string) string { - run("./" + CreateCaScript) +func CreateFactoryCa(s KeyStorage, ou string) string { + run(s, "./" + CreateCaScript) return string(readFile(FactoryCaCertFile)) } -func CreateDeviceCa(cn string, ou string) string { - run("./"+CreateDeviceCaScript, DeviceCaKeyFile, DeviceCaCertFile) +func CreateDeviceCa(s KeyStorage, cn string, ou string) string { + run(s, "./"+CreateDeviceCaScript, DeviceCaKeyFile, DeviceCaCertFile) return string(readFile(DeviceCaCertFile)) } -func SignTlsCsr(csrPem string) string { - return run("./"+SignTlsScript, TlsCsrFile) +func SignTlsCsr(s KeyStorage, csrPem string) string { + return run(s, "./"+SignTlsScript, TlsCsrFile) } -func SignCaCsr(csrPem string) string { - return run("./"+SignCaScript, OnlineCaCsrFile) +func SignCaCsr(s KeyStorage, csrPem string) string { + return run(s, "./"+SignCaScript, OnlineCaCsrFile) } -func SignEl2GoCsr(csrPem string) string { +func SignEl2GoCsr(s KeyStorage, csrPem string) string { tmpFile, err := os.CreateTemp("", "el2g-*.csr") subcommands.DieNotNil(err) defer os.Remove(tmpFile.Name()) defer tmpFile.Close() _, err = tmpFile.Write([]byte(csrPem)) subcommands.DieNotNil(err) - return run("./"+SignCaScript, tmpFile.Name()) + return run(s, "./"+SignCaScript, tmpFile.Name()) } + +type KeyStorage interface { + setEnvVariables() +} + +func (s *KeyStorageHsm) setEnvVariables() { + os.Setenv("HSM_MODULE", s.Hsm.Module) + os.Setenv("HSM_PIN", s.Hsm.Pin) + os.Setenv("HSM_TOKEN_LABEL", s.Hsm.TokenLabel) +} + +func (s *KeyStorageFiles) setEnvVariables() {} diff --git a/x509/common.go b/x509/common.go index 8698e00e..493cefc0 100644 --- a/x509/common.go +++ b/x509/common.go @@ -27,3 +27,15 @@ func readFile(filename string) string { subcommands.DieNotNil(err) return string(data) } + +type HsmInfo struct { + Module string + Pin string + TokenLabel string +} + +type KeyStorageFiles struct{} + +type KeyStorageHsm struct { + Hsm HsmInfo +} diff --git a/x509/golang.go b/x509/golang.go index 75bdb764..541744df 100644 --- a/x509/golang.go +++ b/x509/golang.go @@ -1,11 +1,10 @@ -//go:build windows +//go:build windows || !bashPKI package x509 import ( "bytes" - "crypto/ecdsa" - "crypto/elliptic" + "crypto" "crypto/rand" "crypto/x509" "crypto/x509/pkix" @@ -31,23 +30,8 @@ func genRandomSerialNumber() *big.Int { return serial } -func genAndSaveKey(fn string) *ecdsa.PrivateKey { - 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} - - factoryKeyBytes := pem.EncodeToMemory(keyBlock) - err = os.WriteFile(fn, factoryKeyBytes, 0600) - subcommands.DieNotNil(err) - return priv -} - -func genCertificate(crtTemplate *x509.Certificate, caCrt *x509.Certificate, pub any, signerKey *ecdsa.PrivateKey) string { - certRaw, err := x509.CreateCertificate(rand.Reader, crtTemplate, caCrt, pub, signerKey) +func genCertificate(crtTemplate *x509.Certificate, caCrt *x509.Certificate, pub crypto.PublicKey, priv crypto.Signer) string { + certRaw, err := x509.CreateCertificate(rand.Reader, crtTemplate, caCrt, pub, priv) subcommands.DieNotNil(err) certPemBlock := pem.Block{Type: "CERTIFICATE", Bytes: certRaw} @@ -67,13 +51,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) @@ -112,8 +89,8 @@ func marshalSubject(cn string, ou string) pkix.Name { return pkix.Name{ExtraNames: pkixAttrTypeValue} } -func CreateFactoryCa(ou string) string { - priv := genAndSaveKey(FactoryCaKeyFile) +func CreateFactoryCa(storage KeyStorage, ou string) string { + priv, pub := storage.genAndSaveFactoryCaKey() crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), Subject: marshalSubject("Factory-CA", ou), @@ -125,15 +102,14 @@ func CreateFactoryCa(ou string) string { KeyUsage: x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } - factoryCaString := genCertificate(&crtTemplate, &crtTemplate, &priv.PublicKey, priv) + factoryCaString := genCertificate(&crtTemplate, &crtTemplate, pub, priv) writeFile(FactoryCaCertFile, factoryCaString, 0400) return factoryCaString } - -func CreateDeviceCa(cn string, ou string) string { - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) +func CreateDeviceCa(storage KeyStorage, cn string, ou string) string { + factoryKey := storage.getFactoryCaKey() factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) - priv := genAndSaveKey(DeviceCaKeyFile) + _, pub := genAndSaveKeyToFile(DeviceCaKeyFile) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), Subject: marshalSubject(cn, ou), @@ -146,14 +122,14 @@ func CreateDeviceCa(cn string, ou string) string { MaxPathLenZero: true, KeyUsage: x509.KeyUsageCertSign, } - crtPem := genCertificate(&crtTemplate, factoryCa, &priv.PublicKey, factoryKey) + crtPem := genCertificate(&crtTemplate, factoryCa, pub, factoryKey) writeFile(DeviceCaCertFile, crtPem, 0400) return crtPem } -func SignTlsCsr(csrPem string) string { +func SignTlsCsr(storage KeyStorage, csrPem string) string { csr := parsePemCertificateRequest(csrPem) - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) + factoryKey := storage.getFactoryCaKey() factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), @@ -171,9 +147,9 @@ func SignTlsCsr(csrPem string) string { return crtPem } -func SignCaCsr(csrPem string) string { +func SignCaCsr(storage KeyStorage, csrPem string) string { csr := parsePemCertificateRequest(csrPem) - factoryKey := parsePemPrivateKey(readFile(FactoryCaKeyFile)) + factoryKey := storage.getFactoryCaKey() factoryCa := parsePemCertificate(readFile(FactoryCaCertFile)) crtTemplate := x509.Certificate{ SerialNumber: genRandomSerialNumber(), @@ -191,6 +167,32 @@ func SignCaCsr(csrPem string) string { return crtPem } -func SignEl2GoCsr(csrPem string) string { - return SignCaCsr(csrPem) +func SignEl2GoCsr(storage KeyStorage, csrPem string) string { + return SignCaCsr(storage, csrPem) +} + +type KeyStorage interface { + genAndSaveFactoryCaKey() (crypto.Signer, crypto.PublicKey) + getFactoryCaKey() crypto.Signer +} + +func (s *KeyStorageFiles) genAndSaveFactoryCaKey() (crypto.Signer, crypto.PublicKey) { + return genAndSaveKeyToFile(FactoryCaKeyFile) +} + +func (s *KeyStorageFiles) getFactoryCaKey() crypto.Signer { + return loadKeyFromFile(FactoryCaKeyFile) +} + +const ( + FactoryCaKeyHsmId = "1" + FactoryCaKeyHsmLabel = "root-ca" +) + +func (s *KeyStorageHsm) genAndSaveFactoryCaKey() (crypto.Signer, crypto.PublicKey) { + return genAndSaveKeyToHsm(s.Hsm, FactoryCaKeyHsmId, FactoryCaKeyHsmLabel) +} + +func (s *KeyStorageHsm) getFactoryCaKey() crypto.Signer { + return loadKeyFromHsm(s.Hsm, FactoryCaKeyHsmId, FactoryCaKeyHsmLabel) } diff --git a/x509/hsm_test.go b/x509/hsm_test.go new file mode 100644 index 00000000..f2887fba --- /dev/null +++ b/x509/hsm_test.go @@ -0,0 +1,14 @@ +//go:build windows || !bashPKI + +package x509 + +import ( + "testing" + "fmt" +) + +func TestHsm(t *testing.T) { + storage := &KeyStorageHsm{HsmInfo{"/usr/lib/softhsm/libsofthsm2.so", "1234", "mine"}} + + fmt.Println(CreateFactoryCa(storage, "test")) +} diff --git a/x509/storage_fs.go b/x509/storage_fs.go new file mode 100644 index 00000000..ae453a9e --- /dev/null +++ b/x509/storage_fs.go @@ -0,0 +1,38 @@ +//go:build windows || !bashPKI + +package x509 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "os" + + "github.com/foundriesio/fioctl/subcommands" +) + +func genAndSaveKeyToFile(fn string) (crypto.Signer, crypto.PublicKey) { + 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} + + factoryKeyBytes := pem.EncodeToMemory(keyBlock) + err = os.WriteFile(fn, factoryKeyBytes, 0600) + subcommands.DieNotNil(err) + return priv, &priv.PublicKey +} + +func loadKeyFromFile(fn string) crypto.Signer { + keyPem := readFile(fn) + caPrivateKeyPemBlock, _ := pem.Decode([]byte(keyPem)) + caPrivateKey, err := x509.ParseECPrivateKey(caPrivateKeyPemBlock.Bytes) + subcommands.DieNotNil(err) + return caPrivateKey +} diff --git a/x509/storage_hsm.go b/x509/storage_hsm.go new file mode 100644 index 00000000..da1c64a9 --- /dev/null +++ b/x509/storage_hsm.go @@ -0,0 +1,74 @@ +//go:build windows || !bashPKI + +package x509 + +import ( + "crypto" + "crypto/x509" + "io" + "os" + "os/exec" + + "github.com/foundriesio/fioctl/subcommands" +) + +type hsmSigner struct { + hsm HsmInfo + id string + label string +} + +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 { + args := append(s.keyArgs(), "--read-object", "--type=pubkey") + cmd := exec.Command("pkcs11-tool", args...) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + subcommands.DieNotNil(err) + key, err := x509.ParsePKIXPublicKey(out) + subcommands.DieNotNil(err) + return key +} + +func (s *hsmSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + 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, crypto.PublicKey) { + key := hsmSigner{hsm, id, 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, key.Public() +} + +func loadKeyFromHsm(hsm HsmInfo, id, label string) crypto.Signer { + return &hsmSigner{hsm, id, label} +}