External Signers Guide¶
For production deployments, you likely manage private keys in a secure
enclave (hardware wallet, AWS KMS, Google Cloud KMS, HashiCorp Vault,
etc.) rather than in-process. The O2 SDK supports this via the
ExternalSigner and
ExternalEvmSigner classes.
How it works¶
The SDK handles all message framing (prefix bytes, hashing) internally
via the shared helpers fuel_personal_sign_digest()
(Fuel) and evm_personal_sign_digest() (EVM).
Your external signing function only needs to:
Receive a 32-byte digest.
Sign it with secp256k1 (ECDSA).
Return a 64-byte Fuel compact signature.
Use to_fuel_compact_signature() to convert from
standard (r, s, recovery_id) components.
Fuel-native external signer¶
For Fuel-native accounts:
from o2_sdk import O2Client, Network, OrderSide, ExternalSigner, to_fuel_compact_signature
def my_kms_sign(digest: bytes) -> bytes:
"""Sign a 32-byte digest using your KMS/HSM."""
r, s, recovery_id = my_kms.sign(key_id="my-key", digest=digest)
return to_fuel_compact_signature(r, s, recovery_id)
signer = ExternalSigner(
b256_address="0x1234...abcd", # Your Fuel B256 address
sign_digest=my_kms_sign,
)
async with O2Client(network=Network.MAINNET) as client:
account = await client.setup_account(signer)
session = await client.create_session(
owner=signer, markets=["FUEL/USDC"]
)
result = await client.create_order(
"FUEL/USDC", OrderSide.BUY, 0.02, 100.0
)
The SDK will call my_kms_sign with a Fuel-prefixed SHA-256 digest
for session creation and withdrawals.
Important
Session actions (orders, cancels, settlements) are signed with the session key — not the external signer. The external signer is only needed for session creation and withdrawals.
EVM external signer¶
For EVM-compatible accounts (MetaMask, Ledger via Ethereum, etc.):
from o2_sdk import ExternalEvmSigner, to_fuel_compact_signature
signer = ExternalEvmSigner(
b256_address="0x000000000000000000000000abcd...1234",
evm_address="0xabcd...1234",
sign_digest=my_kms_sign, # Same callback interface
)
The only difference is the message framing: ExternalEvmSigner
uses Ethereum’s \x19Ethereum Signed Message:\n prefix + keccak-256,
while ExternalSigner uses Fuel’s
\x19Fuel Signed Message:\n prefix + SHA-256.
Implementing the callback¶
The SignDigestFn callback must return a 64-byte
Fuel compact signature. Here is how to build one from standard
components:
from o2_sdk import to_fuel_compact_signature
def sign_digest(digest: bytes) -> bytes:
# Your KMS/HSM returns (r, s, recovery_id)
r: bytes = ... # 32 bytes
s: bytes = ... # 32 bytes (must be low-s normalized)
v: int = ... # 0 or 1
return to_fuel_compact_signature(r, s, v)
The Fuel compact format stores the recovery ID in the MSB of the first
byte of s:
s[0] = (recovery_id << 7) | (s[0] & 0x7F)
Warning
The s component must be low-s normalized before passing to
to_fuel_compact_signature(). Most modern signing
libraries (coincurve, ethers.js, etc.) do this automatically, but
check your KMS documentation.
AWS KMS example¶
import boto3
from o2_sdk import ExternalSigner, to_fuel_compact_signature
kms = boto3.client("kms")
def aws_kms_sign(digest: bytes) -> bytes:
response = kms.sign(
KeyId="alias/my-trading-key",
Message=digest,
MessageType="DIGEST",
SigningAlgorithm="ECDSA_SHA_256",
)
# Parse DER-encoded signature to (r, s)
der_sig = response["Signature"]
r, s, v = parse_der_signature(der_sig) # your DER parser
return to_fuel_compact_signature(r, s, v)
signer = ExternalSigner(
b256_address="0x...",
sign_digest=aws_kms_sign,
)
Custom Signer protocol¶
You can also implement the Signer protocol
directly for full control. Use the shared digest helpers to ensure your
framing matches the SDK:
from o2_sdk import fuel_personal_sign_digest
class MyCustomSigner:
@property
def b256_address(self) -> str:
return "0x..."
@property
def address_bytes(self) -> bytes:
return bytes.fromhex(self.b256_address[2:])
def personal_sign(self, message: bytes) -> bytes:
digest = fuel_personal_sign_digest(message)
# Sign the 32-byte digest with your own signing backend
return my_backend_sign(digest)
signer = MyCustomSigner()
session = await client.create_session(owner=signer, markets=["FUEL/USDC"])
For EVM accounts, use evm_personal_sign_digest()
instead.