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

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

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エンコードし、ウォレットアドレスとともにサーバーに送信してください。

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

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 は、クライアントが返してきたコピーを信用せず、サーバー上でnonceをキーにして取得してください。検証に成功したらnonceを消費し、同じ署名で二重にサインインできないようにします。

生メッセージを検証する

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");
}
}

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

@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 の時刻を含めて、nonceをキーにしたコピーを保持してください。サインインの検証に成功したら、nonceを消費します。

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 の Wallet Standard 機能 solana:signOffchainMessage を実装することで対応します。

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

Is this page helpful?

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