签名与密钥操作
OXN 预编译让智能合约能生成密钥对、签名消息、验证签名——支持 Ed25519、secp256k1、sr25519。这是"合约控制的链下身份"的基础:合约持有私钥、代表授权调用方签名、私钥永不暴露。
签名算法
enum SigningAlg {
Ed25519_Oasis, // 带 Oasis 上下文前缀的 Ed25519
Ed25519_Pure, // 标准 Ed25519
Ed25519_PrehashedSha512, // 对预哈希 SHA-512 的 Ed25519
Secp256k1_Oasis, // 带 Oasis 上下文前缀的 secp256k1
Secp256k1_PrehashedKeccak256, // 对预哈希 keccak256 的 secp256k1
Secp256k1_PrehashedSha256, // 对预哈希 SHA-256 的 secp256k1
Sr25519 // sr25519
}
按验证方期望选择:
- Ed25519_Pure——独立签名最常用。
- Secp256k1_PrehashedKeccak256——需要被以太坊风格
ecrecover验证的签名。 - Sr25519——Substrate / Polkadot 兼容。
生成密钥对
function generateSigningKeyPair(
SigningAlg alg,
bytes memory seed
) internal view returns (bytes memory publicKey, bytes memory privateKey);
seed 作为密钥对的熵。从 Sapphire.randomBytes 取随机值最安全,或传确定性值得到可复现密钥(少见)。
import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
contract Signer {
bytes public publicKey; // 可以公开
bytes private privateKey; // 加密存储
function initialize() external {
bytes memory seed = Sapphire.randomBytes(32, "keygen");
(publicKey, privateKey) = Sapphire.generateSigningKeyPair(
Sapphire.SigningAlg.Ed25519_Pure,
seed
);
}
}
返回的 privateKey 通过 OXN 存储加密静态加密。它永远不在 enclave 外以明文出现。
签名消息
function sign(
SigningAlg alg,
bytes memory privateKey,
bytes memory context,
bytes memory message
) internal view returns (bytes memory);
context——domain-separation 字符串。例如带上下文前缀的 Ed25519 会把它混进签名,不同上下文的签名无法相互验证。message—— 被签名的数据。
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
);
}
签名是所选算法的标准格式——可以在链下用任何标准库验证。
验证签名
function verify(
SigningAlg alg,
bytes memory publicKey,
bytes memory context,
bytes memory message,
bytes memory signature
) internal view returns (bool);
签名合法返回 true,否则 false。context 必须与签名时用的一致。
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
);
}
用例:合约控制的链下身份
一个常见模式:你的合约持有代表链上身份的签名密钥。用户在合约上调方法;合约代表用户签名声明,但只在检查完访问规则之后。
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 或治理控制
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)
);
}
}
链下验证方知道合约的 publicKey。用户呈交签名后的 attestation,验证方检查签名,确知它是合约代表这个具体调用方签发的。
以太坊风格签名
需要被以太坊 ecrecover 验证的签名:
bytes memory sig = Sapphire.sign(
Sapphire.SigningAlg.Secp256k1_PrehashedKeccak256,
privateKey,
"",
keccak256(message)
);
// sig 可以拆成 (r, s, v) 传给 ecrecover
签名格式:65 字节(r || s || v)。v 遵循以太坊约定(27 或 28)。
最佳实践
不要跨用途重用签名密钥。 每个角色一把。
用 context 做 domain separation。 两个应用用同一签名密钥但不同 context,不会接受对方的签名。
小心 emit 签名事件。 记住 OXN 上事件按调用者加密。如果你需要签名全局可验证,必须返回它,不要emit 它。
不要泄漏私钥。 永远不通过 getter 暴露、不放进事件、不作为返回值。