メッセージに署名してユーザーを認証する

メッセージへの署名は、ユーザーがウォレットを管理していることの証明になります。トランザクションも手数料もネットワーク呼び出しも不要です。アプリは署名済みメッセージを、ユーザーのサインイン、利用規約への同意の記録、ウォレットと既存アカウントの紐付け、注文などオフチェーンで行われるアクションの承認に使用します。

Solanaウォレットがメッセージに署名する方法は2つあります。

  • オフチェーンメッセージ(solana:signOffchainMessage)は、オフチェーンメッセージ署名(OCMS)の仕様で定義された標準フォーマットでテキストをラップします。このフォーマットはトランザクションと取り違えられることがなく、署名が必要なアカウントを明示し、ハードウェアウォレットが署名する形式でもあります。
  • 生のメッセージ(solana:signMessage)は、アプリが渡したバイト列をそのまま、プレフィックスも署名者の宣言もなしで署名します。ほとんどのソフトウェアウォレットが対応していますが、バイト列自体にはトランザクションではなくメッセージであることを示すものがありません。

ウォレットが対応している場合はオフチェーンメッセージを使用し、対応していない場合は生のメッセージにフォールバックします。このガイドの以降の説明ではサインインを例に取り上げますが、署名と検証の手順はどのメッセージにも共通です。

メッセージ形式を選ぶ

オフチェーンメッセージ生のメッセージトランザクション
ウォレットの機能signOffchainMessagesignMessagesignTransaction
ハードウェアウォレット対応デバイスによる非対応対応
ユーザーが読めるはいソフトウェアウォレットのみデバイスによる
必要な署名者を宣言はいいいえはい
RPC呼び出しと手数料なしなしブロックハッシュ取得、送信、手数料、承認確認

トランザクションを使わない理由

かつてハードウェアウォレットには、読めるメッセージに署名する手段がありませんでした。そのため多くのアプリは、代わりに何もしないトランザクションへの署名をこれらのユーザーに求めていました。この方法でも動作はしますが、レイテンシ、コスト、リスクが増えます。

  • ネットワーク手数料:トランザクションは、何も実行しない場合でも手数料がかかります。
  • 複数のRPC呼び出し:アプリはブロックハッシュを取得し、シミュレーションと送信を行い、その後で承認確認をポーリングします。
  • レイテンシの増加:これらの呼び出しはそれぞれが往復通信であり、ユーザーがサインインするまでの待ち時間になります。
  • 内容が読めない:ユーザーが承認するのはトランザクションのバイト列であり、同意する内容を記した文章ではありません。
  • 行動面のリスク:ログインするだけのためにトランザクションへの署名を求めると、ユーザーは有害なトランザクションにも署名するよう仕向けられやすくなります。

オフチェーンメッセージなら、これらはすべて不要です。往復通信が発生するのは、自社のサーバーとの間だけです。

A sign-in transaction fetches a blockhash, signs, simulates, sends, and polls for confirmation before the server can verify it. An off-chain message goes from a server challenge to a wallet signature to a server-side check, with no RPC calls and no fee.

ウォレットが署名する内容

オフチェーンメッセージは、固定の署名ドメイン \xffsolana offchain で始まります。有効なトランザクションがこのバイト列で始まることはないため、署名済みメッセージをトランザクションとして使われることはありません。フォーマットのバージョン1では、続けて必要な署名者の一覧が置かれ、メッセージ本体がUTF-8テキストとして格納されます。長さの上限はありません。

ウォレットはこれらのバイト列を組み立てて署名し、署名とともに返します。レガシーのバージョン0を含むフォーマットの詳細は、仕様を参照してください。

A version 1 off-chain message is a 16-byte signing domain, a version byte, a signer count, each signer's 32-byte public key in sorted order, and the UTF-8 content. The envelope prefixes it with a signature count and one 64-byte signature per signer.

サインイン方法を選ぶ

すべてのウォレットがオフチェーンメッセージに対応しているわけではありません。署名を求める前に、接続中のアカウントの機能を確認し、機能がない場合はフォールバックしてください。

  1. アカウントがメッセージバージョン1の solana:signOffchainMessage に対応している場合は、オフチェーンメッセージに署名します。
  2. 対応していない場合は、同じチャレンジを solana:signMessage で署名します。ほとんどのソフトウェアウォレットが対応しています。
  3. メッセージ署名に対応していないハードウェアウォレットのユーザーには、「ハードウェアウォレットを使用中」のトグルなど明示的なオプトインを用意し、代わりにサインイン用トランザクションに署名してもらいます。この方法をデフォルトで案内しないでください。
