Skip to content

Commit a649bf7

Browse files
test(signer): add unit tests for signserver package
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent c06937f commit a649bf7

1 file changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package signserver
17+
18+
import (
19+
"crypto/ecdsa"
20+
"crypto/elliptic"
21+
"crypto/rand"
22+
"crypto/x509"
23+
"crypto/x509/pkix"
24+
"encoding/pem"
25+
"io"
26+
"math/big"
27+
"net/http"
28+
"net/http/httptest"
29+
"os"
30+
"strings"
31+
"testing"
32+
"time"
33+
34+
"github.com/stretchr/testify/assert"
35+
"github.com/stretchr/testify/require"
36+
)
37+
38+
// --- helpers -----------------------------------------------------------------
39+
40+
func generateSelfSignedCertPEM(t *testing.T) []byte {
41+
t.Helper()
42+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
43+
require.NoError(t, err)
44+
template := &x509.Certificate{
45+
SerialNumber: big.NewInt(1),
46+
Subject: pkix.Name{CommonName: "test"},
47+
NotBefore: time.Now().Add(-time.Minute),
48+
NotAfter: time.Now().Add(time.Hour),
49+
IsCA: true,
50+
}
51+
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
52+
require.NoError(t, err)
53+
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
54+
}
55+
56+
func generatePrivateKeyPEM(t *testing.T) []byte {
57+
t.Helper()
58+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
59+
require.NoError(t, err)
60+
der, err := x509.MarshalPKCS8PrivateKey(key)
61+
require.NoError(t, err)
62+
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
63+
}
64+
65+
// generateEncryptedPrivateKeyPEM creates a PEM-encrypted private key (RFC1423/legacy format).
66+
//
67+
//nolint:staticcheck
68+
func generateEncryptedPrivateKeyPEM(t *testing.T, password string) []byte {
69+
t.Helper()
70+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
71+
require.NoError(t, err)
72+
der, err := x509.MarshalPKCS8PrivateKey(key)
73+
require.NoError(t, err)
74+
// x509.EncryptPEMBlock is deprecated but remains the standard way to test
75+
// the RFC1423 decryption path exercised by privateKeyFromPem.
76+
block, err := x509.EncryptPEMBlock(rand.Reader, "PRIVATE KEY", der, []byte(password), x509.PEMCipherAES128) //nolint:staticcheck
77+
require.NoError(t, err)
78+
return pem.EncodeToMemory(block)
79+
}
80+
81+
// writePEMToTempFile writes a raw PEM block to a temp file and returns its path.
82+
func writePEMToTempFile(t *testing.T, blockType string, der []byte) string {
83+
t.Helper()
84+
f, err := os.CreateTemp(t.TempDir(), "*.pem")
85+
require.NoError(t, err)
86+
defer f.Close()
87+
err = pem.Encode(f, &pem.Block{Type: blockType, Bytes: der})
88+
require.NoError(t, err)
89+
return f.Name()
90+
}
91+
92+
// tlsServerCAFile starts a TLS httptest server and returns it together with a
93+
// temp-file path containing its CA certificate. Caller is responsible for
94+
// calling srv.Close().
95+
func tlsServerWithCA(t *testing.T, handler http.Handler) (*httptest.Server, string) {
96+
t.Helper()
97+
srv := httptest.NewTLSServer(handler)
98+
caPath := writePEMToTempFile(t, "CERTIFICATE", srv.TLS.Certificates[0].Certificate[0])
99+
return srv, caPath
100+
}
101+
102+
// --- TestParseKeyReference ---------------------------------------------------
103+
104+
func TestParseKeyReference(t *testing.T) {
105+
tests := []struct {
106+
name string
107+
input string
108+
wantHost string
109+
wantWorker string
110+
wantErr bool
111+
}{
112+
{
113+
name: "valid reference",
114+
input: "signserver://myhost.com/CMSSigner",
115+
wantHost: "myhost.com",
116+
wantWorker: "CMSSigner",
117+
},
118+
{
119+
name: "valid reference with port",
120+
input: "signserver://myhost.com:8443/PlainSigner",
121+
wantHost: "myhost.com:8443",
122+
wantWorker: "PlainSigner",
123+
},
124+
{
125+
name: "missing worker segment",
126+
input: "signserver://myhost.com",
127+
wantErr: true,
128+
},
129+
{
130+
name: "too many path segments",
131+
input: "signserver://myhost.com/worker/extra",
132+
wantErr: true,
133+
},
134+
{
135+
name: "no scheme separator",
136+
input: "noscheme",
137+
wantErr: true,
138+
},
139+
{
140+
// ParseKeyReference only validates structure, not the scheme prefix.
141+
// Scheme enforcement (signserver://) is the caller's responsibility (GetSigner).
142+
name: "non-signserver scheme parses successfully",
143+
input: "https://myhost.com/worker",
144+
wantHost: "myhost.com",
145+
wantWorker: "worker",
146+
},
147+
}
148+
149+
for _, tc := range tests {
150+
t.Run(tc.name, func(t *testing.T) {
151+
host, worker, err := ParseKeyReference(tc.input)
152+
if tc.wantErr {
153+
require.Error(t, err)
154+
return
155+
}
156+
require.NoError(t, err)
157+
assert.Equal(t, tc.wantHost, host)
158+
assert.Equal(t, tc.wantWorker, worker)
159+
})
160+
}
161+
}
162+
163+
// --- TestNewSigner_WithOptions ------------------------------------------------
164+
165+
func TestNewSigner_WithOptions(t *testing.T) {
166+
s := NewSigner("myhost.com", "CMSSigner",
167+
WithCAPath("/ca.pem"),
168+
WithClientCertPath("/cert.pem"),
169+
WithClientCertPass("secret"),
170+
)
171+
assert.Equal(t, "myhost.com", s.host)
172+
assert.Equal(t, "CMSSigner", s.worker)
173+
assert.Equal(t, "/ca.pem", s.caPath)
174+
assert.Equal(t, "/cert.pem", s.clientCertPath)
175+
assert.Equal(t, "secret", s.clientCertPass)
176+
}
177+
178+
func TestNewSigner_Defaults(t *testing.T) {
179+
s := NewSigner("host", "worker")
180+
assert.Empty(t, s.caPath)
181+
assert.Empty(t, s.clientCertPath)
182+
assert.Empty(t, s.clientCertPass)
183+
}
184+
185+
// --- TestPublicKey -----------------------------------------------------------
186+
187+
func TestPublicKey_NotSupported(t *testing.T) {
188+
s := NewSigner("host", "worker")
189+
key, err := s.PublicKey()
190+
assert.Error(t, err)
191+
assert.Nil(t, key)
192+
}
193+
194+
// --- TestSignMessage ---------------------------------------------------------
195+
196+
func TestSignMessage_Success(t *testing.T) {
197+
var gotWorker string
198+
var gotFileName string
199+
var gotFileContent []byte
200+
201+
srv, caPath := tlsServerWithCA(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
202+
require.NoError(t, r.ParseMultipartForm(1<<20))
203+
gotWorker = r.FormValue("workerName")
204+
f, fh, err := r.FormFile("file")
205+
require.NoError(t, err)
206+
gotFileName = fh.Filename
207+
gotFileContent, _ = io.ReadAll(f)
208+
w.WriteHeader(http.StatusOK)
209+
_, _ = w.Write([]byte("fakesignature"))
210+
}))
211+
defer srv.Close()
212+
213+
host := strings.TrimPrefix(srv.URL, "https://")
214+
s := NewSigner(host, "CMSSigner", WithCAPath(caPath))
215+
216+
sig, err := s.SignMessage(strings.NewReader("hello attestation"))
217+
require.NoError(t, err)
218+
assert.Equal(t, []byte("fakesignature"), sig)
219+
assert.Equal(t, "CMSSigner", gotWorker)
220+
assert.Equal(t, "attestation.json", gotFileName)
221+
assert.Equal(t, []byte("hello attestation"), gotFileContent)
222+
}
223+
224+
func TestSignMessage_Non200Status(t *testing.T) {
225+
srv, caPath := tlsServerWithCA(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
226+
w.WriteHeader(http.StatusInternalServerError)
227+
}))
228+
defer srv.Close()
229+
230+
host := strings.TrimPrefix(srv.URL, "https://")
231+
s := NewSigner(host, "CMSSigner", WithCAPath(caPath))
232+
233+
_, err := s.SignMessage(strings.NewReader("data"))
234+
require.Error(t, err)
235+
assert.Contains(t, err.Error(), "500")
236+
}
237+
238+
func TestSignMessage_NetworkError(t *testing.T) {
239+
srv, caPath := tlsServerWithCA(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
240+
host := strings.TrimPrefix(srv.URL, "https://")
241+
srv.Close() // closed before the call
242+
243+
s := NewSigner(host, "CMSSigner", WithCAPath(caPath))
244+
_, err := s.SignMessage(strings.NewReader("data"))
245+
require.Error(t, err)
246+
assert.Contains(t, err.Error(), "failed to send request")
247+
}
248+
249+
func TestSignMessage_InvalidCAPath(t *testing.T) {
250+
s := NewSigner("host", "worker", WithCAPath("/nonexistent/ca.pem"))
251+
_, err := s.SignMessage(strings.NewReader("data"))
252+
require.Error(t, err)
253+
assert.Contains(t, err.Error(), "failed to read ca cert")
254+
}
255+
256+
// --- TestCertFromPem ---------------------------------------------------------
257+
258+
func TestCertFromPem(t *testing.T) {
259+
cert1 := generateSelfSignedCertPEM(t)
260+
cert2 := generateSelfSignedCertPEM(t)
261+
keyOnly := generatePrivateKeyPEM(t)
262+
263+
tests := []struct {
264+
name string
265+
input []byte
266+
wantCerts int
267+
}{
268+
{"single certificate", cert1, 1},
269+
{"two certificates", append(cert1, cert2...), 2},
270+
{"empty input", []byte{}, 0},
271+
{"key block only — no certs extracted", keyOnly, 0},
272+
{"cert followed by key", append(cert1, keyOnly...), 1},
273+
}
274+
275+
for _, tc := range tests {
276+
t.Run(tc.name, func(t *testing.T) {
277+
result, err := certFromPem(tc.input)
278+
require.NoError(t, err)
279+
assert.Len(t, result.Certificate, tc.wantCerts)
280+
})
281+
}
282+
}
283+
284+
// --- TestPrivateKeyFromPem ---------------------------------------------------
285+
286+
func TestPrivateKeyFromPem(t *testing.T) {
287+
tests := []struct {
288+
name string
289+
pemBytes func(t *testing.T) []byte
290+
password string
291+
wantErr bool
292+
}{
293+
{
294+
name: "unencrypted PKCS8 key with empty password",
295+
pemBytes: func(t *testing.T) []byte { return generatePrivateKeyPEM(t) },
296+
password: "",
297+
},
298+
{
299+
name: "RFC1423 encrypted key with correct password",
300+
pemBytes: func(t *testing.T) []byte { return generateEncryptedPrivateKeyPEM(t, "correct") },
301+
password: "correct",
302+
},
303+
{
304+
name: "RFC1423 encrypted key with wrong password",
305+
pemBytes: func(t *testing.T) []byte { return generateEncryptedPrivateKeyPEM(t, "correct") },
306+
password: "wrong",
307+
wantErr: true,
308+
},
309+
{
310+
name: "no PEM data at all",
311+
pemBytes: func(_ *testing.T) []byte { return []byte("not-pem-data") },
312+
password: "",
313+
wantErr: true,
314+
},
315+
{
316+
name: "certificate block only — no key",
317+
pemBytes: func(t *testing.T) []byte { return generateSelfSignedCertPEM(t) },
318+
password: "",
319+
wantErr: true,
320+
},
321+
}
322+
323+
for _, tc := range tests {
324+
t.Run(tc.name, func(t *testing.T) {
325+
key, err := privateKeyFromPem(tc.pemBytes(t), tc.password)
326+
if tc.wantErr {
327+
require.Error(t, err)
328+
return
329+
}
330+
require.NoError(t, err)
331+
assert.NotNil(t, key)
332+
_, ok := key.(*ecdsa.PrivateKey)
333+
assert.True(t, ok, "expected *ecdsa.PrivateKey")
334+
})
335+
}
336+
}

0 commit comments

Comments
 (0)