Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI
on: [push, pull_request]
jobs:
hsm:
env:
HSM_SO_PIN: 1234
HSM_PIN: 1234
TOKEN_LABEL: 'test'
KEY_LABEL: 'test-key'
CGO_ENABLED: 1
name: Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
go: [ 1.24.x ]
steps:

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}

- name: Check out code
uses: actions/checkout@v2

- name: Setup SoftHSM
env:
SOFTHSM2_CONF: ${{ github.workspace }}/softhsm2.conf
id: softhsm
run: |
mkdir ${GITHUB_WORKSPACE}/softhsm2-tokens
# 2) custom SoftHSM config that uses it
cat > "${SOFTHSM2_CONF}" <<EOF
directories.tokendir = ${GITHUB_WORKSPACE}/softhsm2-tokens
objectstore.backend = file
log.level = INFO
EOF
echo "SOFTHSM2_CONF=${SOFTHSM2_CONF}" >> "${GITHUB_ENV}" # make it stick for later steps

sudo apt-get update
sudo apt-get -y install libsofthsm2 gnutls-bin p11-kit

# set output of lib to environment variable

softhsm2-util --init-token --free --label $TOKEN_LABEL --so-pin $HSM_SO_PIN --pin $HSM_PIN
p11tool --generate-privkey=rsa --login --set-pin=$HSM_PIN --label="$KEY_LABEL" "pkcs11:token=$TOKEN_LABEL" --outfile ${{ github.workspace }}/public_key.pem

