このガイドは、solana-keychainライブラリに新しい鍵管理ソリューションを統合したいウォレットサービスプロバイダーや開発者向けです。署名者の実装を追加することで、統一されたインターフェースを通じて、開発者が安全なSolanaトランザクション署名にあなたのサービスを使用できるようになります。
KeychainはRust、TypeScript、Python、Goの各実装の機能を同等に保っています。4言語すべてを含むPRを強く推奨します。1つだけの貢献でも問題ありませんが、署名機能がリリースに含まれる前に、残りの実装も必須となる点をご了承ください。
Claude Codeを使っていますか?リポジトリのadd-signer-ciスキルは、フォークPRで提供された新しい署名機能向けのCIワークフローを設定します。
アーキテクチャ概要
このライブラリは、src/traits.rsで定義されたトレイトベースのアーキテクチャを採用しています。すべての署名機能は、基本トレイトSolanaSigner(pubkey、sign_message、is_available)と、プロバイダーの形態に合致する機能トレイトを1つだけ実装します:
TransactionSigner(sign_transaction):プロバイダーが呼び出し元のバイト列に署名し、呼び出し元がブロードキャストします。ほとんどのバックエンドが該当します。ModifyingSigner(modify_and_sign_transaction):プロバイダーがトランザクションを書き換えたうえで署名し、ブロードキャストは行いません。SendingSigner(sign_and_send_transaction):プロバイダーがサーバー側で署名とブロードキャストを行います。
このライブラリは、すべての実装をラップする統合Signerenumも提供しており、署名バックエンドを実行時に選択できます。実装するのは基本トレイトのみで、各機能へはas_transaction_signer()、as_modifying_signer()、as_sending_signer()を通じてアクセスします。
クイック統合チェックリスト
- 実装を含む署名者モジュールを作成
- 基本トレイト
SolanaSigner(pubkey、sign_message、is_available)と、プロバイダーの形態に合致する機能トレイトを1つ(通常はTransactionSigner)実装する Cargo.tomlに機能フラグを追加する。署名機能でreqwestを使う場合は、内部用の_remoteマーカーも含めるsrc/lib.rsのSignerenumを更新する(バリアント、dispatch_signer!のアーム、および形態に合致する機能アクセサのアーム)- HTTPSを強制し、HTTPクライアントのタイムアウトを設定
- メッセージのバイト列に署名し、返された署名を検証してから添付する
- 包括的なユニットテストを追加する(wiremockベース、モジュール内)
- 統合テストファイルを追加し、
.env.exampleを更新する - TypeScript、Python、Goの実装を追加する
- ドキュメントを更新
- CIワークフローの更新(メンテナーと調整してください。CIワークフローの更新を参照)
- PRを提出
ステップ1:署名者モジュールの作成
src/配下に実装用の新しいディレクトリを作成します:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
ステップ2:署名者構造体の定義
src/your_service/mod.rsで、署名者構造体を定義します:
//! YourService API signer integrationuse crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};use crate::{error::SignerError, traits::SolanaSigner};use std::str::FromStr;/// YourService-based signer using YourService's API#[derive(Clone)]pub struct YourServiceSigner {api_key: String,api_secret: String,wallet_id: String,api_base_url: String,client: reqwest::Client,public_key: Pubkey,}impl std::fmt::Debug for YourServiceSigner {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {f.debug_struct("YourServiceSigner").field("public_key", &self.public_key).finish_non_exhaustive()}}
ステップ3:コンストラクタとヘルパーメソッドの実装
リモート署名者は、HTTPSを必ず強制し、HTTPタイムアウトを設定する必要があります。タイムアウト設定には共有のHttpClientConfig構造体を使用してください。
use crate::http_client_config::HttpClientConfig;impl YourServiceSigner {pub fn new(api_key: String,api_secret: String,wallet_id: String,public_key: String,http_config: Option<HttpClientConfig>,) -> Result<Self, SignerError> {let pubkey = Pubkey::from_str(&public_key).map_err(|e| SignerError::InvalidPublicKey(format!("Invalid public key: {e}")))?;let http = http_config.unwrap_or_default();let builder = reqwest::Client::builder().timeout(http.resolved_request_timeout()).connect_timeout(http.resolved_connect_timeout());// Enforce HTTPS in production; wiremock uses HTTP in tests#[cfg(not(test))]let builder = builder.https_only(true);let client = builder.build().map_err(|e| {SignerError::ConfigError(format!("Failed to build HTTP client: {e}"))})?;Ok(Self {api_key,api_secret,wallet_id,api_base_url: "https://api.yourservice.com/v1".to_string(),client,public_key: pubkey,})}/// Sign raw bytes using your service's APIasync fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);let url = format!("{}/sign", self.api_base_url);let response = self.client.post(&url).header("Authorization", format!("Bearer {}", self.api_key)).json(&serde_json::json!({"wallet_id": self.wallet_id,"message": encoded_message,})).send().await?;// Use generic error messages: never expose raw API response textif !response.status().is_success() {let status = response.status().as_u16();return Err(SignerError::remote_api(format!("YourService API returned status {status}")));}// Parse response: always use map_err, never .expect() or .unwrap()let response_data: SignResponse = response.json().await.map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;let sig_bytes = base64::engine::general_purpose::STANDARD.decode(&response_data.signature).map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;Ok(Signature::from(sig_array))}}
ステップ4:署名機能トレイトの実装
基本トレイトSolanaSignerには、2つの非同期メソッド(sign_message、is_available)とpubkey()があります。トランザクションの署名は機能トレイトで行います。ほとんどのプロバイダーはTransactionSignerを実装し、そのsign_transactionはSignTransactionResult(トランザクションが完全署名か部分署名かを示すタグ付きenum)を返します。
署名するのはメッセージのバイト列であり、ワイヤートランザクション全体ではありません。添付する前に、プロバイダーの署名をそのバイト列に対して検証してください。署名とシリアライズには、共有のTransactionUtilヘルパーを使用します。
use crate::signature_util::verify_or_reject;use crate::traits::{SignTransactionResult, TransactionSigner};use crate::transaction_util::TransactionUtil;#[async_trait::async_trait]impl SolanaSigner for YourServiceSigner {fn pubkey(&self) -> Pubkey {self.public_key}async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {let signature = self.sign(message).await?;verify_or_reject(&signature, &self.public_key, message)?;Ok(signature)}async fn is_available(&self) -> bool {let url = format!("{}/health", self.api_base_url);self.client.get(&url).send().await.map(|r| r.status().is_success()).unwrap_or(false)}}#[async_trait::async_trait]impl TransactionSigner for YourServiceSigner {async fn sign_transaction(&self,tx: &mut VersionedTransaction,) -> Result<SignTransactionResult, SignerError> {let message = tx.message.serialize();let signature = self.sign(&message).await?;verify_or_reject(&signature, &self.public_key, &message)?;TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;let serialized = TransactionUtil::serialize_transaction(tx)?;Ok(TransactionUtil::classify_signed_transaction(tx,(serialized, signature),))}}
プロバイダーが署名前にトランザクションを書き換える場合は、代わりにModifyingSignerを実装し、呼び出し元のトランザクションをプロバイダーのバイト列で丸ごと置き換えます。サーバー側でブロードキャストする場合はSendingSignerを実装し、呼び出し元のトランザクションには手を加えません。プロバイダーが提供できない機能は決して実装しないでください。
ステップ5:API型の追加(オプション)
APIにカスタム型が必要な場合は、src/your_service/types.rsを作成してください:
use serde::{Deserialize, Serialize};#[derive(Serialize)]pub struct SignRequest {pub wallet_id: String,pub message: String,}#[derive(Deserialize)]pub struct SignResponse {pub signature: String,}
ステップ6:機能フラグの追加
Cargo.tomlを更新して、署名機能をオプション機能として追加します。リモートバックエンドには内部用の_remoteマーカーを含めます。これはsrc/error.rsのFrom<reqwest::Error>実装を制御するもので、これがないと、自分の機能のみを有効にした場合にreqwest呼び出しへの?がコンパイルできません。
[features]default = ["memory", "sdk-v2"]memory = []vault = ["dep:reqwest", "_remote"]privy = ["dep:reqwest", "dep:p256", "_remote"]your_service = ["dep:reqwest", "_remote"] # Add your featureall = ["memory", "vault", "privy", "...", "your_service"] # Update all
ステップ7:署名機能Enumの更新
src/lib.rsに署名機能を追加します。基本のSolanaSigner実装はdispatch_signer!マクロ経由でディスパッチするため、そこにアームを1つ追加します。機能へのアクセスはas_transaction_signer()、as_modifying_signer()、as_sending_signer()の各アクセサにあります。自分の機能トレイトに対応するアクセサにSome(s)アームを追加してください(送信型または書き換え型のバックエンドの場合は、as_transaction_signer()にNoneアームも追加します)。
// Add feature-gated module#[cfg(feature = "your_service")]pub mod your_service;// Re-export your signer type#[cfg(feature = "your_service")]pub use your_service::YourServiceSigner;// Add to Signer enum#[derive(Debug)]pub enum Signer {#[cfg(feature = "memory")]Memory(MemorySigner),// ... existing variants#[cfg(feature = "your_service")]YourService(YourServiceSigner), // Add your variant}// Add constructor methodimpl Signer {#[cfg(feature = "your_service")]pub fn from_your_service(api_key: String,api_secret: String,wallet_id: String,public_key: String,) -> Result<Self, SignerError> {Ok(Self::YourService(YourServiceSigner::new(api_key,api_secret,wallet_id,public_key,None, // uses default HttpClientConfig)?))}}// Update the dispatch macro: the base SolanaSigner impl uses it for// pubkey, sign_message and is_availablemacro_rules! dispatch_signer {($self:ident, $signer:pat => $body:expr) => {match $self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService($signer) => $body,}};}// Add your arm to the capability accessor matching your shapeimpl Signer {pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => Some(s),}}}
ステップ8:包括的なテストの追加
モジュールにテストを追加してください(src/your_service/mod.rsの最下部):
#[cfg(test)]mod tests {use super::*;use solana_sdk::{signature::Keypair, signer::Signer};use wiremock::{matchers::{header, method, path},Mock, MockServer, ResponseTemplate,};#[tokio::test]async fn test_new() {let keypair = Keypair::new();let signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,);assert!(signer.is_ok());}#[tokio::test]async fn test_sign_message() {let mock_server = MockServer::start().await;let keypair = Keypair::new();let message = b"test message";let signature = keypair.sign_message(message);Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())}))).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(message).await;assert!(result.is_ok());}#[tokio::test]async fn test_sign_unauthorized() {let mock_server = MockServer::start().await;let keypair = Keypair::new();Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(401)).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("bad-key".to_string(),"bad-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(b"test").await;assert!(result.is_err());}}
署名検証の失敗もカバーしてください。署名が検証に通らないモックレスポンスは、エラーを返す必要があります。
ステップ9:統合テストの追加
rust/src/tests/test_<name>_integration.rsを作成します。統合テストは(wiremockではなく)実際のサービスAPIに対して実行され、integration-tests機能で制御されます。各ファイルには以下が必要です:
- 環境変数名用の
pub const宣言 dotenvy経由で環境変数を読み込むget_signer()ヘルパー- 3つのテスト関数:
test_<name>_sign_message、test_<name>_sign_transaction、test_<name>_is_available - 機能ゲート:モジュールに
#[cfg(feature = "your_service")]、各テストに#[cfg(feature = "integration-tests")]
完全なリファレンスはrust/src/tests/test_para_integration.rsを参照し、モジュールをrust/src/tests/mod.rsで宣言してください:
pub mod test_your_service_integration;
ステップ10:環境変数の更新
既存のパターンに従って、ルートの.env.exampleに署名機能の環境変数を追加します:
# YourService Configuration (for SIGNER_TYPE=your_service)YOUR_SERVICE_API_KEY=your-api-keyYOUR_SERVICE_WALLET_ID=your-wallet-id# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this
必須の変数をプレースホルダー値付きで先に列挙し、任意の変数はデフォルト値付きでコメントアウトします。統合テストで使用するすべての環境変数は.env.exampleに記載する必要があり、TypeScript、Python、Goのテストスイートでも同じ名前を使用します。
ステップ11:ドキュメントの更新
README.mdのサポートされているバックエンドの表に署名機能を追加してください:
| バックエンド | ユースケース | 機能フラグ |
|---|---|---|
| Memory | ローカルキーペア、開発、テスト | memory |
| Vault | HashiCorp Vaultを使用したエンタープライズ鍵管理 | vault |
| Privy | Privyインフラストラクチャを使用した埋め込みウォレット | privy |
| Turnkey | Turnkeyによる非カストディアル鍵管理 | turnkey |
| YourService | サービスの簡単な説明 | your_service |
各言語のREADMEにある署名機能の対応表にも、バックエンドを追加してください。使用例を追加します:
use solana_keychain::{Signer, SolanaSigner};#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {let signer = Signer::from_your_service("your-api-key".to_string(),"your-api-secret".to_string(),"your-wallet-id".to_string(),"your-public-key".to_string(),)?;let pubkey = signer.pubkey();println!("Public key: {}", pubkey);Ok(())}
統合のテスト
生のcargoではなくjustレシピを使用してください。SDK機能(sdk-v2、sdk-v3、sdk-v4)は相互排他的であるため、cargo test --all-featuresは失敗します。
# Unit tests across the sdk-v2, sdk-v3 and sdk-v4 matricesjust rust-test# Live integration tests (require credentials in .env)just rust-test-integration
CIワークフローの更新(フォークPR)
CIは2フェーズのプロセスです。PRをテストできるようにするには、メンテナーと調整してmainを準備する必要があります。メンテナーはfork-external-live-manual.ymlを更新し、ci.ymlに環境変数を追加し、GitHub Secretsを設定します。
PRには、署名機能のコードとあわせて、以下のフェーズ2の変更を含める必要があります:
ci.yml:Rust機能をbackendマトリクスに(allの前に)追加し、統合テスト関数をtestマトリクスに追加するtypescript-ci.yml:パッケージをユニットテストと統合テストのマトリクスに追加し、統合テストステップに環境変数を追加するtypescript-publish.yml:パッケージをPUBLISH_PACKAGES、GitHub Releaseのpackages配列、およびサマリーテーブルに追加するpython-ci.yml:resolve-signers内のCI_SIGNER_*ゲートと統合マトリクスにバックエンドを追加するgo-ci.yml:CI_SIGNER_*ゲートとmoduleBySignerにバックエンドを追加する
TypeScript署名機能
TypeScript署名機能パッケージも追加する場合は、typescript/packages/your-signer/に作成してください。主要なパターン:
- ファクトリ関数
createYourSigner()はSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>を返します。署名機能のクラスは内部に留め、ファクトリとその設定型のみをエクスポートしてください - 設定インターフェース(
YourSignerConfig)をエクスポート - バックエンドがメッセージに署名できない場合は、
SolanaMessageSignerを省略し、signMessagesメソッドをまったく公開しないでください。Kitはメソッドの有無で署名機能を分類するため、例外をスローするスタブでは署名機能が誤って分類されます apiBaseUrl設定フィールドには、@solana/keychain-coreのassertHttpsUrl()でHTTPSを強制する- リモート呼び出しは
@solana/keychain-coreのfetchSignerJson()経由で行う。エラーのマッピング、レスポンスのサニタイズ、リダイレクトの拒否、デフォルトのタイムアウトは、この関数が担当します - その他のリモートAPIエラーテキストは、
@solana/keychain-coreのsanitizeRemoteErrorResponse()でサニタイズする @solana/keychain-coreのthrowSignerError(SignerErrorCode.*, { cause, message })を使用- すべての署名メソッドで
config?.abortSignalを尊重し、fetchSignerJson()とsignBatchStaggered()に渡す requestDelayMsをvalidateRequestDelayMs()で検証し、signBatchStaggered()でバッチ処理する- ファクトリ関数にエラーコードを列挙する
@throwsJSDoc を追加 @solana/*パッケージは、>=8.1.0のピア依存関係として宣言する- ユニットテストはvitestとモック化した
fetchで、統合テストは@solana/keychain-test-utilsのrunSignerIntegrationTestで行う
送信型署名機能
プロバイダーがトランザクションを書き換える場合、またはサーバー側でブロードキャストする場合、その署名は呼び出し元のトランザクションに適用できません。そのため、SolanaTransactionSignerではなくSolanaSendingSignerにする必要があります:
signAndSendTransactions()を実装し、signTransactionsは公開しない(バックエンドが実際にメッセージに署名する場合を除き、signMessagesも公開しない)config?.abortSignalを尊重する。プロバイダーが作成リクエストを受理した後は、あらゆる失敗(中断を含む)を、プロバイダーのトランザクションIDを付けたBROADCAST_UNCONFIRMEDとして報告し、中断の理由はcauseとして保持する- 送信型署名機能の型を返す、型付きの
createKeychainSignerオーバーロードを追加する typescript/packages/kit-plugin/src/keychain-plugin.tsのKeychainKitPluginConfigからバックエンドを除外する。送信型署名機能はKitクライアントのpayerやidentityとして機能できないためです- ユニットテストで、インスタンスに対して
isTransactionPartialSigner()がfalse、isTransactionSendingSigner()がtrueであることをアサートする
アンブレラパッケージの更新
typescript/packages/keychain/を更新します(変更するファイルは7つ):
src/types.ts:KeychainSignerConfigの判別共用体にYourSignerConfigを追加するsrc/create-keychain-signer.ts:switchのcaseを追加する(各バックエンドパッケージは動的なimport()で読み込まれます)src/resolve-address.ts:高速パスまたはフェッチパスのswitch caseに追加するsrc/index.ts:設定型、名前空間、ファクトリ関数のエクスポートを追加するpackage.json:@solana/keychain-your-signer: "workspace:*"依存関係を追加するtsconfig.json:{ "path": "../your-signer" }参照を追加するtypescript/scripts/test-treeshake-umbrella.mjs:パッケージをSIGNER_MARKERSに、ファクトリをFACTORIESに追加する
switch文には網羅的なneverチェックがあるため、共用体に追加してcaseを忘れるとTypeScriptがエラーを出します。その後、just ts-treeshakeを実行してください。すべてのパッケージとアンブレラパッケージがツリーシェイキング可能な状態を保つ必要があります。
Python署名機能
python/src/solana_keychain/your_signer/にモジュールを作成します(__init__.pyとsigner.py)。主なパターン:
solana_keychain.coreのSolanaSigner(pubkeyプロパティ、async sign_message()、async is_available())をサブクラス化し、さらに機能クラスを1つだけ実装する:TransactionSigner(async sign_transaction())、ModifyingSigner(async modify_and_sign_transaction())、またはSendingSigner(async sign_and_send_transaction())- バックエンドがアドレスをリモートで解決する場合は、
init()をawaitするasync create_your_signer()ファクトリをエクスポートする。初期化前の署名はSignerErrorCode.NOT_INITIALIZEDを送出する - シークレットに
field(repr=False)を指定した設定データクラス(YourSignerConfig)をエクスポートする - ベースURLフィールドには
assert_https_url()でHTTPSを強制し、すべてのリクエストをfetch_signer_json()経由にする - プロバイダーの署名は、返す前に検証する。署名の対象となるバイト列には
signed_message_bytes()を使い、手作業のバージョンプレフィックスは使わない python/src/solana_keychain/keychain.pyの_BACKENDSにバックエンドを登録する- バックエンドがプロバイダーSDKを必要とする場合は、importをガードし、
pip install 'solana-keychain[your-signer]'を示すImportErrorを送出し、extraをpython/pyproject.tomlで宣言してください。また、パッケージルートからバックエンドを先行importしないでください - pytestによるユニットテスト: HTTPバックエンドには
respx、SDKバックエンドにはスタブクライアントを使用し、送出されたエラーのstr()やrepr()にシークレットが含まれていないことを確認するチェックも行います python/tests/integration/test_live_signers.pyにmake_your_signer()ファクトリを追加し、_MESSAGE_CAPABLEに登録します(sign_messageを持たないバックエンドの場合は_TRANSACTION_ONLY)
Goのサイナー
各Goバックエンドは独立したモジュールで、go/signers/yoursigner/ に配置し、モジュールパスは
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2 とします。参考実装は
go/signers/para/ を参照してください。主なパターン:
core.SolanaSigner(Pubkey()、SignMessage(ctx, message)、IsAvailable(ctx))を実装し、さらにケイパビリティインターフェースを1つだけ実装します:core.TransactionSigner、core.ModifyingSigner、core.SendingSignerのいずれかです。 ケイパビリティは型アサーションで検出されるため、トランザクションに署名できないバックエンドにはSignTransactionメソッドを一切持たせてはいけませんConfig構造体とNewコンストラクタをエクスポートします(バックエンドがアドレスをリモートで解決する場合はNew(ctx, cfg)。解決前はcore.NewNotInitializedErrorを返します)- HTTPクライアントは
core.NewHTTPClientまたはcore.ResolveHTTPClientで構築し、 ベースURLはcore.NormalizeHTTPSBaseURLで検証します - トランザクションへの署名には
core.SignTransactionWithを使用します。これはメッセージのバイト列に署名し、その署名を付与します。プロバイダーの署名はcore.VerifySignatureで検証します - エラーは
core.NewSignerError/core.WrapSignerErrorと、core.CodeSigningFailedなどの安定したコードを使って返します - ユニットテストは
httptestで実施し、統合テストはintegration_test.goに記述して//go:build integrationタグで制御します
提出チェックリスト
PR を提出する前に:
- コードが警告なしでコンパイルされる(
just build) - すべてのテストが合格する(
just test) - コードがフォーマット/リントに合格する(
just fmt) - コードにハードコードされた値やシークレットがない
- エラーメッセージが汎用的である(生の API レスポンステキストを含まない)
- 信頼できないAPIレスポンスに対して
.expect()や.unwrap()を使用していない - リモート HTTP クライアントで HTTPS が強制されている
- HTTP タイムアウトが
HttpClientConfig経由で設定されている - 返された署名が、ローカルで計算したメッセージのバイト列に対して検証されている
- 命名規則に従っている(RustとPythonはsnake_case、TypeScriptはcamelCase)
- 統合テストを追加し、
.env.exampleを更新した - TypeScript、Python、Goの実装を含めている(またはメンテナーと計画済み)
- アンブレラパッケージを更新した(TypeScriptのアンブレラ、Pythonの
_BACKENDS) - CIの変更を含めている
- README.mdの対応バックエンド表とサイナー機能表に追加した
実装のヒント
エラー処理
必ず既存の SignerError バリアントを使用してください。SignerError の Display と Debug はどちらもマスキングされているため、メッセージは汎用的なものにし、APIレスポンスの生データは決して含めないでください。信頼できないAPIレスポンスに対して .expect() や .unwrap() を使用してはいけません:
// Good: uses the constructor with a generic messagereturn Err(SignerError::remote_api(format!("YourService API returned status {status}")));// Good: converts from standard errorslet bytes = base64::decode(data).map_err(|e| SignerError::SerializationError(format!("Failed to decode: {e}")))?;
セキュリティ要件
これらは単なる推奨事項ではありません。利用者はこれらを根拠にバックエンドを選びます。ご利用のプロバイダーによっていずれかが実現不可能な場合は、PRおよびリポジトリの
docs/SECURITY_MODEL.md にその旨を明記してください。
- プロバイダーが署名した内容を検証すること。 メッセージのバイト列をローカルで計算し、返された署名をそのバイト列に対して、サイナーが要求する位置で検証してから付与してください。検証に失敗した署名はエラーとして扱い、決して付与してはいけません。
- プロバイダーがブロードキャストするかどうかを明示すること。 サーバー側で実行される場合は、送信型の形態を実装し、ランディングしたトランザクションを特定できる署名を返してください。プロバイダーに署名のみのエンドポイントがない場合は、呼び出し元の知らないところで送信するのではなく、バックエンドを送信専用にしてください。
- 署名が対象としていないトランザクションを呼び出し元に持たせないこと。 トランザクションを書き換えるプロバイダーは、自身のバイト列に署名します。したがって、呼び出し元のトランザクションには手を加えない(ブロードキャスト管理型のケース)か、変更型の形態を採用してプロバイダーのバイト列で全体を置き換えてください。
- ブロードキャストの不確実性はそのまま報告すること。 送信後に失敗しても、トランザクションはすでにランディングしている可能性があります。汎用的なエラーではなく
BROADCAST_UNCONFIRMEDを返し、プロバイダーのトランザクションIDがある場合は必ず含め、作成のたびにメッセージから導出した冪等性キーを送信してください。これにより、バイト単位で同一の再送も安全に行えます。 - 初期化前に署名しないこと。
init()の前の公開鍵には(Pubkey::default()ではなく)Option<Pubkey>を使用し、先に署名が試みられた場合は設定エラーで失敗させてください。 - 機密データ(秘密鍵、APIシークレット、APIの生レスポンス)は決してログに出力せず、
機密フィールドを隠す
Debug実装を使用します - HTTPSを強制し(
https_only(true)。wiremockのために#[cfg(not(test))]で制御)、リクエストと接続のタイムアウトをHttpClientConfigで設定します (デフォルト: 30秒/5秒) - 中間的な鍵素材は
zeroize::Zeroizing<Vec<u8>>でゼロ化します
モックを使用したテスト
HTTP
APIのモックにはwiremockを使用してください。エラーメッセージのテキストではなく、エラータイプに対してアサートを行います:
#[cfg(test)]mod tests {use wiremock::{MockServer, Mock, ResponseTemplate};#[tokio::test]async fn test_api_call() {let mock_server = MockServer::start().await;Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)).mount(&mock_server).await;// Use mock_server.uri() as your api_base_url}}
ヘルプの取得
- 既存の署名者実装のパターンを確認してください:
src/memory/mod.rs: シンプルで同期的src/privy/mod.rs: 初期化が必要src/turnkey/mod.rs: 複雑な署名処理src/vault/mod.rs: 外部クライアントライブラリtypescript/packages/para/、python/src/solana_keychain/para/、go/signers/para/: 他の言語における完全なリファレンス実装
- 主要ファイル:
src/traits.rs(トレイト定義)、src/transaction_util.rsとsrc/signature_util.rs(共有ヘルパー)、src/http_client_config.rs(タイムアウト設定) - リポジトリの完全なガイド: docs/ADDING_SIGNERS.md
- 作業を開始する前に、設計に関する議論のためにissueを開いてください
PRの構成例
feat(signer): add YourService signer integrationAdds support for YourService as a signing backend.- [X] Code compiles without warnings (`just build`)- [X] Code is formatted/linting passes (`just fmt`)- [X] Add comprehensive tests with wiremock - All tests pass (`just test`)- [X] Implemented SolanaSigner and TransactionSigner for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Returned signatures verified before attaching- [X] Added integration tests and updated .env.example- [X] TypeScript, Python and Go implementations included- [X] CI Phase 2 changes included- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?