Skip to main content

ark/
message.rs

1//! Signing and verification of arbitrary messages.
2//!
3//! Messages are signed with a BIP-340 Schnorr signature over a
4//! SHA256 hash of the message bytes:
5//! ```text
6//! digest = SHA256( "bark/message" || message )
7//! ```
8//!
9//! The hash is prefixed with a constant string to prevent collisions
10//! with other signatures.
11//!
12//! A message can be verified either against a public key directly, or
13//! against an Ark [Address].
14
15use std::io::Write;
16
17use bitcoin::hashes::{Hash, sha256};
18use bitcoin::secp256k1::{schnorr, Keypair, Message, PublicKey};
19
20use crate::SECP;
21use crate::address::Address;
22
23const CHALLENGE_MESSAGE_PREFIX: &[u8] = b"bark/message";
24
25/// The digest to be signed for the given message
26fn signable_digest(message: &[u8]) -> Message {
27	let mut engine = sha256::Hash::engine();
28	engine.write_all(CHALLENGE_MESSAGE_PREFIX).unwrap();
29	engine.write_all(message).unwrap();
30
31	let hash = sha256::Hash::from_engine(engine).to_byte_array();
32	Message::from_digest(hash)
33}
34
35/// Sign an arbitrary message with the given keypair
36///
37/// The message is domain-separated by hashing it with a constant
38/// prefix, see the [module documentation](self) for the exact scheme.
39pub fn sign(keypair: &Keypair, message: &[u8]) -> schnorr::Signature {
40	SECP.sign_schnorr_with_aux_rand(&signable_digest(message), keypair, &rand::random())
41}
42
43/// Verify a message signature created by [sign] against
44/// the given public key
45pub fn verify(
46	pubkey: PublicKey,
47	message: &[u8],
48	signature: &schnorr::Signature,
49) -> bool {
50	SECP.verify_schnorr(signature, &signable_digest(message), &pubkey.into()).is_ok()
51}
52
53/// Verify a message signature created by [sign] against the
54/// user public key of the given [Address]
55pub fn verify_for_address(
56	address: &Address,
57	message: &[u8],
58	signature: &schnorr::Signature,
59) -> bool {
60	verify(address.policy().user_pubkey(), message, signature)
61}
62
63#[cfg(test)]
64mod test {
65	use std::str::FromStr;
66
67	use crate::address::VtxoDelivery;
68	use crate::mailbox::BlindedMailboxIdentifier;
69
70	use super::*;
71
72	#[test]
73	fn sign_and_verify() {
74		let keypair = Keypair::from_str(
75			"6b0f024af54172a9aed9a0f044689175787676c469ff2aa75024cae5445c7a02",
76		).unwrap();
77		let message = b"hello ark";
78
79		let sig = sign(&keypair, message);
80		assert!(verify(keypair.public_key(), message, &sig));
81
82		// wrong message
83		assert!(!verify(keypair.public_key(), b"hello bark", &sig));
84
85		// wrong key
86		let other = Keypair::from_str(
87			"0000000000000000000000000000000000000000000000000000000000000001",
88		).unwrap();
89		assert!(!verify(other.public_key(), message, &sig));
90
91		// an unprefixed plain sha256 signature must not verify
92		let plain = Message::from_digest(
93			bitcoin::hashes::sha256::Hash::hash(message).to_byte_array(),
94		);
95		let plain_sig = SECP.sign_schnorr_no_aux_rand(&plain, &keypair);
96		assert!(!verify(keypair.public_key(), message, &plain_sig));
97	}
98
99	#[test]
100	fn verify_message_for_address() {
101		let server = PublicKey::from_str(
102			"02037188bdd7579a0cd0b22a51110986df1ea08e30192658fe0e219590e4a723d3",
103		).unwrap();
104		let keypair = Keypair::from_str(
105			"6b0f024af54172a9aed9a0f044689175787676c469ff2aa75024cae5445c7a02",
106		).unwrap();
107		let blinded_id = BlindedMailboxIdentifier::from_pubkey(keypair.public_key());
108		let addr = Address::builder()
109			.server_pubkey(server)
110			.pubkey_policy(keypair.public_key())
111			.delivery(VtxoDelivery::ServerMailbox { blinded_id })
112			.into_address().unwrap();
113
114		let message = b"hello ark";
115		let sig = sign(&keypair, message);
116		assert!(verify_for_address(&addr, message, &sig));
117		assert!(!verify_for_address(&addr, b"hello bark", &sig));
118	}
119
120	/// Pins the signing scheme: this signature was created with
121	/// `sign_schnorr_no_aux_rand` over the prefixed digest. If this test
122	/// breaks, the message signing scheme changed, which breaks all
123	/// existing signatures in the wild.
124	#[test]
125	fn test_vector() {
126		let keypair = Keypair::from_str(
127			"6b0f024af54172a9aed9a0f044689175787676c469ff2aa75024cae5445c7a02",
128		).unwrap();
129		let message = b"ark signmessage test vector";
130
131		let digest = signable_digest(message);
132		let sig = SECP.sign_schnorr_no_aux_rand(&digest, &keypair);
133		assert_eq!(sig.to_string(),
134			"84e04017d4afeb4e945a2f69d2d969edfc5c15a0ced4397ea9dcc07a34bffb15\
135			f5ae22424126adc2bc23183b6949f0ec5113e5c99081ffde241c653772e9c64f",
136		);
137		assert!(verify(keypair.public_key(), message, &sig));
138	}
139}