Skip to content

Commit 1053ca8

Browse files
authored
feat: HMAC authentication for covenant emulator and covenant signer (#109)
* feat: HMAC authentication for covenant emulator and covenant signer * CHANGELOG * fix: move hmac to middleware and wrap docs to 80 characters * fix: client imports * fix: imports on tests * fix: imports in e2e tests * fix: imports
1 parent c46ee05 commit 1053ca8

File tree

17 files changed

+978
-23
lines changed

17 files changed

+978
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
3939

4040
### Improvements
4141

42+
* [#109](https://github.com/babylonlabs-io/covenant-emulator/pull/109) HMAC authentication between signer and emulator.
4243
* [#110](https://github.com/babylonlabs-io/covenant-emulator/pull/110) bump babylon to v1.0.0-rc.7
4344

4445
### Bug Fixes

config/remotesigner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ const (
1010
type RemoteSignerCfg struct {
1111
URL string `long:"url" description:"URL of the remote signer"`
1212
Timeout time.Duration `long:"timeout" description:"client when making requests to the remote signer"`
13+
HMACKey string `long:"hmackey" description:"HMAC key for authenticating requests to the remote signer - can be a direct key or reference to a cloud secret manager. Leave empty to disable HMAC authentication."`
1314
}
1415

1516
func DefaultRemoteSignerConfig() RemoteSignerCfg {
1617
return RemoteSignerCfg{
1718
URL: defaultUrl,
1819
Timeout: defaultTimeout,
20+
HMACKey: "",
1921
}
2022
}

covenant-signer/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ idle-timeout = {{ .Server.IdleTimeout }}
9494
# Max content length in bytes
9595
max-content-length = {{ .Server.MaxContentLength }}
9696
97+
# HMAC key for request authentication
98+
# This can be a direct key or a reference to a cloud secret manager
99+
# Leave empty to disable HMAC authentication
100+
hmac-key = "{{ .Server.HMACKey }}"
101+
97102
[metrics]
98103
# The prometheus server host
99104
host = "{{ .Metrics.Host }}"

covenant-signer/config/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type ServerConfig struct {
99
ReadTimeout uint32 `mapstructure:"read-timeout"`
1010
IdleTimeout uint32 `mapstructure:"idle-timeout"`
1111
MaxContentLength uint32 `mapstructure:"max-content-length"`
12+
HMACKey string `mapstructure:"hmac-key"`
1213
}
1314

1415
type ParsedServerConfig struct {
@@ -18,6 +19,7 @@ type ParsedServerConfig struct {
1819
ReadTimeout time.Duration
1920
IdleTimeout time.Duration
2021
MaxContentLength uint32
22+
HMACKey string
2123
}
2224

2325
func (c *ServerConfig) Parse() (*ParsedServerConfig, error) {
@@ -29,6 +31,7 @@ func (c *ServerConfig) Parse() (*ParsedServerConfig, error) {
2931
ReadTimeout: time.Duration(c.ReadTimeout) * time.Second,
3032
IdleTimeout: time.Duration(c.IdleTimeout) * time.Second,
3133
MaxContentLength: c.MaxContentLength,
34+
HMACKey: c.HMACKey,
3235
}, nil
3336
}
3437

@@ -40,5 +43,6 @@ func DefaultServerConfig() *ServerConfig {
4043
ReadTimeout: 15,
4144
IdleTimeout: 120,
4245
MaxContentLength: 10 * 1024 * 1024, // 10MB,
46+
HMACKey: "",
4347
}
4448
}

covenant-signer/itest/e2e_test.go

Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerapp"
3030
"github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerservice"
31+
"github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerservice/middlewares"
3132
"github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerservice/types"
3233
)
3334

@@ -130,24 +131,24 @@ func buildDataToSign(t *testing.T, covnenantPublicKey *btcec.PublicKey) signerap
130131
}
131132

132133
func TestGetPublicKey(t *testing.T) {
133-
tm := StartManager(t, 100, false)
134+
tm := StartManager(t, 100, false, "")
134135
// default passphrase is empty in non encrypted keyring
135-
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "")
136+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", "")
136137
require.NoError(t, err)
137138

138-
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second)
139+
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, tm.hmacKey)
139140
require.NoError(t, err)
140141
require.NotNil(t, pubKey)
141142

