En bref. Il existe deux formats. Le format historique, spécifié après coup sous le nom de BIP-137, est une signature ECDSA récupérable de 65 octets sur "\x18Bitcoin Signed Message:\n" ‖ length ‖ message ; il prouve seulement le contrôle d’une clé, et son premier octet (de 27 à 42) indique quel type d’adresse le signataire visait. Bitcoin Core ne produit ce format que pour les adresses P2PKH historiques, et les portefeuilles divergent sur l’en-tête à utiliser pour les adresses SegWit. BIP-322 (statut Complete) prouve le contrôle d’une adresse : il construit une transaction virtuelle qui dépense depuis l’adresse, et la signature est le témoin (witness) de cette dépense, si bien qu’il fonctionne pour P2WPKH, P2TR, le multisig et les scripts. Depuis la version 2.0.0 du BIP, les signatures portent un préfixe : smp (simple), ful (complète) ou pof (preuve de fonds). Vérifiez avec une bibliothèque qui comprend le préfixe, et acceptez les signatures simples sans préfixe produites par les anciens portefeuilles.
Le format historique (BIP-137)
Le condensat est un double SHA-256 d’un préfixe fixe, de la longueur du message encodée en varint Bitcoin et des octets du message. La signature est header ‖ r ‖ s, soit 65 octets, généralement encodés en base64. L’en-tête porte l’identifiant de récupération (de 0 à 3), qui permet au vérificateur de calculer la clé publique à partir de la signature, plus une constante qui indique le type de clé et d’adresse (BIP-137, statut Deployed) :
| En-tête | Signification |
|---|---|
| 27–30 | P2PKH, clé publique non compressée |
| 31–34 | P2PKH, clé publique compressée |
| 35–38 | P2SH-P2WPKH (SegWit imbriqué) |
| 39–42 | P2WPKH (SegWit natif, bc1q…) |
La vérification récupère la clé publique, dérive l’adresse du type indiqué par l’en-tête, puis compare. Il n’y a ni transaction ni script : le format ne peut pas exprimer « cette adresse multisig » ou « cette adresse Taproot », seulement « le détenteur de cette clé ». BIP-137 renvoie lui-même à BIP-322 pour tout ce qui va au-delà.
Le module ci-dessous implémente le format historique et, plus bas, les signatures BIP-322 simples pour P2WPKH, avec @noble/curves et @scure/base :
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { ripemd160 } from "@noble/hashes/legacy.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { bech32, bech32m, createBase58check } from "@scure/base";
const b58 = createBase58check(sha256);
const sha256d = (b) => sha256(sha256(Uint8Array.from(b)));
const hash160 = (b) => ripemd160(sha256(b));
const utf8 = (s) => new TextEncoder().encode(s);
const cat = (...parts) => Uint8Array.from(parts.flatMap((p) => [...p]));
const varint = (n) => (n < 0xfd ? [n] : [0xfd, n & 0xff, n >> 8]); // messages under 64 KiB
export const NETWORKS = {
mainnet: { p2pkh: 0x00, p2sh: 0x05, hrp: "bc" },
regtest: { p2pkh: 0x6f, p2sh: 0xc4, hrp: "bcrt" },
};
/** Addresses of one compressed public key. */
export function addressesOf(pubkey, net = NETWORKS.mainnet) {
const h = hash160(pubkey);
const redeem = cat([0x00, 0x14], h); // P2SH-P2WPKH redeem script
return {
p2pkh: b58.encode(cat([net.p2pkh], h)),
"p2sh-p2wpkh": b58.encode(cat([net.p2sh], hash160(redeem))),
p2wpkh: bech32.encode(net.hrp, [0, ...bech32.toWords(h)]),
};
}
/** scriptPubKey of an address, for BIP-322's message_challenge. */
export function scriptPubKey(address, net = NETWORKS.mainnet) {
if (address.toLowerCase().startsWith(`${net.hrp}1`)) {
const isV0 = address.toLowerCase().startsWith(`${net.hrp}1q`);
const { words } = (isV0 ? bech32 : bech32m).decode(address);
const program = (isV0 ? bech32 : bech32m).fromWords(words.slice(1));
return cat([words[0] === 0 ? 0x00 : 0x50 + words[0], program.length], program);
}
const raw = b58.decode(address);
if (raw[0] === net.p2pkh) return cat([0x76, 0xa9, 0x14], raw.slice(1), [0x88, 0xac]);
if (raw[0] === net.p2sh) return cat([0xa9, 0x14], raw.slice(1), [0x87]);
throw new Error(`unsupported address ${address}`);
}
// ---------------------------------------------------------------------------- BIP-137 (legacy)
/** The digest every legacy message signature covers. */
export const legacyDigest = (message) => {
const m = utf8(message);
return sha256d(cat([0x18], utf8("Bitcoin Signed Message:\n"), varint(m.length), m));
};
const HEADER_BASE = { p2pkh: 31, "p2sh-p2wpkh": 35, p2wpkh: 39 }; // compressed keys
/** 65-byte compact signature, base64. headerFor "electrum" keeps 31-34 for every address type. */
export function signLegacy(message, privateKey, type = "p2pkh", headerFor = "bip137") {
const sig = secp256k1.Signature.fromBytes(secp256k1.sign(legacyDigest(message), privateKey, { prehash: false, format: "recovered" }), "recovered");
const base = headerFor === "electrum" ? 31 : HEADER_BASE[type];
return Buffer.from(cat([base + sig.recovery], sig.toBytes("compact"))).toString("base64");
}
/** Recovers the key and checks it against the address; strict mode also checks the header type. */
export function verifyLegacy(address, message, signatureBase64, { net = NETWORKS.mainnet, strict = false } = {}) {
const raw = Buffer.from(signatureBase64, "base64");
if (raw.length !== 65) throw new Error(`expected 65 bytes, got ${raw.length}`);
const header = raw[0];
if (header < 27 || header > 42) throw new Error(`header byte ${header} out of range`);
const recovery = (header - 27) % 4;
const typeFromHeader = header >= 39 ? "p2wpkh" : header >= 35 ? "p2sh-p2wpkh" : "p2pkh";
const compressed = header >= 31;
const sig = secp256k1.Signature.fromBytes(raw.subarray(1), "compact").addRecoveryBit(recovery);
const pubkey = sig.recoverPublicKey(legacyDigest(message)).toBytes(compressed);
const candidates = addressesOf(pubkey, net);
const match = Object.entries(candidates).find(([, a]) => a === address)?.[0];
if (!match) return { valid: false };
if (strict && match !== typeFromHeader) return { valid: false, reason: `header says ${typeFromHeader}, address is ${match}` };
return { valid: true, type: match, header };
}
// ---------------------------------------------------------------------------- BIP-322 (simple, P2WPKH)
const tag = sha256(utf8("BIP0322-signed-message"));
/** sha256(tag || tag || m), a BIP-340 tagged hash of the raw message. */
export const messageHash = (message) => sha256(cat(tag, tag, utf8(message)));
const le32 = (n) => [n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, n >>> 24];
const zero64 = new Array(8).fill(0);
/** The two virtual transactions of BIP-322, serialized without witness. Returns txids too. */
export function virtualTransactions(address, message, net = NETWORKS.mainnet) {
const challenge = scriptPubKey(address, net);
const toSpend = cat(
le32(0), [1], new Array(32).fill(0), le32(0xffffffff), // version 0, one input spending 000…000:FFFFFFFF
[34, 0x00, 0x20], messageHash(message), le32(0), // scriptSig OP_0 PUSH32[message_hash], nSequence 0
[1], zero64, [challenge.length], challenge, le32(0), // one output, value 0, scriptPubKey = challenge
);
const toSpendId = sha256d(toSpend); // internal byte order
const toSign = cat(le32(0), [1], toSpendId, le32(0), [0], le32(0), [1], zero64, [1, 0x6a], le32(0));
const display = (h) => Buffer.from(h).reverse().toString("hex");
return { challenge, toSpendId, toSpendTxid: display(toSpendId), toSignTxid: display(sha256d(toSign)) };
}
/** Verifies a BIP-322 simple signature for a P2WPKH address. Accepts the "smp" prefix or none. */
export function verifySimpleP2wpkh(address, message, signature, net = NETWORKS.mainnet) {
const b64 = signature.startsWith("smp") ? signature.slice(3) : signature;
const w = Buffer.from(b64, "base64");
// Witness stack: count, then <len><DER sig + sighash byte>, <len><33-byte pubkey>.
if (w[0] !== 2) throw new Error("expected a two-item witness stack");
const sigWithType = w.subarray(2, 2 + w[1]);
const pubkey = w.subarray(3 + w[1], 3 + w[1] + w[2 + w[1]]);
const { challenge, toSpendId } = virtualTransactions(address, message, net);
if (challenge[0] !== 0x00 || challenge[1] !== 20) throw new Error("not a P2WPKH address");
if (Buffer.compare(Buffer.from(hash160(pubkey)), Buffer.from(challenge.subarray(2))) !== 0) return false;
if (sigWithType.at(-1) !== 0x01) throw new Error("only SIGHASH_ALL is allowed");
// BIP-143 digest of to_sign input 0, spending to_spend:0 worth 0 sat.
const scriptCode = cat([0x19, 0x76, 0xa9, 0x14], challenge.subarray(2), [0x88, 0xac]);
const outputs = cat(zero64, [1, 0x6a]);
const preimage = cat(
le32(0), sha256d(cat(toSpendId, le32(0))), sha256d(le32(0)), toSpendId, le32(0),
scriptCode, zero64, le32(0), sha256d(outputs), le32(0), le32(1),
);
const der = sigWithType.subarray(0, -1);
return secp256k1.verify(der, sha256d(preimage), pubkey, { prehash: false, format: "der", lowS: true });
}
Ce que prend en charge Bitcoin Core
Les RPC signmessage et verifymessage de Bitcoin Core implémentent le format historique, et uniquement pour les adresses historiques. Un nœud regtest, une adresse de chaque type :
#!/bin/sh
# Bitcoin Core in regtest: which address types can sign and verify messages, and the exact errors.
set -e
DATA=/tmp/regtest
mkdir -p "$DATA"
bitcoind -regtest -datadir="$DATA" -daemonwait -fallbackfee=0.0001 >/dev/null
cli() { bitcoin-cli -regtest -datadir="$DATA" "$@"; }
cli -named createwallet wallet_name=lab >/dev/null
echo "$(bitcoind -version | head -1)"
MSG="ecdsa.com lab, 2026-10-20"
for type in legacy p2sh-segwit bech32 bech32m; do
addr=$(cli getnewaddress "" "$type")
printf '%-12s %s\n' "$type" "$addr"
if sig=$(cli signmessage "$addr" "$MSG" 2>/tmp/err); then
echo " signmessage: $sig"
echo " verifymessage: $(cli verifymessage "$addr" "$sig" "$MSG")"
echo "$addr $sig" > "/w/core-$type.txt"
else
echo " signmessage: $(tr '\n' ' ' < /tmp/err)"
fi
done
# A signature made elsewhere for a segwit address: Core cannot even attempt it.
seg=$(cli getnewaddress "" bech32)
leg=$(cut -d' ' -f2 /w/core-legacy.txt)
echo "verifymessage with a bech32 address: $(cli verifymessage "$seg" "$leg" "$MSG" 2>&1 | tr '\n' ' ')"
cli stop >/dev/null
Bitcoin Core daemon version v31.1.0 bitcoind
legacy mnaZRqP42LwTsVNQGfSb1eeTMvVzkaMoWp
signmessage: ICZ/Fs0I/i7XNJbC+ubYkwpDZXI0uFZU1oaqrSaJlS+hYavTSzdfPKmRkLJrUdi6/RXl9WsYNEW5OU9l9hfMHCI=
verifymessage: true
p2sh-segwit 2MyhmVFuFbTr4kAF1S4TtVmjEgkdouEntKK
signmessage: error code: -3 error message: Address does not refer to key
bech32 bcrt1qulxz0zapcld55a5twc9k88028y6dyxrggxug0n
signmessage: error code: -3 error message: Address does not refer to key
bech32m bcrt1pq8x39vj0qay20825g8m3kffzx7sqwu9lfzmtjejta94luunwkmfsdc0hnd
signmessage: error code: -3 error message: Address does not refer to key
verifymessage with a bech32 address: error code: -3 error message: Address does not refer to key
Toutes les adresses SegWit et Taproot provoquent l’erreur Address does not refer to key, aussi bien pour signer que pour vérifier une signature faite par quelqu’un d’autre. Le premier octet de la signature historique, 31 ou 32 lors de ces exécutions (H ou I en base64), désigne une clé P2PKH compressée.
Notre implémentation produit les mêmes octets que Bitcoin Core. Avec une clé générée pour l’exécution et importée dans le portefeuille regtest sous forme de descripteur pkh(), les deux ont signé le même message :
address mk1giEaBaAFPXNRMQcZNptPAp9kJmttKyp
core signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
our signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
identical: yes
core verifymessage(our signature): true
Les signatures sont identiques parce que les deux utilisent des nonces déterministes (RFC 6979) et la normalisation low-S ; un signataire qui utilise des nonces aléatoires produit à chaque fois une signature différente, tout aussi valide.
Le problème de l’en-tête pour les adresses SegWit
Les portefeuilles qui signent des messages au format historique pour des adresses SegWit ne s’accordent pas sur l’en-tête. Electrum signe avec un en-tête de 27 à 34 pour tous les types d’adresse et, à la vérification, essaie tous les types d’adresse à clé unique que la clé récupérée peut avoir (electrum/bitcoin.py). Trezor suit BIP-137 et ajoute 4 pour P2SH-P2WPKH et 8 pour P2WPKH, avec une option (no_script_type) pour produire plutôt la forme d’Electrum (trezor-firmware, sign_message.py). Un vérificateur doit donc choisir entre un contrôle strict (l’en-tête doit désigner le type de l’adresse) et un contrôle souple (n’importe quelle adresse de la clé récupérée). Voici les deux conventions, avec la même clé et le même message, soumises à notre vérificateur et à bip322-js 4.0.0 :
// Same key, same message, P2WPKH address: the BIP-137 header and the Electrum convention, and who accepts which.
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { Verifier } from "bip322-js";
import { addressesOf, signLegacy, verifyLegacy } from "./btcmsg.mjs";
const priv = secp256k1.utils.randomSecretKey();
const { p2pkh, p2wpkh } = addressesOf(secp256k1.getPublicKey(priv, true));
const message = "ecdsa.com lab, 2026-10-20";
const lib = (address, sig, strict) => {
try {
return Verifier.verifySignature(address, message, sig, strict);
} catch (e) {
return `throws: ${e.message}`;
}
};
for (const [label, sig] of [
["BIP-137 header (39-42)", signLegacy(message, priv, "p2wpkh", "bip137")],
["Electrum header (31-34)", signLegacy(message, priv, "p2wpkh", "electrum")],
]) {
const header = Buffer.from(sig, "base64")[0];
const loose = verifyLegacy(p2wpkh, message, sig);
const strict = verifyLegacy(p2wpkh, message, sig, { strict: true });
console.log(`${label}: header ${header}`);
console.log(` ours, loose: ${loose.valid} ours, strict: ${strict.valid}${strict.reason ? ` (${strict.reason})` : ""}`);
console.log(` bip322-js, loose: ${lib(p2wpkh, sig, false)} bip322-js, strict: ${lib(p2wpkh, sig, true)}`);
console.log(` same signature for the P2PKH address of the key: ours, loose ${verifyLegacy(p2pkh, message, sig).valid}`);
}
BIP-137 header (39-42): header 39
ours, loose: true ours, strict: true
bip322-js, loose: true bip322-js, strict: true
same signature for the P2PKH address of the key: ours, loose true
Electrum header (31-34): header 31
ours, loose: true ours, strict: false (header says p2pkh, address is p2wpkh)
bip322-js, loose: true bip322-js, strict: false
same signature for the P2PKH address of the key: ours, loose true
La vérification souple accepte les deux, et accepte aussi la même signature comme preuve pour l’adresse P2PKH de la clé. C’est là le sens véritable d’une signature historique : quiconque détient cette clé, pour toute adresse qui en est dérivée. La vérification stricte rejette toutes les signatures SegWit au format Electrum. Si vous devez accepter des signatures historiques pour des adresses SegWit, vérifiez en mode souple et consignez que ce que vous avez vérifié, c’est le contrôle d’une clé.
BIP-322 : prouver le contrôle d’une adresse
BIP-322 (bip-0322.mediawiki, statut Complete, version 2.0.0) réutilise la mécanique des transactions au lieu d’inventer un format de message. Deux transactions virtuelles sont construites :
to_spend: version 0, une entrée qui dépense la sortie inexistante (outpoint)000…000:FFFFFFFFavec un scriptSigOP_0 PUSH32[message_hash], une sortie de 0 satoshi dont le scriptPubKey est l’adresse à prouver.message_hashest un condensat étiqueté BIP-340,SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message)avec l’étiquetteBIP0322-signed-message.to_sign: version 0, une entrée qui dépenseto_spend:0, une sortie de 0 satoshi versOP_RETURN.
Signer consiste à dépenser to_spend:0 dans to_sign comme un portefeuille dépenserait n’importe quelle sortie de cette adresse. La vérification contrôle cette dépense selon les règles de script standard (SIGHASH_ALL ou le type par défaut de Taproot, low-S et DER strict, CLEANSTACK, entre autres). Comme aucune des deux transactions n’est valide sur le réseau, une signature ne peut pas être diffusée pour déplacer des fonds.
Encodages :
| Variante | Préfixe | Contenu | Usage |
|---|---|---|---|
| Simple | smp | la pile de témoin, en base64 | SegWit natif et Taproot, sans verrouillage temporel |
| Complète | ful | toute la transaction to_sign signée, en base64 | n’importe quel script, y compris SegWit imbriqué, historique et verrouillages temporels |
| Complète (preuve de fonds) | pof | un PSBT finalisé de to_sign avec des entrées supplémentaires | prouver le contrôle d’UTXO précis |
| Historique | aucun | la signature BIP-137 | autorisée uniquement pour les adresses P2PKH historiques |
Les préfixes de variante sont une nouveauté de la version 2.0.0, et les signataires DOIVENT désormais les inclure. Pour la compatibilité, « un vérificateur pourrait supposer la variante simple en l’absence de préfixe », ce qui permet aux signatures des portefeuilles conçus d’après la version antérieure du texte de continuer à fonctionner. La vérification a trois issues, et non deux : valide, invalide, ou non concluante lorsque le vérificateur ne peut pas évaluer les scripts en jeu.
Multisig et portefeuilles matériels : signer via PSBT
Pour une adresse multisig, le témoin nécessite les signatures de plusieurs clés, détenues par des personnes ou des appareils différents. BIP-322 y répond avec les outils dont les utilisateurs de multisig disposent déjà : la transaction to_sign circule sous forme de PSBT avec un nouveau champ global, PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (type 0x09), qui porte le message. Un signataire qui voit ce champ, une première entrée qui dépense la sortie 0 du to_spend correspondant et une unique sortie OP_RETURN de valeur nulle doit présenter à l’utilisateur le message et l’adresse plutôt qu’une transaction ; il signe ensuite comme il signerait une dépense. Chaque cosignataire ajoute une signature partielle ; le résultat finalisé devient une signature ful. Un appareil qui ne reconnaît pas le champ voit ce qui ressemble à une transaction dépensant 0 satoshi : c’est pourquoi la prise en charge de ce flux par les portefeuilles compte davantage que la cryptographie.
Le BIP cite deux implémentations de référence : une pull request de Bitcoin Core pour la prise en charge de base (#24058), fermée sans avoir été fusionnée, et une pull request de btcd pour la prise en charge complète (#2521), source des vecteurs de test générés et toujours ouverte au 20 octobre 2026. Aucune des deux ne fait partie d’une version publiée, ce qui explique en partie pourquoi les portefeuilles et les bibliothèques du tableau ci-dessous qui prennent en charge BIP-322 l’implémentent chacun de leur côté.
Contrôle avec les vecteurs de test du BIP
BIP-322 fournit des vecteurs de test de base : les condensats de message et les identifiants de transaction de trois messages, ainsi que des signatures simples pour une adresse P2WPKH. Pour les vérifier, il suffit de l’adresse, du message et de la signature. Les mêmes signatures, avec et sans le nouveau préfixe, ont aussi été soumises à bip322-js 4.0.0 :
// BIP-322's published test vectors (addresses, messages and signatures only), against btcmsg.mjs and bip322-js 4.0.0.
import { readFileSync } from "node:fs";
import { Verifier } from "bip322-js";
import { messageHash, verifySimpleP2wpkh, virtualTransactions } from "./btcmsg.mjs";
const vectors = JSON.parse(readFileSync("bip322-basic-test-vectors.json", "utf8"));
for (const v of vectors.tx_hashes) {
const { toSpendTxid, toSignTxid } = virtualTransactions(v.address, v.message);
const ok = Buffer.from(messageHash(v.message)).toString("hex") === v.message_hash && toSpendTxid === v.to_spend_tx_hash && toSignTxid === v.to_sign_tx_hash;
console.log(`hashes for ${JSON.stringify(v.message).padEnd(16).slice(0, 16)} message_hash, to_spend, to_sign match: ${ok}`);
}
for (const v of vectors.simple.filter((x) => x.type === "p2wpkh")) {
for (const sig of v.bip322_signatures) {
const ours = verifySimpleP2wpkh(v.address, v.message, sig);
const call = (s) => { try { return Verifier.verifySignature(v.address, v.message, s); } catch (e) { return `throws: ${e.message}`; } };
console.log(`${JSON.stringify(v.message).padEnd(14)} ${sig.slice(0, 12)}... ours: ${ours} | bip322-js: ${call(sig)} | bip322-js, prefix removed: ${call(sig.slice(3))}`);
}
}
const wrong = vectors.error.find((e) => e.description.startsWith("wrong message for valid simple p2wpkh"));
console.log(`error vector "${wrong.description}": ours ${verifySimpleP2wpkh(wrong.address, wrong.message, wrong.signature)}`);
hashes for "" message_hash, to_spend, to_sign match: true
hashes for "Hello World" message_hash, to_spend, to_sign match: true
hashes for "UTF-8 support: message_hash, to_spend, to_sign match: true
"" smpAkcwRAIgM... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"" smpAkgwRQIhA... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"Hello World" smpAkcwRAIgZ... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"Hello World" smpAkgwRQIhA... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
error vector "wrong message for valid simple p2wpkh signature (empty message was signed)": ours false
L’implémentation ci-dessus reproduit chaque condensat, vérifie chaque signature P2WPKH simple, avec ou sans préfixe, et rejette le vecteur d’erreur. La bibliothèque JavaScript bip322-js 4.0.0, très utilisée, ne comprend pas le préfixe smp : elle décode le préfixe comme du base64 et lève l’erreur buffer too small, alors que la même signature sans le préfixe se vérifie. Les signatures produites par les portefeuilles récents échoueront donc dans les services qui reposent sur elle tant qu’elle n’aura pas été mise à jour ; retirer un préfixe connu avant de l’appeler est une solution de transition viable pour les signatures simples.
Qui produit et qui accepte quoi
D’après le code source de chaque projet, ou un test lorsque c’est indiqué, en octobre 2026 :
| Logiciel | Historique (BIP-137) | BIP-322 | Remarques |
|---|---|---|---|
| Bitcoin Core 31.1 | P2PKH uniquement | non | testé ci-dessus |
| Electrum (master) | P2PKH, P2WPKH, P2SH-P2WPKH, en-tête 27–34 | non | vérifie les deux conventions d’en-tête ; refuse les adresses sans clé unique |
| Firmware Trezor (main) | P2PKH, P2SH-P2WPKH, P2WPKH, en-têtes BIP-137 | non | Taproot : « Unsupported script type » |
| Sparrow (drongo master) | oui | simple, avec les préfixes smp/ful/pof | vérification : variante simple, P2WPKH et P2TR à clé unique |
| bip322-js 4.0.0 | oui, en mode souple ou strict | simple, P2WPKH, P2SH-P2WPKH, P2TR à clé unique | sans préfixes ; voir l’encadré ci-dessus |
Conséquences pratiques : une adresse Taproot ne peut être prouvée qu’avec BIP-322 ; une adresse multisig ne peut pas du tout être prouvée avec le format historique ; et un service qui veut accepter les preuves des utilisateurs de tous ces portefeuilles doit accepter les signatures historiques (en mode souple pour SegWit), les signatures BIP-322 simples sans préfixe, et celles qui en ont un.
Vérifier dans un service
- Fixez le message vous-même. Incluez votre domaine, un nonce ou un horodatage, et la finalité (
"Proof of ownership for example.com, nonce 7f3a…"), et n’acceptez chaque nonce qu’une seule fois, exactement comme avec Sign-In with Ethereum. Une signature sur un texte générique peut être rejouée auprès de n’importe quel autre service qui demande le même texte. - Comparez les octets exacts. Les deux formats signent le message brut. L’interface d’Electrum supprime les espaces en début et en fin de texte avant de signer ; un saut de ligne final laissé par un copier-coller, ou des fins de ligne CRLF, produisent un condensat différent et une « signature invalide ».
- Détectez le format, dans cet ordre : un préfixe
smp,fuloupof; une chaîne base64 qui se décode en exactement 65 octets dont le premier est compris entre 27 et 42 (format historique) ; sinon, essayez une signature BIP-322 simple sans préfixe. - Décidez de ce que prouve une signature historique pour les adresses SegWit : la vérification souple prouve le contrôle de la clé, ce qui, pour les adresses à clé unique, revient au contrôle de l’adresse.
- Considérez un résultat non concluant comme non vérifié. Un vérificateur qui ne peut pas évaluer un script (verrouillages temporels, scripts inhabituels) ne doit pas signaler de succès.
- Pour une preuve de fonds, consultez la blockchain. Une signature
pofprouve le contrôle des sorties listées au moment de la signature ; savoir si elles sont toujours non dépensées relève d’une consultation distincte, et une telle signature ne prouve jamais qu’il s’agit de toutes les sorties d’une adresse.
Sources
- BIP-137 : Signatures of Messages using Private Keys
- BIP-322 : Generic Signed Message Format, version 2.0.0, et ses vecteurs de test de base
- BIP-340 (condensats étiquetés), BIP-143 (condensat de signature SegWit), BIP-174 (PSBT)
- RFC 6979 : Deterministic DSA and ECDSA
- Bitcoin Core 31.1, RPC
signmessageetverifymessage - Electrum
bitcoin.py, Trezorsign_message.py, Sparrow drongoBip322.java, bip322-js