Signature and Key Operations
OXN precompiles let smart contracts generate keypairs, sign messages, and verify signatures — supporting Ed25519, secp256k1, and sr25519. This is what enables "contract-controlled off-chain identity": a contract holds a private key, signs on behalf of authorized callers, and never exposes the key.
Signing algorithms
enum SigningAlg {
Ed25519_Oasis, // Ed25519 with Oasis context prefix
Ed25519_Pure, // Standard Ed25519
Ed25519_PrehashedSha512, // Ed25519 over pre-hashed SHA-512
Secp256k1_Oasis, // secp256k1 with Oasis context prefix
Secp256k1_PrehashedKeccak256, // secp256k1 over pre-hashed keccak256
Secp256k1_PrehashedSha256, // secp256k1 over pre-hashed SHA-256
Sr25519 // sr25519
}
Choose based on what the verifier expects:
- Ed25519_Pure — most common for standalone signatures.
- Secp256k1_PrehashedKeccak256 — for signatures that need to be verified by Ethereum-style
ecrecover. - Sr25519 — for Substrate / Polkadot compatibility.
Generating a keypair
function generateSigningKeyPair(
SigningAlg alg,
bytes memory seed
) internal view returns (bytes memory publicKey, bytes memory privateKey);
The seed parameter is used as entropy for the keypair. Pass a random value from Sapphire.randomBytes for maximum security, or a deterministic value if you want reproducible keys (rare).
import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
contract Signer {
bytes public publicKey; // safe to make public
bytes private privateKey; // stays encrypted in storage
function initialize() external {
bytes memory seed = Sapphire.randomBytes(32, "keygen");
(publicKey, privateKey) = Sapphire.generateSigningKeyPair(
Sapphire.SigningAlg.Ed25519_Pure,
seed
);
}
}
The privateKey returned is encrypted at rest by OXN's storage encryption. It never appears in plaintext outside the enclave.
Signing a message
function sign(
SigningAlg alg,
bytes memory privateKey,
bytes memory context,
bytes memory message
) internal view returns (bytes memory);
context— a domain-separation string. Ed25519 with context prefix, for example, mixes this into the signature so signatures from different contexts cannot cross-verify.message— the data being signed.
function signMessage(bytes calldata data) external view returns (bytes memory) {
require(canSign(msg.sender, data), "not authorized");
return Sapphire.sign(
Sapphire.SigningAlg.Ed25519_Pure,
privateKey,
"MyApp v1",
data
);
}
The signature is standard-format for the chosen algorithm — you can verify it off-chain with any standard library.
Verifying a signature
function verify(
SigningAlg alg,
bytes memory publicKey,
bytes memory context,
bytes memory message,
bytes memory signature
) internal view returns (bool);
Returns true if the signature is valid, false otherwise. context must match what was used to sign.
function verifyClaim(bytes calldata claim, bytes calldata sig, bytes calldata claimantPk)
external view returns (bool)
{
return Sapphire.verify(
Sapphire.SigningAlg.Ed25519_Pure,
claimantPk,
"MyApp v1",
claim,
sig
);
}
Use case: contract-controlled off-chain identity
A common pattern: your contract holds a signing key that represents an on-chain identity. Users invoke methods on the contract; the contract signs statements on their behalf, but only after checking access rules.
contract IdentityHolder {
bytes public publicKey;
bytes private privateKey;
mapping(address => bool) public authorized;
function initialize() external {
bytes memory seed = Sapphire.randomBytes(32, "id");
(publicKey, privateKey) = Sapphire.generateSigningKeyPair(
Sapphire.SigningAlg.Ed25519_Pure, seed
);
}
function authorize(address user) external {
// Owner-only or governance-controlled in real code
authorized[user] = true;
}
function signAttestation(bytes calldata attestation)
external view returns (bytes memory)
{
require(authorized[msg.sender], "not authorized");
return Sapphire.sign(
Sapphire.SigningAlg.Ed25519_Pure,
privateKey,
"MyApp attestation",
abi.encodePacked(msg.sender, attestation)
);
}
}
Off-chain verifiers know the contract's publicKey. When a user presents a signed attestation, verifiers check the signature and know it was produced by the contract on behalf of the specific caller.
Ethereum-style signatures
If you need signatures verifiable by Ethereum's ecrecover:
bytes memory sig = Sapphire.sign(
Sapphire.SigningAlg.Secp256k1_PrehashedKeccak256,
privateKey,
"",
keccak256(message)
);
// sig can now be split into (r, s, v) and passed to ecrecover
Signature format: 65 bytes (r || s || v). The v value follows Ethereum's convention (27 or 28).
Best practices
Do not reuse a signing key across purposes. One key per role.
Use context for domain separation. Two apps using the same signing key with different contexts will not accept each other's signatures.
Log signature events carefully. If your contract emits an event containing a signature, remember that events on OXN are encrypted per-caller. If you need a signature to be globally verifiable, you must return it, not emit it.
Don't leak the private key. Never expose it through a getter, never include it in an event, never pass it as a return value.
Next steps
- Key Derivation — X25519 for encrypt-to-user patterns
- Access Control Patterns — gating who can sign