Skip to content

Commit c730e44

Browse files
fix: allow concurrent sign operations and serialize if not possible (#32)
<!-- markdownlint-disable MD041 --> #### What this PR does / why we need it #### Which issue(s) this PR fixes <!-- Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> #26
1 parent 1906bae commit c730e44

File tree

8 files changed

+295
-16
lines changed

8 files changed

+295
-16
lines changed

.github/workflows/ci.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CI
2+
on: [push, pull_request]
3+
jobs:
4+
hsm:
5+
env:
6+
HSM_SO_PIN: 1234
7+
HSM_PIN: 1234
8+
TOKEN_LABEL: 'test'
9+
KEY_LABEL: 'test-key'
10+
CGO_ENABLED: 1
11+
name: Build and Test
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
go: [ 1.24.x ]
16+
steps:
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v2
20+
with:
21+
go-version: ${{ matrix.go }}
22+
23+
- name: Check out code
24+
uses: actions/checkout@v2
25+
26+
- name: Setup SoftHSM
27+
env:
28+
SOFTHSM2_CONF: ${{ github.workspace }}/softhsm2.conf
29+
id: softhsm
30+
run: |
31+
mkdir ${GITHUB_WORKSPACE}/softhsm2-tokens
32+
# 2) custom SoftHSM config that uses it
33+
cat > "${SOFTHSM2_CONF}" <<EOF
34+
directories.tokendir = ${GITHUB_WORKSPACE}/softhsm2-tokens
35+
objectstore.backend = file
36+
log.level = INFO
37+
EOF
38+
echo "SOFTHSM2_CONF=${SOFTHSM2_CONF}" >> "${GITHUB_ENV}" # make it stick for later steps
39+
40+
sudo apt-get update
41+
sudo apt-get -y install libsofthsm2 gnutls-bin p11-kit
42+
43+
# set output of lib to environment variable
44+
45+
softhsm2-util --init-token --free --label $TOKEN_LABEL --so-pin $HSM_SO_PIN --pin $HSM_PIN
46+
p11tool --generate-privkey=rsa --login --set-pin=$HSM_PIN --label="$KEY_LABEL" "pkcs11:token=$TOKEN_LABEL" --outfile ${{ github.workspace }}/public_key.pem
47+
48+
p11-kit list-modules
49+
- name: Run Tests
50+
working-directory: ${{ github.workspace }}
51+
env:
52+
HSM_MODULE: "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so"
53+
SIGNING_SERVER_BIN: ${{ github.workspace }}/signing-server
54+
HSM_PUBLIC_KEY_FILE: ${{ github.workspace }}/public_key.pem
55+
SOFTHSM2_CONF: ${{ github.workspace }}/softhsm2.conf
56+
run: |
57+
go test -v ./...

cmd/signing-server/main.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ type Config struct {
7878
HSMKeyLabel string
7979
HSMKeyId string
8080

81-
HSMContext sign.HSMOptions
81+
HSMContext *sign.HSMOptions
8282

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

99100
var slot *int
100101
if c.HSMSlot >= 0 {
@@ -105,6 +106,7 @@ func (c *Config) SetupHSM() error {
105106
TokenLabel: c.HSMTokenLabel,
106107
Slot: slot,
107108
Pin: c.HSMPass,
109+
Logger: c.Logger,
108110
}
109111

110112
ctx, err := crypto11.NewSession(config)
@@ -494,7 +496,7 @@ func run(cfg *Config) error {
494496
return err
495497
}
496498
}
497-
return RunServer(cfg, responseBuilders)
499+
return RunServer(context.Background(), cfg, responseBuilders)
498500
} else {
499501
return RunSigner(cfg, pflag.CommandLine.Args(), responseBuilders)
500502
}
@@ -516,7 +518,7 @@ func RunSigner(cfg *Config, args []string, responseBuilders map[string]encoding.
516518

517519
hashfunc, ok := sign.GetHashFunction(cfg.Hash)
518520
if !ok {
519-
return fmt.Errorf("unknown hash algorith %q", cfg.Hash)
521+
return fmt.Errorf("unknown hash algorithm %q", cfg.Hash)
520522
}
521523

522524
var data []byte
@@ -563,7 +565,7 @@ func RunSigner(cfg *Config, args []string, responseBuilders map[string]encoding.
563565
return err
564566
}
565567

566-
func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder) error {
568+
func RunServer(ctx context.Context, cfg *Config, responseBuilders map[string]encoding.ResponseBuilder) error {
567569
var err error
568570

569571
addr := ":" + cfg.Port
@@ -574,6 +576,11 @@ func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder
574576
cfg.Logger.Info("register route", zap.String("route", route))
575577
r.Methods(http.MethodPost).Path(route).Handler(h.HTTPHandler(responseBuilders, cfg.MaxBodySizeBytes))
576578
}
579+
r.Methods(http.MethodGet).Path("/healthz").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
580+
w.Header().Set("Content-Type", "text/plain")
581+
w.WriteHeader(http.StatusOK)
582+
_, _ = w.Write([]byte("ok"))
583+
})
577584
lm := logutil.LoggingMiddleware{
578585
Logger: cfg.Logger,
579586
}
@@ -627,6 +634,7 @@ func RunServer(cfg *Config, responseBuilders map[string]encoding.ResponseBuilder
627634
select {
628635
case <-c:
629636
case <-stop:
637+
case <-ctx.Done():
630638
}
631639

632640
// Create a deadline to wait for.

cmd/signing-server/main_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto"
7+
"crypto/rsa"
8+
"crypto/x509"
9+
"encoding/hex"
10+
"encoding/pem"
11+
"fmt"
12+
"io"
13+
"log"
14+
"net/http"
15+
"os"
16+
"strings"
17+
"sync"
18+
"testing"
19+
"time"
20+
21+
"go.uber.org/zap"
22+
)
23+
24+
func TestSoftHSMConcurrentSignRequests(t *testing.T) {
25+
hsmModule := os.Getenv("HSM_MODULE")
26+
if hsmModule == "" {
27+
t.Skip("HSM_MODULE environment variable is not set")
28+
}
29+
tokenLabel := os.Getenv("TOKEN_LABEL")
30+
if tokenLabel == "" {
31+
t.Skip("TOKEN_LABEL environment variable is not set")
32+
}
33+
keyLabel := os.Getenv("KEY_LABEL")
34+
if keyLabel == "" {
35+
t.Skip("KEY_LABEL environment variable is not set")
36+
}
37+
hsmPin := os.Getenv("HSM_PIN")
38+
if hsmPin == "" {
39+
t.Skip("HSM_PIN environment variable is not set")
40+
}
41+
42+
const (
43+
base = "http://localhost:8080"
44+
healthURL = base + "/healthz"
45+
url = base + "/sign/rsassa-pss?hashAlgorithm=sha256"
46+
bodyHex = "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f"
47+
)
48+
49+
var (
50+
headers = map[string]string{
51+
"Content-Type": "text/plain",
52+
"Content-Encoding": "hex",
53+
"Accept": "application/x-pem-file",
54+
}
55+
)
56+
57+
l, err := zap.NewDevelopment()
58+
if err != nil {
59+
t.Fatalf("Failed to create logger: %v", err)
60+
}
61+
62+
ctx, cancel := context.WithCancel(t.Context())
63+
t.Cleanup(func() {
64+
cancel()
65+
})
66+
67+
go func() {
68+
if err := run(&Config{
69+
HSMModule: strings.TrimSpace(hsmModule),
70+
HSMTokenLabel: tokenLabel,
71+
HSMSlot: -1,
72+
HSMKeyLabel: keyLabel,
73+
HSMPass: hsmPin,
74+
Port: "8080",
75+
DisableHTTPS: true,
76+
DisableAuth: true,
77+
MaxBodySizeBytes: 2048,
78+
Logger: l,
79+
RunServer: true,
80+
}); err != nil {
81+
cancel()
82+
t.Errorf("Failed to start signing server: %v", err)
83+
return
84+
}
85+
}()
86+
87+
var wg sync.WaitGroup
88+
client := &http.Client{Timeout: 10 * time.Second}
89+
90+
outer:
91+
for {
92+
select {
93+
case <-ctx.Done():
94+
t.Fatal("Test context was cancelled before health check passed")
95+
default:
96+
log.Println("Waiting for signing server to be ready...")
97+
healthReq, err := http.NewRequest(http.MethodGet, healthURL, nil)
98+
if err != nil {
99+
t.Fatalf("Health request creation failed: %v", err)
100+
}
101+
healthResp, _ := client.Do(healthReq)
102+
if healthResp != nil && healthResp.StatusCode == http.StatusOK {
103+
log.Println("Health check passed")
104+
break outer
105+
}
106+
time.Sleep(500 * time.Millisecond)
107+
}
108+
}
109+
110+
for i := 0; i < 10; i++ {
111+
wg.Add(1)
112+
113+
go func(index int) {
114+
defer wg.Done()
115+
116+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(bodyHex))
117+
if err != nil {
118+
t.Errorf("Request %d creation failed: %v", index, err)
119+
return
120+
}
121+
122+
for k, v := range headers {
123+
req.Header.Set(k, v)
124+
}
125+
126+
resp, err := client.Do(req)
127+
if err != nil {
128+
t.Errorf("Request %d failed: %v", index, err)
129+
return
130+
}
131+
defer resp.Body.Close()
132+
133+
body, err := io.ReadAll(resp.Body)
134+
if err != nil {
135+
t.Errorf("Request %d read body failed: %v", index, err)
136+
return
137+
}
138+
if resp.StatusCode != http.StatusOK {
139+
t.Errorf("Request %d failed with status %d: %s", index, resp.StatusCode, body)
140+
return
141+
}
142+
143+
block, _ := pem.Decode(body)
144+
if block == nil || block.Type != "SIGNATURE" {
145+
log.Fatal("Failed to parse PEM block")
146+
}
147+
148+
hsmPublicKeyFile := os.Getenv("HSM_PUBLIC_KEY_FILE")
149+
if hsmPublicKeyFile == "" {
150+
log.Println("HSM_PUBLIC_KEY_FILE environment variable is not set, skipping public key verification")
151+
} else {
152+
hsmPublicKeyData, err := os.ReadFile(hsmPublicKeyFile)
153+
if err != nil {
154+
log.Fatalf("Failed to read public key file %s: %v", hsmPublicKeyFile, err)
155+
}
156+
publicKeyBlock, _ := pem.Decode(hsmPublicKeyData)
157+
if publicKeyBlock == nil || publicKeyBlock.Type != "PUBLIC KEY" {
158+
log.Fatal("Failed to parse PEM block for public key")
159+
}
160+
pub, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
161+
if err != nil {
162+
log.Fatalf("Failed to parse public key: %v", err)
163+
}
164+
rsaPubKey, ok := pub.(*rsa.PublicKey)
165+
if !ok {
166+
log.Fatal("Public key is not of type RSA")
167+
}
168+
rawDigest, err := hex.DecodeString(bodyHex)
169+
if err != nil {
170+
log.Fatalf("Failed to decode hex body: %v", err)
171+
}
172+
if err := rsa.VerifyPSS(rsaPubKey, crypto.SHA256, rawDigest, block.Bytes, nil); err != nil {
173+
log.Fatalf("Signature verification failed: %v", err)
174+
} else {
175+
log.Printf("Signature verification succeeded for request %d", index)
176+
}
177+
}
178+
179+
fmt.Printf("Decoded signature: %x\n", block.Bytes)
180+
181+
log.Printf("Response %d: %d - %.100q\n", index, resp.StatusCode, body)
182+
}(i)
183+
}
184+
185+
wg.Wait()
186+
}

pkg/crypto11/rsa.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ func (s *Session) SignPSS(key pkcs11.ObjectHandle, digest []byte, opts *rsa.PSSO
121121
ulongToBytes(mgf),
122122
ulongToBytes(sLen))
123123
mech := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_PSS, parameters)}
124+
if s.serialized {
125+
s.mu.Lock()
126+
defer s.mu.Unlock()
127+
}
124128
if err = s.Ctx.SignInit(s.Handle, mech, key); err != nil {
125129
return nil, err
126130
}
@@ -142,6 +146,10 @@ func (s *Session) SignPKCS1v15(key pkcs11.ObjectHandle, digest []byte, hash cryp
142146
copy(T[0:len(oid)], oid)
143147
copy(T[len(oid):], digest)
144148
mech := []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS, nil)}
149+
if s.serialized {
150+
s.mu.Lock()
151+
defer s.mu.Unlock()
152+
}
145153
err = s.Ctx.SignInit(s.Handle, mech, key)
146154
if err == nil {
147155
signature, err = s.Ctx.Sign(s.Handle, T)

0 commit comments

Comments
 (0)