When a wallet connects, check whether its account supports solana:signOffchainMessage version 1. If it does, sign an off-chain message. If not, sign a raw message with solana:signMessage, and let hardware wallet users opt into signing a sign-in transaction.

ブラウザーでメッセージに署名する

Kitと併せてWallet Standardのパッケージをインストールします。

Terminal
$
npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry

これらのヘルパーは UiWalletAccount を受け取ります。たとえば、Reactガイドの useConnectedWallet が返す account を渡します。

offchain-message.ts
import {
SolanaSignOffchainMessage,
type SolanaSignOffchainMessageFeature
} from "@solana/wallet-standard-features";
import type { UiWalletAccount } from "@wallet-standard/ui";
import { getWalletAccountFeature } from "@wallet-standard/ui-features";
import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";
type OffchainMessageFeature =
SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];
export function supportsOffchainMessages(account: UiWalletAccount) {
if (!account.features.includes(SolanaSignOffchainMessage)) return false;
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
return feature.supportedMessageVersions.includes(1);
}
export async function signOffchainMessage(
account: UiWalletAccount,
message: string
) {
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
const [output] = await feature.signOffchainMessage({
messageVersion: 1,
account: getWalletAccountForUiWalletAccount(account),
message,
requiredSigners: [account.publicKey]
});
return output;
}

signOffchainMessage は、ウォレットが署名したバイト列全体である signedOffchainMessage と、signature を返します。両方をBase64エンコードし、ウォレットアドレスとともにサーバーへ送信します。

チャレンジは、小さな画面でも読みやすいように書きます。アプリ名とドメインを明記し、署名によって何が行われるかを説明したうえで、サーバーが発行する使い捨てのノンスと発行時刻を含めてください。

swap.app wants you to sign in with your Solana account.
Signing in costs nothing and does not submit a transaction.
Nonce: 8f3a1c9e
Issued at: 2026-09-29T17:04:00Z

サーバーで署名を検証する

検証はローカルでの署名チェックであり、RPC接続は不要です。ウォレットが署名したバイト列をデコードし、発行したチャレンジと一致することを確認してから、署名を検証します。

verify-sign-in.ts
import {
address,
assertOffchainMessageV1Equal,
getBase64Encoder,
getOffchainMessageDecoder,
verifyOffchainMessageEnvelope,
type OffchainMessageBytes,
type SignatureBytes
} from "@solana/kit";
export async function verifySignIn(input: {
walletAddress: string;
expectedMessage: string;
signedOffchainMessage: string;
signature: string;
}) {
const signer = address(input.walletAddress);
const base64 = getBase64Encoder();
const content = base64.encode(
input.signedOffchainMessage
) as OffchainMessageBytes;
const signature = base64.encode(input.signature) as SignatureBytes;
const received = getOffchainMessageDecoder().decode(content);
if (received.version !== 1) {
throw new Error(`Expected a v1 message, got v${received.version}`);
}
assertOffchainMessageV1Equal(received, {
version: 1,
requiredSignatories: [{ address: signer }],
content: input.expectedMessage
});
await verifyOffchainMessageEnvelope({
content,
signatures: { [signer]: signature }
});
}

assertOffchainMessageV1Equal は、内容または署名者が想定と異なる場合にスローします。verifyOffchainMessageEnvelope は、署名がない場合または無効な場合にスローします。expectedMessage は、クライアントから返送されたコピーを信用せず、サーバー側でノンスをキーにして取得してください。検証に成功したらノンスを消費し、同じ署名で二度サインインできないようにします。

生のメッセージを検証する

signMessage の署名はチャレンジのバイト列を直接対象とするため、クライアントが送信するのは署名とウォレットアドレスだけで済みます。発行したチャレンジに対して検証してください。