142143
}
143144

144145
func TestSigningTransactions(t *testing.T) {
145-
tm := StartManager(t, 100, false)
146+
tm := StartManager(t, 100, false, "")
146147
// default passphrase is empty in non encrypted keyring
147-
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "")
148+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", "")
148149
require.NoError(t, err)
149150

150-
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second)
151+
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, tm.hmacKey)
151152
require.NoError(t, err)
152153
require.NotNil(t, pubKey)
153154

@@ -158,6 +159,7 @@ func TestSigningTransactions(t *testing.T) {
158159
tm.SigningServerUrl(),
159160
10*time.Second,
160161
&dataToSign,
162+
"",
161163
)
162164

163165
require.NoError(t, err)
@@ -168,7 +170,7 @@ func TestSigningTransactions(t *testing.T) {
168170
}
169171

170172
func TestRejectToLargeRequest(t *testing.T) {
171-
tm := StartManager(t, 100, false)
173+
tm := StartManager(t, 100, false, "")
172174
r := rand.New(rand.NewSource(time.Now().UnixNano()))
173175
tmContentLimit := tm.signerConfig.Server.MaxContentLength
174176
size := tmContentLimit + 1
@@ -200,12 +202,12 @@ func TestRejectToLargeRequest(t *testing.T) {
200202
}
201203

202204
func TestSigningTransactionsUsingEncryptedFileKeyRing(t *testing.T) {
203-
tm := StartManager(t, 100, true)
205+
tm := StartManager(t, 100, true, "")
204206

205-
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "testtest")
207+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "testtest", "")
206208
require.NoError(t, err)
207209

208-
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second)
210+
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, tm.hmacKey)
209211
require.NoError(t, err)
210212
require.NotNil(t, pubKey)
211213

@@ -216,6 +218,7 @@ func TestSigningTransactionsUsingEncryptedFileKeyRing(t *testing.T) {
216218
tm.SigningServerUrl(),
217219
10*time.Second,
218220
&dataToSign,
221+
"",
219222
)
220223

221224
require.NoError(t, err)
@@ -226,19 +229,19 @@ func TestSigningTransactionsUsingEncryptedFileKeyRing(t *testing.T) {
226229
}
227230

228231
func TestLockingKeyring(t *testing.T) {
229-
tm := StartManager(t, 100, true)
232+
tm := StartManager(t, 100, true, "")
230233

231-
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "testtest")
234+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "testtest", "")
232235
require.NoError(t, err)
233236

234-
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second)
237+
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, tm.hmacKey)
235238
require.NoError(t, err)
236239
require.NotNil(t, pubKey)
237240

238241
dataToSign := buildDataToSign(t, pubKey)
239242

240243
// lock the keyring, and clear the private key from memory
241-
err = signerservice.Lock(context.Background(), tm.SigningServerUrl(), 10*time.Second)
244+
err = signerservice.Lock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "")
242245
require.NoError(t, err)
243246

244247
// try to sign a transaction with a locked keyring, it should fail
@@ -247,9 +250,164 @@ func TestLockingKeyring(t *testing.T) {
247250
tm.SigningServerUrl(),
248251
10*time.Second,
249252
&dataToSign,
253+
"",
250254
)
251255

