diff --git a/integration/nwo/fabricconfig/core.go b/integration/nwo/fabricconfig/core.go index ee5f9716e7a..6ce4252be3e 100644 --- a/integration/nwo/fabricconfig/core.go +++ b/integration/nwo/fabricconfig/core.go @@ -170,6 +170,7 @@ type Authentication struct { type BCCSP struct { Default string `yaml:"Default,omitempty"` SW *SoftwareProvider `yaml:"SW,omitempty"` + PKCS11 *PKCS11 `yaml:"PKCS11,omitempty"` } type SoftwareProvider struct { @@ -177,6 +178,14 @@ type SoftwareProvider struct { Security int `yaml:"Security,omitempty"` } +type PKCS11 struct { + Hash string `yaml:"Hash,omitempty"` + Security int `yaml:"Security,omitempty"` + Pin string `yaml:"Pin,omitempty"` + Label string `yaml:"Label,omitempty"` + Library string `yaml:"Library,omitempty"` +} + type DeliveryClient struct { ReconnectTotalTimeThreshold time.Duration `yaml:"reconnectTotalTimeThreshold,omitempty"` AddressOverrides []*AddressOverride `yaml:"addressOverrides,omitempty"` diff --git a/integration/nwo/network.go b/integration/nwo/network.go index 24610a72b06..388db10fc5f 100644 --- a/integration/nwo/network.go +++ b/integration/nwo/network.go @@ -416,6 +416,28 @@ func (n *Network) userCryptoDir(org *Organization, nodeOrganizationType, user, c ) } +// PeerOrgCADir returns the path to the folder containing the CA certificate(s) and/or +// keys for the specified peer organization. +func (n *Network) PeerOrgCADir(o *Organization) string { + return filepath.Join( + n.CryptoPath(), + "peerOrganizations", + o.Domain, + "ca", + ) +} + +// OrdererOrgCADir returns the path to the folder containing the CA certificate(s) and/or +// keys for the specified orderer organization. +func (n *Network) OrdererOrgCADir(o *Organization) string { + return filepath.Join( + n.CryptoPath(), + "ordererOrganizations", + o.Domain, + "ca", + ) +} + // PeerUserMSPDir returns the path to the MSP directory containing the // certificates and keys for the specified user of the peer. func (n *Network) PeerUserMSPDir(p *Peer, user string) string { diff --git a/integration/pkcs11/p11key.go b/integration/pkcs11/p11key.go new file mode 100644 index 00000000000..f41571aa372 --- /dev/null +++ b/integration/pkcs11/p11key.go @@ -0,0 +1,62 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pkcs11 + +import ( + "crypto" + "crypto/ecdsa" + "encoding/asn1" + "fmt" + "io" + "math/big" + + "github.com/miekg/pkcs11" +) + +// P11ECDSAKey test implementation of crypto.Signer. +type P11ECDSAKey struct { + ctx *pkcs11.Ctx + session pkcs11.SessionHandle + publicKey *ecdsa.PublicKey + privateKeyHandle pkcs11.ObjectHandle +} + +// Public returns the corresponding public key for the +// private key. +func (k *P11ECDSAKey) Public() crypto.PublicKey { + return k.publicKey +} + +// Sign implements crypto.Signer Sign(). Signs the digest the with the private key and returns a byte signature. +func (k *P11ECDSAKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + if len(digest) != opts.HashFunc().Size() { + return nil, fmt.Errorf("digest length does not equal hash function length") + } + + mech := []*pkcs11.Mechanism{ + pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil), + } + + err = k.ctx.SignInit(k.session, mech, k.privateKeyHandle) + if err != nil { + return nil, fmt.Errorf("sign init failed: %s", err) + } + + signature, err = k.ctx.Sign(k.session, digest) + if err != nil { + return nil, fmt.Errorf("sign failed: %s", err) + } + + type ECDSASignature struct{ R, S *big.Int } + + R := new(big.Int) + S := new(big.Int) + R.SetBytes(signature[0 : len(signature)/2]) + S.SetBytes(signature[len(signature)/2:]) + + return asn1.Marshal(ECDSASignature{R: R, S: S}) +} diff --git a/integration/pkcs11/pkcs11_suite_test.go b/integration/pkcs11/pkcs11_suite_test.go new file mode 100644 index 00000000000..9a7b06de965 --- /dev/null +++ b/integration/pkcs11/pkcs11_suite_test.go @@ -0,0 +1,54 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package pkcs11 + +import ( + "encoding/json" + "testing" + + bpkcs11 "github.com/hyperledger/fabric/bccsp/pkcs11" + "github.com/hyperledger/fabric/integration" + "github.com/hyperledger/fabric/integration/nwo" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestPKCS11(t *testing.T) { + RegisterFailHandler(Fail) + lib, pin, label := bpkcs11.FindPKCS11Lib() + if lib == "" || pin == "" || label == "" { + t.Skip("Skipping PKCS11 Suite: Required ENV variables not set") + } + RunSpecs(t, "PKCS11 Suite") +} + +var ( + buildServer *nwo.BuildServer + components *nwo.Components +) + +var _ = SynchronizedBeforeSuite(func() []byte { + buildServer = nwo.NewBuildServer("-tags=pkcs11") + buildServer.Serve() + + components = buildServer.Components() + payload, err := json.Marshal(components) + Expect(err).NotTo(HaveOccurred()) + return payload +}, func(payload []byte) { + err := json.Unmarshal(payload, &components) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = SynchronizedAfterSuite(func() { +}, func() { + buildServer.Shutdown() +}) + +func StartPort() int { + return integration.PKCS11Port.StartPortForNode() +} diff --git a/integration/pkcs11/pkcs11_test.go b/integration/pkcs11/pkcs11_test.go new file mode 100644 index 00000000000..a9e9b806d43 --- /dev/null +++ b/integration/pkcs11/pkcs11_test.go @@ -0,0 +1,391 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +package pkcs11 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "syscall" + "time" + + docker "github.com/fsouza/go-dockerclient" + bpkcs11 "github.com/hyperledger/fabric/bccsp/pkcs11" + "github.com/hyperledger/fabric/integration/nwo" + "github.com/hyperledger/fabric/integration/nwo/fabricconfig" + "github.com/miekg/pkcs11" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/tedsuo/ifrit" +) + +var _ = Describe("Configures PKCS#11 for peer and orderer", func() { + var ( + tempDir string + network *nwo.Network + client *docker.Client + process ifrit.Process + orderer *nwo.Orderer + ) + + BeforeEach(func() { + var err error + tempDir, err = ioutil.TempDir("", "p11") + Expect(err).NotTo(HaveOccurred()) + + client, err = docker.NewClientFromEnv() + Expect(err).NotTo(HaveOccurred()) + + network = nwo.New(nwo.BasicSolo(), tempDir, client, StartPort(), components) + network.GenerateConfigTree() + + orderer = network.Orderer("orderer") + + lib, pin, label := bpkcs11.FindPKCS11Lib() + myBCCSP := &fabricconfig.BCCSP{ + Default: "PKCS11", + PKCS11: &fabricconfig.PKCS11{ + Security: 256, + Hash: "SHA2", + Pin: pin, + Label: label, + Library: lib, + }, + } + + By("Updating bccsp peer config") + for _, peer := range network.Peers { + peerConfig := network.ReadPeerConfig(peer) + peerConfig.Peer.BCCSP = myBCCSP + network.WritePeerConfig(peer, peerConfig) + } + + By("Updating bccsp orderer config") + ordererConfig := network.ReadOrdererConfig(orderer) + ordererConfig.General.BCCSP = myBCCSP + network.WriteOrdererConfig(orderer, ordererConfig) + + network.Bootstrap() + + ctx, sess := setupPKCS11Ctx(lib, label, pin) + Expect(err).NotTo(HaveOccurred()) + defer ctx.CloseSession(sess) + + configurePeerPKCS11(ctx, sess, network) + configureOrdererPKCS11(ctx, sess, network) + + By("Starting fabric processes") + networkRunner := network.NetworkGroupRunner() + process = ifrit.Invoke(networkRunner) + Eventually(process.Ready(), network.EventuallyTimeout).Should(BeClosed()) + }) + + AfterEach(func() { + process.Signal(syscall.SIGTERM) + Eventually(process.Wait(), network.EventuallyTimeout).Should(Receive()) + + if network != nil { + network.Cleanup() + } + os.RemoveAll(tempDir) + }) + + It("Creates and join channel successfully", func() { + orderer := network.Orderer("orderer") + network.CreateAndJoinChannels(orderer) + }) +}) + +func configurePeerPKCS11(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, network *nwo.Network) { + for _, peer := range network.Peers { + orgName := peer.Organization + + peerPubKey, peerCSR, peerSerial := createCSR(ctx, sess, orgName, "peer") + adminPubKey, adminCSR, adminSerial := createCSR(ctx, sess, orgName, "admin") + + domain := network.Organization(orgName).Domain + + // Retrieves org CA cert + orgCAPath := network.PeerOrgCADir(network.Organization(orgName)) + caBytes, err := ioutil.ReadFile(filepath.Join(orgCAPath, fmt.Sprintf("ca.%s-cert.pem", domain))) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the peer signcerts") + + newOrdererPemCert := buildCert(network, caBytes, orgCAPath, peerCSR, peerSerial, peerPubKey) + updateMSPFolder(network.PeerLocalMSPDir(peer), fmt.Sprintf("peer.%s-cert.pem", domain), newOrdererPemCert) + + By("Updating the peer admin user signcerts") + newAdminPemCert := buildCert(network, caBytes, orgCAPath, adminCSR, adminSerial, adminPubKey) + + orgAdminMSPPath := network.PeerUserMSPDir(peer, "Admin") + updateMSPFolder(orgAdminMSPPath, fmt.Sprintf("Admin@%s-cert.pem", domain), newAdminPemCert) + } +} + +func configureOrdererPKCS11(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, network *nwo.Network) { + orderer := network.Orderer("orderer") + orgName := orderer.Organization + domain := network.Organization(orgName).Domain + + ordererPubKey, ordererCSR, ordererSerial := createCSR(ctx, sess, orgName, "orderer") + adminPubKey, adminCSR, adminSerial := createCSR(ctx, sess, orgName, "admin") + + // Retrieves org CA cert + orgCAPath := network.OrdererOrgCADir(network.Organization(orgName)) + caBytes, err := ioutil.ReadFile(filepath.Join(orgCAPath, fmt.Sprintf("ca.%s-cert.pem", domain))) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the orderer signcerts") + + newOrdererPemCert := buildCert(network, caBytes, orgCAPath, ordererCSR, ordererSerial, ordererPubKey) + + updateMSPFolder(network.OrdererLocalMSPDir(orderer), fmt.Sprintf("orderer.%s-cert.pem", domain), newOrdererPemCert) + + By("Updating the orderer admin user signcerts") + newAdminPemCert := buildCert(network, caBytes, orgCAPath, adminCSR, adminSerial, adminPubKey) + + orgAdminMSPPath := network.OrdererUserMSPDir(orderer, "Admin") + updateMSPFolder(orgAdminMSPPath, fmt.Sprintf("Admin@%s-cert.pem", domain), newAdminPemCert) +} + +// Creates pkcs11 context and session +func setupPKCS11Ctx(lib, label, pin string) (*pkcs11.Ctx, pkcs11.SessionHandle) { + ctx := pkcs11.New(lib) + + err := ctx.Initialize() + Expect(err).NotTo(HaveOccurred()) + + slot := findPKCS11Slot(ctx, label) + Expect(slot).Should(BeNumerically(">", 0), "Could not find slot with label %s", label) + + sess, err := ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + Expect(err).NotTo(HaveOccurred()) + + // Login + err = ctx.Login(sess, pkcs11.CKU_USER, pin) + Expect(err).NotTo(HaveOccurred()) + + return ctx, sess +} + +// Identifies pkcs11 slot using specified label +func findPKCS11Slot(ctx *pkcs11.Ctx, label string) uint { + slots, err := ctx.GetSlotList(true) + Expect(err).NotTo(HaveOccurred()) + + for _, s := range slots { + tokInfo, err := ctx.GetTokenInfo(s) + Expect(err).NotTo(HaveOccurred()) + + if tokInfo.Label == label { + return s + } + } + + return 0 +} + +// Creates CSR for provided organization and organizational unit +func createCSR(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, org, ou string) (*ecdsa.PublicKey, *x509.CertificateRequest, *big.Int) { + pubKey, pkcs11Key := generateKeyPair(ctx, sess) + + csrTemplate := x509.CertificateRequest{ + Subject: pkix.Name{ + Country: []string{"US"}, + Province: []string{"California"}, + Locality: []string{"San Francisco"}, + Organization: []string{fmt.Sprintf("%s.example.com", org)}, + OrganizationalUnit: []string{ou}, + CommonName: fmt.Sprintf("peer.%s.example.com", org), + }, + SignatureAlgorithm: x509.ECDSAWithSHA256, + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, pkcs11Key) + Expect(err).NotTo(HaveOccurred()) + + csr, err := x509.ParseCertificateRequest(csrBytes) + Expect(err).NotTo(HaveOccurred()) + err = csr.CheckSignature() + Expect(err).NotTo(HaveOccurred()) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + Expect(err).NotTo(HaveOccurred()) + + return pubKey, csr, serialNumber +} + +func buildCert(network *nwo.Network, caBytes []byte, org1CAPath string, csr *x509.CertificateRequest, serialNumber *big.Int, pubKey *ecdsa.PublicKey) []byte { + pemBlock, _ := pem.Decode(caBytes) + Expect(pemBlock).NotTo(BeNil()) + + caCert, err := x509.ParseCertificate(pemBlock.Bytes) + Expect(err).NotTo(HaveOccurred()) + + keyBytes, err := ioutil.ReadFile(filepath.Join(org1CAPath, "priv_sk")) + Expect(err).NotTo(HaveOccurred()) + + pemBlock, _ = pem.Decode(keyBytes) + Expect(pemBlock).NotTo(BeNil()) + key, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) + Expect(err).NotTo(HaveOccurred()) + caKey := key.(*ecdsa.PrivateKey) + + certTemplate := &x509.Certificate{ + Signature: csr.Signature, + SignatureAlgorithm: csr.SignatureAlgorithm, + PublicKey: csr.PublicKey, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + + SerialNumber: serialNumber, + NotBefore: time.Now().Add(-1 * time.Minute).UTC(), + NotAfter: time.Now().Add(365 * 24 * time.Hour).UTC(), + BasicConstraintsValid: true, + + Subject: csr.Subject, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{}, + } + + // Use root CA to create and sign cert + signedCert, err := x509.CreateCertificate(rand.Reader, certTemplate, caCert, pubKey, caKey) + Expect(err).NotTo(HaveOccurred()) + + return pem.EncodeToMemory(&pem.Block{Bytes: signedCert, Type: "CERTIFICATE"}) +} + +// Overwrites existing cert and removes private key from keystore folder +func updateMSPFolder(path, certName string, cert []byte) { + // Overwrite existing certificate with new certificate + err := ioutil.WriteFile(filepath.Join(path, "signcerts", certName), cert, 0644) + Expect(err).NotTo(HaveOccurred()) + + // delete the existing private key - this is stored in the hsm + adminKSCert := filepath.Join(path, "keystore", "priv_sk") + err = os.Remove(adminKSCert) + Expect(err).NotTo(HaveOccurred()) +} + +// Generating key pair in HSM, convert, and return keys +func generateKeyPair(ctx *pkcs11.Ctx, sess pkcs11.SessionHandle) (*ecdsa.PublicKey, *P11ECDSAKey) { + publabel, privlabel := "BCPUB7", "BCPRV7" + + curve := asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} // secp256r1 Curve + + marshaledOID, err := asn1.Marshal(curve) + Expect(err).NotTo(HaveOccurred()) + + pubAttrs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, marshaledOID), + + pkcs11.NewAttribute(pkcs11.CKA_ID, publabel), + pkcs11.NewAttribute(pkcs11.CKA_LABEL, publabel), + } + + privAttrs := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC), + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), + pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), + pkcs11.NewAttribute(pkcs11.CKA_SIGN, true), + + pkcs11.NewAttribute(pkcs11.CKA_ID, privlabel), + pkcs11.NewAttribute(pkcs11.CKA_LABEL, privlabel), + + pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, false), + pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true), + } + + pubK, privK, err := ctx.GenerateKeyPair( + sess, + []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_EC_KEY_PAIR_GEN, nil)}, + pubAttrs, + privAttrs, + ) + Expect(err).NotTo(HaveOccurred()) + + ecpt := ecPoint(ctx, sess, pubK) + Expect(ecpt).NotTo(BeEmpty(), "CKA_EC_POINT not found") + + hash := sha256.Sum256(ecpt) + ski := hash[:] + + setskiT := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_ID, ski), + pkcs11.NewAttribute(pkcs11.CKA_LABEL, hex.EncodeToString(ski)), + } + + err = ctx.SetAttributeValue(sess, pubK, setskiT) + Expect(err).NotTo(HaveOccurred()) + + err = ctx.SetAttributeValue(sess, privK, setskiT) + Expect(err).NotTo(HaveOccurred()) + + // convert pub key to rsa types + nistCurve := elliptic.P256() + x, y := elliptic.Unmarshal(nistCurve, ecpt) + if x == nil { + Expect(x).NotTo(BeNil(), "Failed Unmarshaling Public Key") + } + + pubKey := &ecdsa.PublicKey{Curve: nistCurve, X: x, Y: y} + + pkcs11Key := &P11ECDSAKey{ + ctx: ctx, + session: sess, + publicKey: pubKey, + privateKeyHandle: privK, + } + + return pubKey, pkcs11Key +} + +// SoftHSM reports extra two bytes before the uncompressed point +// see /bccsp/pkcs11/pkcs11.go::ecPoint() for additional details +func ecPoint(pkcs11lib *pkcs11.Ctx, session pkcs11.SessionHandle, key pkcs11.ObjectHandle) (ecpt []byte) { + template := []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, nil), + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, nil), + } + + attr, err := pkcs11lib.GetAttributeValue(session, key, template) + if err != nil { + Expect(err).NotTo(HaveOccurred(), "PKCS11: get(EC point)") + } + + for _, a := range attr { + if a.Type != pkcs11.CKA_EC_POINT { + continue + } + + switch { + case ((len(a.Value) % 2) == 0) && (byte(0x04) == a.Value[0]) && (byte(0x04) == a.Value[len(a.Value)-1]): + ecpt = a.Value[0 : len(a.Value)-1] // Trim trailing 0x04 + case byte(0x04) == a.Value[0] && byte(0x04) == a.Value[2]: + ecpt = a.Value[2:len(a.Value)] + default: + ecpt = a.Value + } + } + + return ecpt +} diff --git a/integration/ports.go b/integration/ports.go index ab81c023c0a..88ea8a5c3ea 100644 --- a/integration/ports.go +++ b/integration/ports.go @@ -33,6 +33,7 @@ const ( LifecyclePort MSPPort NWOBasePort + PKCS11Port PluggableBasePort PrivateDataBasePort RaftBasePort