Skip to content

Commit

Permalink
add PKCS11 support (#1153)
Browse files Browse the repository at this point in the history
* add PKCS11 support

* add pkcs11 build option to the makefile, add a stub pkclient to avoid forcing CGO onto people

* don't print the pkcs11 option on nebula-cert keygen if not compiled in

* remove linux-arm64-pkcs11 from the all target to fix CI

* correctly serialize ec keys

* nebula-cert: support PKCS#11 for sign and ca

* fix gofmt lint

* clean up some logic with regard to closing sessions

* pkclient: handle empty correctly for TPM2

* Update Makefile and Actions

---------

Co-authored-by: Morgan Jones <me@numin.it>
Co-authored-by: John Maguire <contact@johnmaguire.me>
  • Loading branch information
3 people committed Sep 9, 2024
1 parent ab81b62 commit 35603d1
Show file tree
Hide file tree
Showing 21 changed files with 754 additions and 120 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ jobs:
- name: End 2 end
run: make e2evv GOEXPERIMENT=boringcrypto CGO_ENABLED=1

test-linux-pkcs11:
name: Build and test on linux with pkcs11
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: '1.22'
check-latest: true

- name: Build
run: make bin-pkcs11

- name: Test
run: make test-pkcs11

test:
name: Build and test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
**.crt
**.key
**.pem
**.pub
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.key
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.crt
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ ALL_LINUX = linux-amd64 \
linux-mips64le \
linux-mips-softfloat \
linux-riscv64 \
linux-loong64
linux-loong64

ALL_FREEBSD = freebsd-amd64 \
freebsd-arm64
Expand All @@ -63,7 +63,7 @@ ALL = $(ALL_LINUX) \
e2e:
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e

e2ev: TEST_FLAGS = -v
e2ev: TEST_FLAGS += -v
e2ev: e2e

e2evv: TEST_ENV += TEST_LOGS=1
Expand Down Expand Up @@ -96,7 +96,7 @@ release-netbsd: $(ALL_NETBSD:%=build/nebula-%.tar.gz)

release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz

BUILD_ARGS = -trimpath
BUILD_ARGS += -trimpath

bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
mv $? .
Expand All @@ -116,6 +116,10 @@ bin-freebsd-arm64: build/freebsd-arm64/nebula build/freebsd-arm64/nebula-cert
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
mv $? .

bin-pkcs11: BUILD_ARGS += -tags pkcs11
bin-pkcs11: CGO_ENABLED = 1
bin-pkcs11: bin

bin:
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
Expand Down Expand Up @@ -168,6 +172,9 @@ test:
test-boringcrypto:
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -v ./...

test-pkcs11:
CGO_ENABLED=1 go test -v -tags pkcs11 ./...

test-cov-html:
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Expand Down
37 changes: 35 additions & 2 deletions cert/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sync/atomic"
"time"

"github.com/slackhq/nebula/pkclient"
"golang.org/x/crypto/curve25519"
"google.golang.org/protobuf/proto"
)
Expand All @@ -41,8 +42,9 @@ const (
)