252256
require.Error(t, err)
253257
require.Nil(t, sigs)
258+
}
259+
260+
func TestHMACAuthentication(t *testing.T) {
261+
// Test with valid HMAC key
262+
testHMACKey := "test-hmac-key-for-authentication"
263+
tm := StartManager(t, 100, false, testHMACKey)
264+
265+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", testHMACKey)
266+
require.NoError(t, err, "Unlock should succeed with valid HMAC key")
267+
268+
pubKey, err := signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, testHMACKey)
269+
require.NoError(t, err)
270+
require.NotNil(t, pubKey)
271+
272+
dataToSign := buildDataToSign(t, pubKey)
273+
274+
sigs, err := signerservice.RequestCovenantSignaure(
275+
context.Background(),
276+
tm.SigningServerUrl(),
277+
10*time.Second,
278+
&dataToSign,
279+
testHMACKey,
280+
)
281+
require.NoError(t, err, "Signing should succeed with valid HMAC key")
282+
require.NotNil(t, sigs)
283+
284+
_, err = signerservice.RequestCovenantSignaure(
285+
context.Background(),
286+
tm.SigningServerUrl(),
287+
10*time.Second,
288+
&dataToSign,
289+
"invalid-hmac-key",
290+
)
291+
require.Error(t, err, "Signing should fail with invalid HMAC key")
292+
require.Contains(t, err.Error(), "401", "Error should be a 401 Unauthorized")
293+
294+
err = signerservice.Lock(context.Background(), tm.SigningServerUrl(), 10*time.Second, testHMACKey)
295+
require.NoError(t, err, "Lock should succeed with valid HMAC key")
296+
297+
err = signerservice.Lock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "invalid-hmac-key")
298+
require.Error(t, err, "Lock should fail with invalid HMAC key")
299+
require.Contains(t, err.Error(), "401", "Error should be a 401 Unauthorized")
300+
}
301+
302+
func TestHMACDirectRequest(t *testing.T) {
303+
testHMACKey := "test-hmac-key-for-direct-request"
304+
tm := StartManager(t, 100, false, testHMACKey)
305+
306+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", testHMACKey)
307+
require.NoError(t, err)
308+
309+
body := []byte(`{"passphrase":""}`)
310+
route := fmt.Sprintf("%s/v1/unlock", tm.SigningServerUrl())
311+
312+
hmacValue, err := middlewares.GenerateHMAC(testHMACKey, body)
313+
require.NoError(t, err)
314+
315+
httpRequest, err := http.NewRequestWithContext(context.Background(), "POST", route, bytes.NewReader(body))
316+
require.NoError(t, err)
317+
httpRequest.Header.Set("Content-Type", "application/json")
318+
httpRequest.Header.Set(middlewares.HeaderCovenantHMAC, hmacValue)
319+
320+
client := http.Client{Timeout: 10 * time.Second}
321+
res, err := client.Do(httpRequest)
322+
require.NoError(t, err)
323+
defer res.Body.Close()
324+
require.Equal(t, http.StatusOK, res.StatusCode, "Request with valid HMAC should succeed")
325+
326+
httpRequest, err = http.NewRequestWithContext(context.Background(), "POST", route, bytes.NewReader(body))
327+
require.NoError(t, err)
328+
httpRequest.Header.Set("Content-Type", "application/json")
329+
httpRequest.Header.Set(middlewares.HeaderCovenantHMAC, "invalidhmacvalue")
330+
331+
res, err = client.Do(httpRequest)
332+
require.NoError(t, err)
333+
defer res.Body.Close()
334+
require.Equal(t, http.StatusUnauthorized, res.StatusCode, "Request with invalid HMAC should fail with 401")
335+
336+
httpRequest, err = http.NewRequestWithContext(context.Background(), "POST", route, bytes.NewReader(body))
337+
require.NoError(t, err)
338+
httpRequest.Header.Set("Content-Type", "application/json")
339+
340+
res, err = client.Do(httpRequest)
341+
require.NoError(t, err)
342+
defer res.Body.Close()
343+
require.Equal(t, http.StatusUnauthorized, res.StatusCode, "Request with missing HMAC should fail with 401")
344+
345+
pkRoute := fmt.Sprintf("%s/v1/public-key", tm.SigningServerUrl())
254346

347+
httpRequest, err = http.NewRequestWithContext(context.Background(), "GET", pkRoute, nil)
348+
require.NoError(t, err)
349+
350+
res, err = client.Do(httpRequest)
351+
require.NoError(t, err)
352+
defer res.Body.Close()
353+
require.Equal(t, http.StatusUnauthorized, res.StatusCode, "Public key request without HMAC should fail with 401")
354+
355+
emptyBody := []byte{}
356+
hmacValue, err = middlewares.GenerateHMAC(testHMACKey, emptyBody)
357+
require.NoError(t, err)
358+
359+
httpRequest, err = http.NewRequestWithContext(context.Background(), "GET", pkRoute, nil)
360+
require.NoError(t, err)
361+
httpRequest.Header.Set(middlewares.HeaderCovenantHMAC, hmacValue)
362+
363+
res, err = client.Do(httpRequest)
364+
require.NoError(t, err)
365+
defer res.Body.Close()
366+
require.Equal(t, http.StatusOK, res.StatusCode, "Public key request with valid HMAC should succeed")
367+
368+
var respData map[string]interface{}
369+
err = json.NewDecoder(res.Body).Decode(&respData)
370+
require.NoError(t, err, "Should receive valid JSON response")
371+
372+
dataObj, exists := respData["data"]
373+
require.True(t, exists, "Response should contain a 'data' object")
374+
375+
dataMap, ok := dataObj.(map[string]interface{})
376+
require.True(t, ok, "Data should be an object")
377+
378+
_, exists = dataMap["public_key_hex"]
379+
require.True(t, exists, "Response should contain a public key")
380+
}
381+
382+
func TestHMACMismatchedKeys(t *testing.T) {
383+
// Test scenario where client and server have different HMAC keys
384+
serverKey := "server-hmac-key"
385+
clientKey := "different-client-hmac-key"
386+
387+
tm := StartManager(t, 100, false, serverKey)
388+
389+
err := signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", clientKey)
390+
require.Error(t, err, "Unlock should fail with mismatched HMAC keys")
391+
require.Contains(t, err.Error(), "401", "Error should be a 401 Unauthorized")
392+
393+
_, err = signerservice.GetPublicKey(context.Background(), tm.SigningServerUrl(), 10*time.Second, clientKey)
394+
require.Error(t, err, "GetPublicKey should require HMAC")
395+
require.Contains(t, err.Error(), "401", "Error should be a 401 Unauthorized")
396+
397+
err = signerservice.Unlock(context.Background(), tm.SigningServerUrl(), 10*time.Second, "", serverKey)
398+
require.NoError(t, err, "Unlock should succeed with matching HMAC key")
399+
400+
route := fmt.Sprintf("%s/v1/public-key", tm.SigningServerUrl())
401+
httpRequest, err := http.NewRequestWithContext(context.Background(), "GET", route, nil)
402+
require.NoError(t, err)
403+
404+
hmacValue, err := middlewares.GenerateHMAC(serverKey, []byte{})
405+
require.NoError(t, err)
406+
httpRequest.Header.Set(middlewares.HeaderCovenantHMAC, hmacValue)
407+
408+
client := http.Client{Timeout: 10 * time.Second}
409+
res, err := client.Do(httpRequest)
410+
require.NoError(t, err)
411+
defer res.Body.Close()
412+
require.Equal(t, http.StatusOK, res.StatusCode, "Request with valid HMAC should succeed")
255413
}

covenant-signer/itest/testmanager.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ type TestManager struct {
3535
signerConfig *config.Config
3636
app *signerapp.SignerApp
3737
server *signerservice.SigningServer
38+
hmacKey string
3839
}
3940

4041
func StartManager(
4142
t *testing.T,
4243
numMatureOutputsInWallet uint32,
4344
useEncryptedFileKeyRing bool,
45+
hmacKey string,
4446
) *TestManager {
4547
m, err := containers.NewManager()
4648
require.NoError(t, err)
@@ -58,6 +60,11 @@ func StartManager(
5860

5961
appConfig := config.DefaultConfig()
6062

63+
// Configure HMAC key if provided
64+
if hmacKey != "" {
65+
appConfig.Server.HMACKey = hmacKey
66+
}
67+
6168
var retriever *cosmos.CosmosKeyringRetriever
6269

6370
if useEncryptedFileKeyRing {
@@ -129,6 +136,7 @@ func StartManager(
129136
signerConfig: appConfig,
130137
app: app,
131138
server: server,
139+
hmacKey: hmacKey,
132140
}
133141
}
134142

0 commit comments

Comments
 (0)