Building a Keychain service with Warden Protocol
Overview
The Keychain service is a crucial component in the Warden Protocol ecosystem. Keychains are responsible for generating cryptographic keys, securely storing them, and signing transactions. To learn how Keychains process key and signature requests, see Request flow.
This tutorial explains how to build a Keychain application in Go using the Keychain SDK. We're also going to test the application using mock key and sign requests.
Note that in a production environment, you'd need to implement actual logic for generating keys and signing transactions, integrate with a secure key storage solution, and add more robust error handling and security measures.
Prerequisites
Go
1.23 or later
Setting up the project
-
Create a new directory for your project:
mkdir warden-keychain-service && cd warden-keychain-service
-
Initialize a new Go module:
go mod init warden-keychain-service
-
Install the required dependencies:
go get github.com/warden-protocol/wardenprotocol/keychain-sdk
go get github.com/stretchr/testify
Creating the main application
-
Create a new file named
main.go
in your project directory and open it in your preferred text editor. -
Add the following code to
main.go
with the skeleton:package main
import (
"context"
"log/slog"
"os"
"time"
"github.com/warden-protocol/wardenprotocol/keychain-sdk"
)
func main() {
// Set up a logger for debugging
// Create a new Keychain application
}
// Set up handlers for key requests and sign requests
app.SetKeyRequestHandler(handleKeyRequest)
app.SetSignRequestHandler(handleSignRequest)
// Start the application
// handleKeyRequest processes incoming key requests
func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
}
// handleSignRequest processes incoming sign requests
func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) {
}Let's first define
handleKeyRequest
function. This function takes in aKeyResponseWriter
and aKeyRequest
as parameters.Inside the function, let us create a logger usingslog.Default()
and log informational messages with the request ID and key type.Note: This function will create a dummy public key as a byte slice.
Finally, let us call the
Fulfil
method on theKeyResponseWriter
with the dummy public key. If there is an error, it logs an error message and calls the Reject method on theKeyResponseWriter
with an error message.// handleKeyRequest processes incoming key requests
func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
logger := slog.Default()
logger.Info("received a key request", "id", req.Id, "key_type", req.KeyType)
// In a real application, you would generate a public key here
// For this example, we'll use a dummy public key
publicKey := []byte("dummy_public_key")
if err := w.Fulfil(publicKey); err != nil {
logger.Error("failed to fulfill the key request", "error", err)
if err := w.Reject("Internal error"); err != nil {
logger.Error("failed to reject the key request", "error", err)
}
}
}Next, let us define
Go
function calledhandleSignRequest
that takes in aSignResponseWriter
and aSignRequest
as parameters. It logs the received sign request and then generates a dummy signature. If theFulfil
method of theSignResponseWriter
returns an error, it logs the error and attempts to reject the sign request.func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) {
logger := slog.Default()
logger.Info("received a sign request", "id", req.Id, "key_id", req.KeyId)
// In a real application, you would sign the data here
// For this example, we'll use a dummy signature
signature := []byte("dummy_signature")
if err := w.Fulfil(signature); err != nil {
logger.Error("failed to fulfill the sign request", "error", err)
if err := w.Reject("Internal error"); err != nil {
logger.Error("failed to reject the sign request", "error", err)
}
}
}OK! Now since our main logic is implemented, let us write the complete
main.go
package main
import (
"context"
"log/slog"
"os"
"time"
"github.com/warden-protocol/wardenprotocol/keychain-sdk"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
app := keychain.NewApp(keychain.Config{
Logger: logger,
ChainID: "warden",
GRPCURL: "localhost:9090",
GRPCInsecure: true,
KeychainID: 1,
Mnemonic: "zebra future seed foil jungle eyebrow rubber spatial measure auction unveil blue toy good lift audit truth obvious voyage inspire gold rule year canyon",
DerivationPath: "m/44'/118'/0'/0/0",
GasLimit: 400000,
BatchInterval: 8 * time.Second,
BatchSize: 10,
})
app.SetKeyRequestHandler(handleKeyRequest)
app.SetSignRequestHandler(handleSignRequest)
if err := app.Start(context.Background()); err != nil {
logger.Error("application error", "error", err)
os.Exit(1)
}
}
func handleKeyRequest(w keychain.KeyResponseWriter, req *keychain.KeyRequest) {
logger := slog.Default()
logger.Info("received key request", "id", req.Id, "key_type", req.KeyType)
// In a real application, you would generate a public key here
// For this example, we'll use a dummy public key
publicKey := []byte("dummy_public_key")
if err := w.Fulfil(publicKey); err != nil {
logger.Error("failed to fulfill key request", "error", err)
if err := w.Reject("Internal error"); err != nil {
logger.Error("failed to reject key request", "error", err)
}
}
}
func handleSignRequest(w keychain.SignResponseWriter, req *keychain.SignRequest) {
logger := slog.Default()
logger.Info("received sign request", "id", req.Id, "key_id", req.KeyId)
// In a real application, you would sign the data here
// For this example, we'll use a dummy signature
signature := []byte("dummy_signature")
if err := w.Fulfil(signature); err != nil {
logger.Error("failed to fulfill sign request", "error", err)
if err := w.Reject("Internal error"); err != nil {
logger.Error("failed to reject sign request", "error", err)
}
}
}
Creating a test
Now, let us write a test to test our previously written function.
-
Create a new file named
keychain_test.go
in your project directory and open it in your text editor. -
Add the following code to
keychain_test.go
:package main
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/warden-protocol/wardenprotocol/keychain-sdk"
wardentypes "github.com/warden-protocol/wardenprotocol/warden/x/warden/types/v1beta3"
)
// Mock implementation of KeyResponseWriter
type mockKeyResponseWriter struct {
fulfilled bool
rejected bool
publicKey []byte
reason string
}
func (m *mockKeyResponseWriter) Fulfil(publicKey []byte) error {
m.fulfilled = true
m.publicKey = publicKey
return nil
}
func (m *mockKeyResponseWriter) Reject(reason string) error {
m.rejected = true
m.reason = reason
return nil
}
// Mock implementation of SignResponseWriter
type mockSignResponseWriter struct {
fulfilled bool
rejected bool
signature []byte
reason string
}
func (m *mockSignResponseWriter) Fulfil(signature []byte) error {
m.fulfilled = true
m.signature = signature
return nil
}
func (m *mockSignResponseWriter) Reject(reason string) error {
m.rejected = true
m.reason = reason
return nil
}
// TestKeychain is the main test function
func TestKeychain(t *testing.T) {
// Set up the Keychain app
app := setupKeychainApp(t)
// Start the app in a goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errChan := make(chan error, 1)
go func() {
if err := app.Start(ctx); err != nil {
errChan <- err
}
}()
// Give the app some time to start
select {
case err := <-errChan:
t.Fatalf("Keychain app error: %v", err)
case <-time.After(10 * time.Second):
t.Log("Keychain app started successfully")
}
t.Run("TestKeyRequest", func(t *testing.T) {
testKeyRequest(t)
})
t.Run("TestSignRequest", func(t *testing.T) {
testSignRequest(t)
})
}
// setupKeychainApp creates and configures a new keychain app for testing
func setupKeychainApp(t *testing.T) *keychain.App {
mnemonic := "zebra future seed foil jungle eyebrow rubber spatial measure auction unveil blue toy good lift audit truth obvious voyage inspire gold rule year canyon"
app := keychain.NewApp(keychain.Config{
ChainID: "warden",
GRPCURL: "localhost:9090",
GRPCInsecure: true,
KeychainID: 1,
Mnemonic: mnemonic,
DerivationPath: "m/44'/118'/0'/0/0",
GasLimit: 400000,
BatchInterval: 8 * time.Second,
BatchSize: 10,
})
t.Logf("Setting up the Keychain app with mnemonic: %s", mnemonic)
return app
}
// testKeyRequest tests the key request handling
func testKeyRequest(t *testing.T) {
// Create a new key request
keyRequest := &keychain.KeyRequest{
Id: 1,
SpaceId: 1,
KeychainId: 1,
KeyType: wardentypes.KeyType_KEY_TYPE_ECDSA_SECP256K1,
RuleId: 1,
}
writer := &mockKeyResponseWriter{}
handleKeyRequest(writer, keyRequest)
assert.True(t, writer.fulfilled)
assert.NotEmpty(t, writer.publicKey)
}
// testSignRequest tests the sign request handling
func testSignRequest(t *testing.T) {
// Create a new sign request
signRequest := &keychain.SignRequest{
Id: 1,
KeyId: 1,
DataForSigning: []byte("test data to sign"),
EncryptionKey: []byte("test encryption key"),
}
writer := &mockSignResponseWriter{}
handleSignRequest(writer, signRequest)
assert.True(t, writer.fulfilled)
assert.NotEmpty(t, writer.signature)
}
Here is a brief explanation of the keychain_test.go
code:
- We define mock implementations of
KeyResponseWriter
andSignResponseWriter
for testing purposes. - The
TestKeychain
function is the main test function. It sets up the keychain app, starts it in a goroutine, and runs two subtests. setupKeychainApp
creates a new Keychain app with the same configuration as inmain.go
.testKeyRequest
creates a mock key request, calls thehandleKeyRequest
function, and asserts that the response is as expected.testSignRequest
does the same for sign requests.
Running tests
To run the tests:
-
Open a terminal and navigate to your project directory.
-
Run the following command:
go test -v
-
You should see output indicating that the tests have run and passed:
RUN TestKeychain
RUN TestKeychain/TestKeyRequest
2024/08/21 18:11:14 INFO received key request id=1 key_type=KEY_TYPE_ECDSA_SECP256K1
RUN TestKeychain/TestSignRequest
2024/08/21 18:11:14 INFO received sign request id=1 key_id=1
PASS: TestKeychain (2.00s)
PASS: TestKeychain/TestKeyRequest (0.00s)
PASS: TestKeychain/TestSignRequest (0.00s)
PASS
ok keychain-sdk 2.990s
Conclusion
This tutorial has walked you through creating a basic Keychain service using the Warden Protocol. You've set up the main application, implemented placeholder handlers for key and sign requests, and created tests to verify the basic functionality.
Happy coding! 🚀