From 0ed3eb61392717cc76242bdfa36b34ac4e0c1766 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Fri, 2 Oct 2026 12:44:17 +0200 Subject: [PATCH] Test the PKCS#11 build against SoftHSM in CI and warn once where the build has no driver --- .github/workflows/golang-test-linux.yml | 36 +++++++++++++++++++ client/internal/certproof/README.md | 6 ++-- client/internal/certproof/pkcs11store_test.go | 23 ++++++++++++ client/internal/certproof/store_other.go | 16 ++++++++- client/internal/certproof/store_other_test.go | 14 ++++++-- client/internal/pkcs11/driver_linux.go | 5 +++ client/internal/pkcs11/driver_stub.go | 5 +++ 7 files changed, 100 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 449eb14fa..bdcf9aea3 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -207,6 +207,42 @@ jobs: # regression test, and need no frontend bundle. run: CGO_ENABLED=1 go test -timeout 5m ./client/ui/authsession/... ./client/ui/i18n/... ./client/ui/preferences/... ./client/ui/services/... + test_client_pkcs11: + name: "Client PKCS#11 / Unit" + # The deb and rpm packages ship the client built with the pkcs11 tag, which loads + # PKCS#11 modules through purego. The other jobs build without the tag, so this one + # compiles that driver and signs with a real SoftHSM token, the build release ships. + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: "go.mod" + cache: false + + - name: Install SoftHSM + run: sudo apt update && sudo apt install -y -q softhsm2 + + - name: Initialise token + run: | + mkdir -p "$RUNNER_TEMP/softhsm/tokens" + printf 'directories.tokendir = %s\nobjectstore.backend = file\n' "$RUNNER_TEMP/softhsm/tokens" > "$RUNNER_TEMP/softhsm/softhsm2.conf" + echo "SOFTHSM2_CONF=$RUNNER_TEMP/softhsm/softhsm2.conf" >> "$GITHUB_ENV" + SOFTHSM2_CONF="$RUNNER_TEMP/softhsm/softhsm2.conf" softhsm2-util --init-token --free --label netbird --pin 1234 --so-pin 1234 + + - name: Vet + run: CGO_ENABLED=0 go vet -tags pkcs11 ./client/internal/pkcs11/... ./client/internal/certproof/... + + - name: Test + env: + NB_TEST_PKCS11_URI: "pkcs11:token=netbird?module-path=/usr/lib/softhsm/libsofthsm2.so&pin-value=1234" + run: CGO_ENABLED=0 go test -tags pkcs11 -timeout 5m ./client/internal/pkcs11/... ./client/internal/certproof/... + test_client_on_docker: name: "Client (Docker) / Unit" needs: [build-cache] diff --git a/client/internal/certproof/README.md b/client/internal/certproof/README.md index d99bb171a..b6f658506 100644 --- a/client/internal/certproof/README.md +++ b/client/internal/certproof/README.md @@ -197,8 +197,10 @@ The module is loaded at runtime without cgo, through `purego`, which means the b dynamically linked against libc. The store is therefore compiled in only with `-tags pkcs11` on linux/amd64 and linux/arm64: the deb and rpm packages are built that way, since they target glibc distributions, while the release tarballs and the Alpine-based container -images keep the fully static build. Without the tag, setting `NB_TPM_PIN` logs that -the build lacks the support. +images keep the fully static build. The arm and 386 packages carry the tag too but have +no driver, so they behave like the static build. A build without support logs one warning +when a token is configured and keeps reading the PEM directory. The "Client PKCS#11 / +Unit" CI job builds with the tag and signs with a SoftHSM token. To exercise the path without hardware, initialise a SoftHSM token and run the end-to-end test, which imports a key and certificate itself: diff --git a/client/internal/certproof/pkcs11store_test.go b/client/internal/certproof/pkcs11store_test.go index e36f800ac..3a8a17b03 100644 --- a/client/internal/certproof/pkcs11store_test.go +++ b/client/internal/certproof/pkcs11store_test.go @@ -339,3 +339,26 @@ func TestNewPKCS11Store_PIN(t *testing.T) { assert.ErrorIs(t, err, errPINNeedsToken, "%s: a PIN must not go to whichever token is listed first", name) } } + +// TestPKCS11Store_WrongPINIsTriedOnce logs in to the real token with a wrong PIN: the +// token refuses it, and the next collection refuses to send the same PIN again rather +// than spending another attempt of the token's lockout counter. +func TestPKCS11Store_WrongPINIsTriedOnce(t *testing.T) { + _, uri := pkcs11TestStore(t, "") + + wrongPIN := "wrong-pin-" + t.Name() + store, err := NewPKCS11Store(PKCS11Config{URI: uri, PIN: wrongPIN}, "") + require.NoError(t, err) + + _, err = store.Candidates(context.Background()) + require.Error(t, err) + assert.True(t, pkcs11.PINRejected(err), "the token itself rejects the PIN: %v", err) + + _, err = store.Candidates(context.Background()) + assert.ErrorIs(t, err, errPINRejectedBefore, "the rejected PIN is not sent to the token again") + + good, err := NewPKCS11Store(PKCS11Config{URI: uri}, "") + require.NoError(t, err) + _, err = good.Candidates(context.Background()) + assert.NoError(t, err, "the correct PIN for the same token is unaffected") +} diff --git a/client/internal/certproof/store_other.go b/client/internal/certproof/store_other.go index a0bbc0c68..1b8092cd2 100644 --- a/client/internal/certproof/store_other.go +++ b/client/internal/certproof/store_other.go @@ -2,7 +2,15 @@ package certproof -import log "github.com/sirupsen/logrus" +import ( + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/pkcs11" +) + +var unsupportedTokenOnce sync.Once // DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs. func DefaultStore() Store { @@ -16,6 +24,12 @@ func storeWithToken(cfg Config) Store { if cfg.PKCS11.URI == "" && cfg.PKCS11.PIN == "" { return files } + if !pkcs11.Supported() { + unsupportedTokenOnce.Do(func() { + log.Warnf("ignoring the configured PKCS#11 token: %v", pkcs11.ErrUnsupported) + }) + return files + } token, err := NewPKCS11Store(cfg.PKCS11, cfg.dir()) if err != nil { log.Warnf("ignoring PKCS#11 URI: %v", err) diff --git a/client/internal/certproof/store_other_test.go b/client/internal/certproof/store_other_test.go index ba0aeb033..19fbe33c8 100644 --- a/client/internal/certproof/store_other_test.go +++ b/client/internal/certproof/store_other_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal/pkcs11" ) func TestStoreWithToken(t *testing.T) { @@ -23,10 +25,18 @@ func TestStoreWithToken(t *testing.T) { assert.IsType(t, &FileStore{}, storeWithToken(Config{PKCS11: PKCS11Config{URI: "not-a-pkcs11-uri"}}), "an invalid URI must not hide the PEM directory") assert.IsType(t, &FileStore{}, storeWithToken(Config{PKCS11: PKCS11Config{PIN: "1234"}}), "a PIN naming no token is refused and leaves the PEM directory") - for name, cfg := range map[string]PKCS11Config{ + configured := map[string]PKCS11Config{ "env pin with token uri": {URI: "pkcs11:token=netbird", PIN: "1234"}, "uri alone": {URI: "pkcs11:token=netbird?pin-value=1234"}, - } { + } + if !pkcs11.Supported() { + for name, cfg := range configured { + assert.IsType(t, &FileStore{}, storeWithToken(Config{Dir: dir, PKCS11: cfg}), + "%s: a build without PKCS#11 support reads the PEM directory alone", name) + } + return + } + for name, cfg := range configured { store, ok := storeWithToken(Config{Dir: dir, PKCS11: cfg}).(Stores) require.True(t, ok, "%s joins the token to the PEM directory", name) require.Len(t, store, 2, name) diff --git a/client/internal/pkcs11/driver_linux.go b/client/internal/pkcs11/driver_linux.go index 6faed4720..3516eccff 100644 --- a/client/internal/pkcs11/driver_linux.go +++ b/client/internal/pkcs11/driver_linux.go @@ -121,6 +121,11 @@ type module struct { cSign func(session ulong, data *byte, dataLen ulong, signature *byte, signatureLen *ulong) ulong } +// Supported reports whether this build can load PKCS#11 modules. +func Supported() bool { + return true +} + func load(path string) (driver, error) { lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL) if err != nil { diff --git a/client/internal/pkcs11/driver_stub.go b/client/internal/pkcs11/driver_stub.go index 30f461cec..35d4d9333 100644 --- a/client/internal/pkcs11/driver_stub.go +++ b/client/internal/pkcs11/driver_stub.go @@ -2,6 +2,11 @@ package pkcs11 +// Supported reports whether this build can load PKCS#11 modules. +func Supported() bool { + return false +} + func load(string) (driver, error) { return nil, ErrUnsupported }