type NebulaCertificate struct {
Details NebulaCertificateDetails
Signature []byte
Details NebulaCertificateDetails
Pkcs11Backed bool
Signature []byte

// the cached hex string of the calculated sha256sum
// for VerifyWithCache
Expand Down Expand Up @@ -555,6 +557,34 @@ func (nc *NebulaCertificate) Sign(curve Curve, key []byte) error {
return nil
}

// SignPkcs11 signs a nebula cert with the provided private key
func (nc *NebulaCertificate) SignPkcs11(curve Curve, client *pkclient.PKClient) error {
if !nc.Pkcs11Backed {
return fmt.Errorf("certificate is not PKCS#11 backed")
}

if curve != nc.Details.Curve {
return fmt.Errorf("curve in cert and private key supplied don't match")
}

if curve != Curve_P256 {
return fmt.Errorf("only P256 is supported by PKCS#11")
}

b, err := proto.Marshal(nc.getRawDetails())
if err != nil {
return err
}

sig, err := client.SignASN1(b)
if err != nil {
return err
}

nc.Signature = sig
return nil
}

// CheckSignature verifies the signature against the provided public key
func (nc *NebulaCertificate) CheckSignature(key []byte) bool {
b, err := proto.Marshal(nc.getRawDetails())
Expand Down Expand Up @@ -693,6 +723,9 @@ func (nc *NebulaCertificate) CheckRootConstrains(signer *NebulaCertificate) erro

// VerifyPrivateKey checks that the public key in the Nebula certificate and a supplied private key match
func (nc *NebulaCertificate) VerifyPrivateKey(curve Curve, key []byte) error {
if nc.Pkcs11Backed {
return nil //todo!
}
if curve != nc.Details.Curve {
return fmt.Errorf("curve in cert and private key supplied don't match")
}
Expand Down
121 changes: 84 additions & 37 deletions cmd/nebula-cert/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"

"flag"
"fmt"
"io"
Expand All @@ -15,6 +16,7 @@ import (

"github.com/skip2/go-qrcode"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/pkclient"
"golang.org/x/crypto/ed25519"
)

Expand All @@ -33,7 +35,8 @@ type caFlags struct {
argonParallelism *uint
encryption *bool

curve *string
curve *string
p11url *string
}

func newCaFlags() *caFlags {
Expand All @@ -52,6 +55,7 @@ func newCaFlags() *caFlags {
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
cf.p11url = p11Flag(cf.set)
return &cf
}

Expand All @@ -76,17 +80,21 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
return err
}

isP11 := len(*cf.p11url) > 0

if err := mustFlagString("name", cf.name); err != nil {
return err
}
if err := mustFlagString("out-key", cf.outKeyPath); err != nil {
return err
if !isP11 {
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
return err
}
}
if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
return err
}
var kdfParams *cert.Argon2Parameters
if *cf.encryption {
if !isP11 && *cf.encryption {
if kdfParams, err = parseArgonParameters(*cf.argonMemory, *cf.argonParallelism, *cf.argonIterations); err != nil {
return err
}
Expand Down Expand Up @@ -143,7 +151,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
}

var passphrase []byte
if *cf.encryption {
if !isP11 && *cf.encryption {
for i := 0; i < 5; i++ {
out.Write([]byte("Enter passphrase: "))
passphrase, err = pr.ReadPassword()
Expand All @@ -166,29 +174,54 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error

var curve cert.Curve
var pub, rawPriv []byte
switch *cf.curve {
case "25519", "X25519", "Curve25519", "CURVE25519":
curve = cert.Curve_CURVE25519
pub, rawPriv, err = ed25519.GenerateKey(rand.Reader)
var p11Client *pkclient.PKClient

if isP11 {
switch *cf.curve {
case "P256":
curve = cert.Curve_P256
default:
return fmt.Errorf("invalid curve for PKCS#11: %s", *cf.curve)
}

p11Client, err = pkclient.FromUrl(*cf.p11url)
if err != nil {
return fmt.Errorf("error while generating ed25519 keys: %s", err)
return fmt.Errorf("error while creating PKCS#11 client: %w", err)
}
case "P256":
var key *ecdsa.PrivateKey
curve = cert.Curve_P256
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
defer func(client *pkclient.PKClient) {
_ = client.Close()
}(p11Client)
pub, err = p11Client.GetPubKey()
if err != nil {
return fmt.Errorf("error while generating ecdsa keys: %s", err)
return fmt.Errorf("error while getting public key with PKCS#11: %w", err)
}
} else {
switch *cf.curve {
case "25519", "X25519", "Curve25519", "CURVE25519":
curve = cert.Curve_CURVE25519
pub, rawPriv, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return fmt.Errorf("error while generating ed25519 keys: %s", err)
}
case "P256":
var key *ecdsa.PrivateKey
curve = cert.Curve_P256
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("error while generating ecdsa keys: %s", err)
}

// ecdh.PrivateKey lets us get at the encoded bytes, even though
// we aren't using ECDH here.
eKey, err := key.ECDH()
if err != nil {
return fmt.Errorf("error while converting ecdsa key: %s", err)
// ecdh.PrivateKey lets us get at the encoded bytes, even though
// we aren't using ECDH here.
eKey, err := key.ECDH()
if err != nil {
return fmt.Errorf("error while converting ecdsa key: %s", err)
}
rawPriv = eKey.Bytes()
pub = eKey.PublicKey().Bytes()
default:
return fmt.Errorf("invalid curve: %s", *cf.curve)
}
rawPriv = eKey.Bytes()
pub = eKey.PublicKey().Bytes()
}

nc := cert.NebulaCertificate{
Expand All @@ -203,34 +236,48 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
IsCA: true,
Curve: curve,
},
Pkcs11Backed: isP11,
}

if _, err := os.Stat(*cf.outKeyPath); err == nil {
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
if !isP11 {
if _, err := os.Stat(*cf.outKeyPath); err == nil {
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
}
}

if _, err := os.Stat(*cf.outCertPath); err == nil {
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
}

err = nc.Sign(curve, rawPriv)
if err != nil {
return fmt.Errorf("error while signing: %s", err)
}

var b []byte
if *cf.encryption {
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)

if isP11 {
err = nc.SignPkcs11(curve, p11Client)
if err != nil {
return fmt.Errorf("error while encrypting out-key: %s", err)
return fmt.Errorf("error while signing with PKCS#11: %w", err)
}
} else {
b = cert.MarshalSigningPrivateKey(curve, rawPriv)
}
err = nc.Sign(curve, rawPriv)
if err != nil {
return fmt.Errorf("error while signing: %s", err)
}

err = os.WriteFile(*cf.outKeyPath, b, 0600)
if err != nil {
return fmt.Errorf("error while writing out-key: %s", err)
if *cf.encryption {
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
if err != nil {
return fmt.Errorf("error while encrypting out-key: %s", err)
}
} else {
b = cert.MarshalSigningPrivateKey(curve, rawPriv)
}

err = os.WriteFile(*cf.outKeyPath, b, 0600)
if err != nil {
return fmt.Errorf("error while writing out-key: %s", err)
}
if _, err := os.Stat(*cf.outCertPath); err == nil {
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
}
}

b, err = nc.MarshalToPEM()
Expand Down
1 change: 1 addition & 0 deletions cmd/nebula-cert/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func Test_caHelp(t *testing.T) {
" \tOptional: path to write the private key to (default \"ca.key\")\n"+
" -out-qr string\n"+
" \tOptional: output a qr code image (png) of the certificate\n"+
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
" -subnets string\n"+
" \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets\n",
ob.String(),
Expand Down
Loading

0 comments on commit 35603d1

Please sign in to comment.