p11-kit list-modules
- name: Run Tests
working-directory: ${{ github.workspace }}
env:
HSM_MODULE: "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so"
SIGNING_SERVER_BIN: ${{ github.workspace }}/signing-server
HSM_PUBLIC_KEY_FILE: ${{ github.workspace }}/public_key.pem
SOFTHSM2_CONF: ${{ github.workspace }}/softhsm2.conf
run: |
go test -v ./...
16 changes: 12 additions & 4 deletions cmd/signing-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ type Config struct {
HSMKeyLabel string
HSMKeyId string

HSMContext sign.HSMOptions
HSMContext *sign.HSMOptions

// calculated by program
Logger *zap.Logger
Expand All @@ -95,6 +95,7 @@ func (c *Config) SetupHSM() error {
if c.HSMKeyId != "" {
c.Logger.Info("selecting key id", zap.String("id", c.HSMKeyId))
}
c.Logger.Info("using token label", zap.String("label", c.HSMTokenLabel))

var slot *int
if c.HSMSlot >= 0 {
Expand All @@ -105,6 +106,7 @@ func (c *Config) SetupHSM() error {
TokenLabel: c.HSMTokenLabel,
Slot: slot,
Pin: c.HSMPass,
Logger: c.Logger,
}

ctx, err := crypto11.NewSession(config)
Expand Down Expand Up @@ -494,7 +496,7 @@ func run(cfg *Config) error {
return err
}
}
return RunServer(cfg, responseBuilders)
return RunServer(context.Background(), cfg, responseBuilders)
} else {
return RunSigner(cfg, pflag.CommandLine.Args(), responseBuilders)
}
Expand All @@ -516,7 +518,7 @@ func RunSigner(cfg *Config, args []string, responseBuilders map[string]encoding.

hashfunc, ok := sign.GetHashFunction(cfg.Hash)
if !ok {
return fmt.Errorf("unknown hash algorith %q", cfg.Hash)
return fmt.Errorf("unknown hash algorithm %q", cfg.Hash)
}

var data []byte
Expand Down Expand Up @@ -563,7 +565,7 @@ func RunSigner(cfg *Config, args []string, responseBuilders map[string]encoding.
return err
}

func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder) error {
func RunServer(ctx context.Context, cfg *Config, responseBuilders map[string]encoding.ResponseBuilder) error {
var err error

addr := ":" + cfg.Port
Expand All @@ -574,6 +576,11 @@ func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder
cfg.Logger.Info("register route", zap.String("route", route))
r.Methods(http.MethodPost).Path(route).Handler(h.HTTPHandler(responseBuilders, cfg.MaxBodySizeBytes))
}
r.Methods(http.MethodGet).Path("/healthz").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
lm := logutil.LoggingMiddleware{
Logger: cfg.Logger,
}
Expand Down Expand Up @@ -627,6 +634,7 @@ func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder
select {
case <-c:
case <-stop:
case <-ctx.Done():
}

// Create a deadline to wait for.
Expand Down
186 changes: 186 additions & 0 deletions cmd/signing-server/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package main

import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"testing"
"time"

"go.uber.org/zap"
)

func TestSoftHSMConcurrentSignRequests(t *testing.T) {
hsmModule := os.Getenv("HSM_MODULE")
if hsmModule == "" {
t.Skip("HSM_MODULE environment variable is not set")
}
tokenLabel := os.Getenv("TOKEN_LABEL")
if tokenLabel == "" {
t.Skip("TOKEN_LABEL environment variable is not set")
}
keyLabel := os.Getenv("KEY_LABEL")
if keyLabel == "" {
t.Skip("KEY_LABEL environment variable is not set")
}
hsmPin := os.Getenv("HSM_PIN")
if hsmPin == "" {
t.Skip("HSM_PIN environment variable is not set")
}

const (
base = "http://localhost:8080"
healthURL = base + "/healthz"
url = base + "/sign/rsassa-pss?hashAlgorithm=sha256"
bodyHex = "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f"
)

var (
headers = map[string]string{
"Content-Type": "text/plain",
"Content-Encoding": "hex",
"Accept": "application/x-pem-file",
}
)

l, err := zap.NewDevelopment()
if err != nil {
t.Fatalf("Failed to create logger: %v", err)
}

ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(func() {
cancel()
})

go func() {
if err := run(&Config{
HSMModule: strings.TrimSpace(hsmModule),
HSMTokenLabel: tokenLabel,
HSMSlot: -1,
HSMKeyLabel: keyLabel,
HSMPass: hsmPin,
Port: "8080",
DisableHTTPS: true,
DisableAuth: true,
MaxBodySizeBytes: 2048,
Logger: l,
RunServer: true,
}); err != nil {
cancel()
t.Errorf("Failed to start signing server: %v", err)
return
}
}()

var wg sync.WaitGroup
client := &http.Client{Timeout: 10 * time.Second}

outer:
for {
select {
case <-ctx.Done():
t.Fatal("Test context was cancelled before health check passed")
default:
log.Println("Waiting for signing server to be ready...")
healthReq, err := http.NewRequest(http.MethodGet, healthURL, nil)
if err != nil {
t.Fatalf("Health request creation failed: %v", err)
}
healthResp, _ := client.Do(healthReq)
if healthResp != nil && healthResp.StatusCode == http.StatusOK {
log.Println("Health check passed")
break outer
}
time.Sleep(500 * time.Millisecond)
}
}

for i := 0; i < 10; i++ {
wg.Add(1)

go func(index int) {
defer wg.Done()

req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(bodyHex))
if err != nil {
t.Errorf("Request %d creation failed: %v", index, err)
return
}

for k, v := range headers {
req.Header.Set(k, v)
}

resp, err := client.Do(req)
if err != nil {
t.Errorf("Request %d failed: %v", index, err)
return
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Request %d read body failed: %v", index, err)
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Request %d failed with status %d: %s", index, resp.StatusCode, body)
return
}

block, _ := pem.Decode(body)
if block == nil || block.Type != "SIGNATURE" {
log.Fatal("Failed to parse PEM block")
}

hsmPublicKeyFile := os.Getenv("HSM_PUBLIC_KEY_FILE")
if hsmPublicKeyFile == "" {
log.Println("HSM_PUBLIC_KEY_FILE environment variable is not set, skipping public key verification")
} else {
hsmPublicKeyData, err := os.ReadFile(hsmPublicKeyFile)
if err != nil {
log.Fatalf("Failed to read public key file %s: %v", hsmPublicKeyFile, err)
}
publicKeyBlock, _ := pem.Decode(hsmPublicKeyData)
if publicKeyBlock == nil || publicKeyBlock.Type != "PUBLIC KEY" {
log.Fatal("Failed to parse PEM block for public key")
}
pub, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
if err != nil {
log.Fatalf("Failed to parse public key: %v", err)
}
rsaPubKey, ok := pub.(*rsa.PublicKey)
if !ok {
log.Fatal("Public key is not of type RSA")
}
rawDigest, err := hex.DecodeString(bodyHex)
if err != nil {
log.Fatalf("Failed to decode hex body: %v", err)
}
if err := rsa.VerifyPSS(rsaPubKey, crypto.SHA256, rawDigest, block.Bytes, nil); err != nil {
log.Fatalf("Signature verification failed: %v", err)
} else {
log.Printf("Signature verification succeeded for request %d", index)
}
}

fmt.Printf("Decoded signature: %x\n", block.Bytes)

log.Printf("Response %d: %d - %.100q\n", index, resp.StatusCode, body)
}(i)
}

wg.Wait()
}
8 changes: 8 additions & 0 deletions pkg/crypto11/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ func (s *Session) SignPSS(key pkcs11.ObjectHandle, digest []byte, opts *rsa.PSSO
ulongToBytes(mgf),
ulongToBytes(sLen))
mech := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_PSS, parameters)}
if s.serialized {
s.mu.Lock()
defer s.mu.Unlock()
}
if err = s.Ctx.SignInit(s.Handle, mech, key); err != nil {
return nil, err
}
Expand All @@ -142,6 +146,10 @@ func (s *Session) SignPKCS1v15(key pkcs11.ObjectHandle, digest []byte, hash cryp
copy(T[0:len(oid)], oid)
copy(T[len(oid):], digest)
mech := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS, nil)}
if s.serialized {
s.mu.Lock()
defer s.mu.Unlock()
}
err = s.Ctx.SignInit(s.Handle, mech, key)
if err == nil {
signature, err = s.Ctx.Sign(s.Handle, T)
Expand Down
Loading