HashiCorp Vault Signer
The Vault signer (github.com/smartcontractkit/crec-sdk/transact/signer/vault) signs CRE Connect payloads using HashiCorp Vault's Transit secrets engine. Vault holds the key (optionally backed by an HSM) and signs digests on request.
Key types supported
| Constant | Algorithm |
|---|---|
vault.KeyTypeRSA2048 | RSA, 2048-bit |
vault.KeyTypeRSA4096 | RSA, 4096-bit |
vault.KeyTypeECDSAP256 | ECDSA on P-256 |
vault.KeyTypeECDSAP384 | ECDSA on P-384 |
vault.KeyTypeECDSAP521 | ECDSA on P-521 |
Prerequisites
- Vault cluster reachable from your service.
- Transit engine mounted (default mount
transit). - A Vault token with policies that allow
update transit/sign/<key>andread transit/keys/<key>.
Create a key in Vault
You can create the key with the Vault CLI:
vault write transit/keys/treasury-prod-eth type=rsa-2048
Or directly through the Go SDK:
import "github.com/smartcontractkit/crec-sdk/transact/signer/vault"
result, err := vault.CreateKeyInVault(
"https://vault.example.com:8200",
os.Getenv("VAULT_TOKEN"),
"transit",
"treasury-prod-eth",
vault.KeyTypeRSA2048,
)
if err != nil { return err }
fmt.Println("modulus (hex):", result.Modulus, "exponent (hex):", "010001") // RSA exponent in Vault is 65537
Save the (modulus, exponent) pair: you'll feed it into AllowedRsaSigners when you provision the wallet. result.Modulus is already hex-encoded, which is the format AllowedRsaSigners expects.
Construct the signer
import "github.com/smartcontractkit/crec-sdk/transact/signer/vault"
s, err := vault.NewSigner(
"https://vault.example.com:8200",
os.Getenv("VAULT_TOKEN"),
"transit",
"treasury-prod-eth",
)
if err != nil { return err }
All four arguments are required; an empty value returns "vaultUrl, token, mountPath, and key must be set".
Inject a custom client (for tests)
client, _ := vaultapi.NewClient(vaultapi.DefaultConfig())
client.SetAddress("http://127.0.0.1:8200")
client.SetToken("dev-only-token")
s, err := vault.NewSigner("http://127.0.0.1:8200", "dev-only-token", "transit", "key", vault.WithClient(client))
Provision the wallet's signer set
Read back the key's hex-encoded modulus and pass it (with hex-encoded exponent for RSA-65537) into wallets.Create. Both e and n must be 0x-prefixed hex strings. GetRSAModulus() returns hex without the prefix, so prepend 0x before passing it to the API:
import (
apiClient "github.com/smartcontractkit/crec-api-go/client"
"github.com/smartcontractkit/crec-sdk/wallets"
)
modHex, err := s.GetRSAModulus()
if err != nil { return err }
rsaSigners := apiClient.RSASignersList{
{E: "0x010001", N: "0x" + modHex},
}
w, err := client.Wallets.Create(ctx, wallets.CreateInput{
Name: "treasury-prod-eth",
ChainSelector: "5009297550715157269",
WalletOwnerAddress: ownerEOA.Hex(),
WalletType: apiClient.Rsa,
AllowedRsaSigners: &rsaSigners,
StatusChannelId: &statusChannelID, // optional; receives wallet.status events
})
For ECDSA-P256 / P-384 / P-521 keys, use s.Public() to retrieve the *ecdsa.PublicKey and serialise it according to your wallet's expected format.
Sign an operation
opHash, sig, err := client.Transact.SignOperation(ctx, op, s, chainSelector)
Internally Sign(ctx, hash):
- Base64-encodes the digest.
- Calls
transit/sign/<key>withprehashed=trueandmarshaling_algorithm=asn1. - Strips the
vault:v1:prefix, decodes the ASN.1 signature, and returns the raw bytes.
For ECDSA-P256 the result is a DER-encoded (r, s) pair; for RSA it's a PKCS#1 v1.5 signature. The CRE Connect Smart Account expects this format for the corresponding WalletType.
Operational notes
- Vault token lifecycle. The signer holds a single Vault token; rotate it by re-creating the signer when the token nears expiry. Consider using AppRole for short-lived tokens.
- Latency. Each
SignOperationcall issues one Vault sign request synchronously. Latency is dominated by the network path to Vault and Vault's own response time; measure against your own Vault deployment and provision a cluster with headroom for your peak throughput. - Audit logging. Vault's audit devices capture every
transit/signcall; pair with the Signing Transparency guide for a complete signer-side audit trail. - HSM backing. For FIPS 140-2 compliance, run Vault Enterprise with the HSM auto-unseal + entropy plugin so the Transit engine's key material lives inside the HSM.
Vault policy
path "transit/keys/treasury-prod-eth" {
capabilities = ["read"]
}
path "transit/sign/treasury-prod-eth" {
capabilities = ["update"]
}
Bind the policy to the AppRole / token used by the signer.
Next steps
- Manage Wallet Signers: encode the modulus into
AllowedRsaSigners. - AWS KMS Signer: alternative HSM path for AWS environments.