Summary
OWS exposes four signing operations: sign() (transaction bytes), signAndSend(), signMessage(), and signTypedData(). There is no operation for signing a raw 32-byte digest directly.
Use Case
Payment and DeFi protocol SDKs commonly compute EIP-712 struct hashes client-side (using language-native libraries) and then need to sign the resulting 32-byte digest. This is the standard pattern for SDKs in Go, Rust, Java, C#, Ruby, Swift, and Elixir — languages where the SDK handles EIP-712 hashing internally and only needs a "sign this hash" primitive.
Example from a Go payment SDK:
// SDK computes the EIP-712 struct hash using Go's crypto libraries
digest := eip712.HashStructData(domain, types, message) // returns [32]byte
// Need: sign this digest with the OWS-managed key
// Today: no OWS API for this
sig, err := ows.SignDigest(walletId, "evm", digest[:])
The TypeScript and Python SDKs can use signTypedData() (passing the full EIP-712 JSON), but the other 7 languages in our SDK matrix cannot — they compute hashes locally and need a raw digest signer.
The Internal Capability Exists
The Rust ChainSigner::sign() trait already accepts a pre-hashed 32-byte message for secp256k1 chains. The gap is only in the public API surface — there is no signDigest operation exposed through the SDK bindings or CLI.
Proposal
Add a signDigest operation to the signing interface:
interface SignDigestRequest {
walletId: WalletId;
chainId: ChainId; // "evm" — secp256k1 chains
digest: string; // hex-encoded 32-byte hash
}
// Returns: SignResult { signature, recoveryId }
This would:
- Expose the existing internal capability as a public API
- Enable protocol SDKs in all languages to integrate with OWS
- Require no changes to the signing device, policy engine, or key isolation model
- Follow the same auth/policy flow as other signing operations
Policy Considerations
A signDigest operation is lower-level than signTypedData — the policy engine cannot inspect the typed data fields (because they've already been hashed). Policy evaluation would be limited to:
- Caller authentication (API key scope)
- Rate limiting (spending caps based on external tracking)
- Chain restriction
This is a reasonable trade-off: protocols that compute their own hashes accept responsibility for the semantic content. The policy engine still controls who can sign and how often, just not what the hash represents.
Alternatives Considered
-
Require all SDKs to pass full EIP-712 JSON via signTypedData — forces every language to serialize to JSON and back, duplicating work the SDK already did natively. Also couples SDK implementations to OWS's JSON format.
-
Wrap digest in a signMessage call — signMessage applies chain-specific prefixes (EIP-191 \x19Ethereum Signed Message:\n), which produces wrong signatures for EIP-712 digests.
-
Use sign() with a synthetic transaction — sign() expects serialized transaction bytes, not arbitrary hashes.
None of these cleanly solve the problem.
Happy to contribute a PR if there's interest — the Rust-side change is minimal (expose the existing ChainSigner::sign path through the public API).
Summary
OWS exposes four signing operations:
sign()(transaction bytes),signAndSend(),signMessage(), andsignTypedData(). There is no operation for signing a raw 32-byte digest directly.Use Case
Payment and DeFi protocol SDKs commonly compute EIP-712 struct hashes client-side (using language-native libraries) and then need to sign the resulting 32-byte digest. This is the standard pattern for SDKs in Go, Rust, Java, C#, Ruby, Swift, and Elixir — languages where the SDK handles EIP-712 hashing internally and only needs a "sign this hash" primitive.
Example from a Go payment SDK:
The TypeScript and Python SDKs can use
signTypedData()(passing the full EIP-712 JSON), but the other 7 languages in our SDK matrix cannot — they compute hashes locally and need a raw digest signer.The Internal Capability Exists
The Rust
ChainSigner::sign()trait already accepts a pre-hashed 32-byte message for secp256k1 chains. The gap is only in the public API surface — there is nosignDigestoperation exposed through the SDK bindings or CLI.Proposal
Add a
signDigestoperation to the signing interface:This would:
Policy Considerations
A
signDigestoperation is lower-level thansignTypedData— the policy engine cannot inspect the typed data fields (because they've already been hashed). Policy evaluation would be limited to:This is a reasonable trade-off: protocols that compute their own hashes accept responsibility for the semantic content. The policy engine still controls who can sign and how often, just not what the hash represents.
Alternatives Considered
Require all SDKs to pass full EIP-712 JSON via
signTypedData— forces every language to serialize to JSON and back, duplicating work the SDK already did natively. Also couples SDK implementations to OWS's JSON format.Wrap digest in a
signMessagecall —signMessageapplies chain-specific prefixes (EIP-191\x19Ethereum Signed Message:\n), which produces wrong signatures for EIP-712 digests.Use
sign()with a synthetic transaction —sign()expects serialized transaction bytes, not arbitrary hashes.None of these cleanly solve the problem.
Happy to contribute a PR if there's interest — the Rust-side change is minimal (expose the existing
ChainSigner::signpath through the public API).