verify-raw-sign-in.ts
import {
address,
getBase64Encoder,
getPublicKeyFromAddress,
getUtf8Encoder,
signatureBytes,
verifySignature
} from "@solana/kit";
export async function verifyRawSignIn(input: {
walletAddress: string;
expectedMessage: string;
signature: string;
}) {
const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));
const signature = signatureBytes(getBase64Encoder().encode(input.signature));
const message = getUtf8Encoder().encode(input.expectedMessage);
if (!(await verifySignature(publicKey, signature, message))) {
throw new Error("Invalid signature");
}
}

ノンスのルールは同じです。expectedMessage をノンスで取得し、検証に成功したら消費します。

@solana/kit は、これらのヘルパーを @solana/offchain-messages から再エクスポートしています。サーバーがKitに依存していない場合は、単体のパッケージを使用してください。

Sign In With Solana

Sign In With Solana(SIWS)は、チャレンジそのものを標準化します。アプリは domain、statement、nonce、issuedAt などの構造化されたフィールドをウォレットの solana:signIn 機能に渡します。ウォレットはそれらを標準のメッセージに整形し、必要に応じて接続したうえで、1ステップで署名します。

solana:signIn のバージョン 1.1.0 では、useOffchainMessage オプションが追加されました。useOffchainMessage: { messageVersion: 1 } を渡すと、ウォレットは署名アカウントを唯一の必要な署名者として、SIWSテキストをバージョン1のオフチェーンメッセージでラップします。ハードウェアウォレットでも表示と署名が可能で、出力には signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 } が含まれます。バージョン 1.0.0 を宣言しているウォレットはこのオプションを認識しないため、先に機能のバージョンを確認し、該当しない場合は省略してください。

solana:signIn はアカウントの機能ではなくウォレットの機能であるため、これらのヘルパーは UiWallet を受け取ります。

sign-in.ts
import {
SolanaSignIn,
type SolanaSignInFeature,
type SolanaSignInInput
} from "@solana/wallet-standard-features";
import type { UiWallet } from "@wallet-standard/ui";
import { getWalletFeature } from "@wallet-standard/ui-features";
type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];
export function supportsOffchainSignIn(wallet: UiWallet) {
if (!wallet.features.includes(SolanaSignIn)) return false;
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
return feature.version === "1.1.0";
}
export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
const [output] = await feature.signIn({
...input,
...(supportsOffchainSignIn(wallet) && {
useOffchainMessage: { messageVersion: 1 }
})
});
return output;
}

入力はサーバーで作成し、使い捨ての nonce と issuedAt の時刻を含めて、ノンスをキーにしたコピーを保持します。サインインの検証に成功したら、ノンスを消費してください。

Wallet Adapterを使用しない場合、@solana/wallet-standard-util の verifySignIn が検証できるのは通常のSIWS出力のみです。signedMessageFormat がオフチェーンメッセージを示している場合、signedMessage は内容がSIWSテキストであるオフチェーンメッセージです。保存しておいた入力と署名アドレスから、@solana/wallet-standard-util の createSignInMessageText で想定されるテキストを再構築し、サーバーで署名を検証するの手順と同様に、他のオフチェーンメッセージと同じ方法で検証します。Kitでのこのフローについては@solana/offchain-messages のREADMEを参照してください。

ウォレット開発者向け

ウォレットは、@solana/wallet-standard-features の solana:signOffchainMessage Wallet Standard機能を実装することで対応します。

  • version: "1.0.0" を報告し、署名可能なメッセージバージョンを supportedMessageVersions に列挙します。現時点では [1] です。
  • requiredSigners からバージョン1のプリアンブルを構築します。その際、requiredSigners をソートし、重複がないか確認してください。アカウントが必須署名者に含まれていない場合は、リクエストを拒否してください。
  • 署名の前に、ハードウェアデバイスを含め、メッセージのテキスト全文をユーザーに表示してください。
  • 署名した正確なバイト列を signedOffchainMessage として、Ed25519 の signature とともに返してください。
  • オフチェーンの Sign In With Solana に対応するには、solana:signIn に対して version: "1.1.0" を報告してください。入力に useOffchainMessage が含まれている場合は、SIWS テキストをバージョン1のオフチェーンメッセージとして署名し、そのバイト列を signedMessage として返したうえで、signedMessageFormat を設定してください。
  • solana:signMessage のサポートも引き続き維持してください。この機能は変更されておらず、アプリはフォールバックとして今も使用しています。

Is this page helpful?

© 2026 Solana Foundation. 無断転載を禁じます。