跳到主要内容

签名与密钥操作

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,否则 falsecontext 必须与签名时用的一致。

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 遵循以太坊约定(2728)。

最佳实践

不要跨用途重用签名密钥。 每个角色一把。

context 做 domain separation。 两个应用用同一签名密钥但不同 context,不会接受对方的签名。

小心 emit 签名事件。 记住 OXN 上事件按调用者加密。如果你需要签名全局可验证,必须返回它,不要emit 它。

不要泄漏私钥。 永远不通过 getter 暴露、不放进事件、不作为返回值。

下一步