bip322-rs 0.0.10

Implements BIP322 generic message signing and verification
Documentation
use bitcoin::{CompressedPublicKey, Network};

use super::*;

/// Verifies the BIP-322 simple from spec-compliant string encodings.
pub fn verify_simple_encoded(address: &str, message: &str, signature: &str) -> Result<()> {
  let address = Address::from_str(address).map_err(|_| error::AddressParse { address }.build())?;

  let mut cursor = bitcoin::io::Cursor::new(
    general_purpose::STANDARD
      .decode(signature)
      .context(error::SignatureDecode { signature })?,
  );

  let witness = Witness::consensus_decode_from_finite_reader(&mut cursor)
    .map_err(|_| error::WitnessMalformed)
    .unwrap();

  verify_simple(&address.assume_checked(), message.as_bytes(), witness)
}

/// Verifies the BIP-322 full from spec-compliant string encodings.
pub fn verify_full_encoded(address: &str, message: &str, to_sign: &str) -> Result<()> {
  let address = Address::from_str(address).map_err(|_| error::AddressParse { address }.build())?;

  let mut cursor = bitcoin::io::Cursor::new(
    general_purpose::STANDARD
      .decode(to_sign)
      .map_err(|_| error::TransactionBase64Decode {
        transaction: to_sign,
      })
      .unwrap(),
  );

  let to_sign = Transaction::consensus_decode_from_finite_reader(&mut cursor)
    .map_err(|_| error::TransactionConsensusDecode {
      transaction: to_sign,
    })
    .unwrap();

  verify_full(&address.assume_checked(), message.as_bytes(), to_sign)
}

/// Verifies the BIP-322 simple from proper Rust types.
pub fn verify_simple(address: &Address, message: &[u8], signature: Witness) -> Result<()> {
  verify_full(
    address,
    message,
    create_to_sign(&create_to_spend(address, message)?, Some(signature))?
      .extract_tx()
      .map_err(|_| error::TransactionExtract)
      .unwrap(),
  )
}

pub fn verify_full_witness(
  pubkey: &PublicKey,
  address: &Address,
  message: &[u8],
  witness: Witness,
) -> Result<()> {
  let to_spend = create_to_spend(address, message)?;
  let to_sign = create_to_sign(&to_spend, Some(witness))?;
  verify_address_pubkey(address, pubkey).and_then(|_| {
    verify_full(
      address,
      message,
      to_sign
        .extract_tx()
        .map_err(|_| error::TransactionExtract)
        .unwrap(),
    )
  })
}

/// Verifies if the provided address is derived from the public key.
/// Only P2TR and P2WPKH addresses are supported.
fn verify_address_pubkey(address: &Address, pubkey: &PublicKey) -> Result<()> {
  match address.to_address_data() {
    AddressData::Segwit { witness_program }
      if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 =>
    {
      let network = if address.to_string().starts_with("bc1") {
        Network::Bitcoin // Mainnet
      } else {
        Network::Testnet
      };

      verify_p2tr_address(address, pubkey, network)
    }
    AddressData::Segwit { witness_program }
      if witness_program.version().to_num() == 0 && witness_program.program().len() == 20 =>
    {
      let network = if address.to_string().starts_with("bc1") {
        Network::Bitcoin // Mainnet
      } else {
        Network::Testnet
      };

      verify_p2wpkh_address(address, pubkey, network)
    }
    _ => panic!("Unsupported address"),
  }
}

fn verify_p2tr_address(address: &Address, pubkey: &PublicKey, network: Network) -> Result<()> {
  let internal_key = XOnlyPublicKey::from_slice(&pubkey.to_bytes()[1..]).unwrap();
  let secp = Secp256k1::verification_only();
  let generated_address = Address::p2tr(&secp, internal_key, None, network);
  if generated_address.to_string() != address.to_string() {
    return Err(Error::PublicKeyMismatch);
  }

  Ok(())
}

fn verify_p2wpkh_address(address: &Address, pubkey: &PublicKey, network: Network) -> Result<()> {
  let generated_address =
    Address::p2wpkh(&CompressedPublicKey::try_from(*pubkey).unwrap(), network);
  if generated_address.to_string() != address.to_string() {
    return Err(Error::PublicKeyMismatch);
  }

  Ok(())
}

/// Verifies the BIP-322 full from proper Rust types.
fn verify_full(address: &Address, message: &[u8], to_sign: Transaction) -> Result<()> {
  match address.to_address_data() {
    AddressData::Segwit { witness_program }
      if witness_program.version().to_num() == 1 && witness_program.program().len() == 32 =>
    {
      let pub_key = XOnlyPublicKey::from_slice(witness_program.program().as_bytes())
        .map_err(|_| Error::InvalidPublicKey)?;

      verify_full_p2tr(address, message, to_sign, pub_key)
    }
    AddressData::Segwit { witness_program }
      if witness_program.version().to_num() == 0 && witness_program.program().len() == 20 =>
    {
      let pub_key =
        PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?;

      verify_full_p2wpkh(address, message, to_sign, pub_key, false)
    }
    AddressData::P2sh { script_hash: _ } => {
      let pub_key =
        PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?;

      verify_full_p2wpkh(address, message, to_sign, pub_key, true)
    }
    _ => Err(Error::UnsupportedAddress {
      address: address.to_string(),
    }),
  }
}

fn verify_full_p2wpkh(
  address: &Address,
  message: &[u8],
  to_sign: Transaction,
  pub_key: PublicKey,
  is_p2sh: bool,
) -> Result<()> {
  let to_spend = create_to_spend(address, message)?;
  let to_sign = create_to_sign(&to_spend, Some(to_sign.input[0].witness.clone()))?;

  let to_spend_outpoint = OutPoint {
    txid: to_spend.compute_txid(),
    vout: 0,
  };

  if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output {
    return Err(Error::ToSignInvalid);
  }

  let witness = if let Some(witness) = to_sign.inputs[0].final_script_witness.clone() {
    witness
  } else {
    return Err(Error::WitnessEmpty);
  };

  if witness.len() != 2 {
    return Err(Error::InvalidWitness);
  }

  let encoded_signature = witness.to_vec()[0].clone();
  let witness_pub_key = &witness.to_vec()[1];

  if &pub_key.to_bytes() != witness_pub_key {
    return Err(Error::PublicKeyMismatch);
  }

  let signature_length = encoded_signature.len();

  let (signature, sighash_type) = match signature_length {
    71 | 72 => (
      bitcoin::secp256k1::ecdsa::Signature::from_der(
        &encoded_signature.as_slice()[..signature_length - 1],
      )
      .map_err(|_| error::SignatureInvalid)
      .unwrap(),
      EcdsaSighashType::from_consensus(encoded_signature[signature_length - 1] as u32),
    ),
    _ => {
      return Err(Error::SignatureLength {
        length: encoded_signature.len(),
        encoded_signature,
      })
    }
  };

  if !(sighash_type == EcdsaSighashType::All) {
    return Err(Error::SigHashTypeUnsupported {
      sighash_type: sighash_type.to_string(),
    });
  }

  let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx);

  let sighash = sighash_cache
    .p2wpkh_signature_hash(
      0,
      &if is_p2sh {
        ScriptBuf::new_p2wpkh(&pub_key.wpubkey_hash().unwrap())
      } else {
        to_spend.output[0].script_pubkey.clone()
      },
      to_spend.output[0].value,
      sighash_type,
    )
    .expect("signature hash should compute");

  let message =
    Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash");

  Secp256k1::verification_only()
    .verify_ecdsa(&message, &signature, &pub_key.inner)
    .map_err(|_| error::SignatureInvalid.build())
}

fn verify_full_p2tr(
  address: &Address,
  message: &[u8],
  to_sign: Transaction,
  pub_key: XOnlyPublicKey,
) -> Result<()> {
  let to_spend = create_to_spend(address, message)?;
  let to_sign = create_to_sign(&to_spend, Some(to_sign.input[0].witness.clone()))?;

  let to_spend_outpoint = OutPoint {
    txid: to_spend.compute_txid(),
    vout: 0,
  };

  if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output {
    return Err(Error::ToSignInvalid);
  }

  let witness = if let Some(witness) = to_sign.inputs[0].final_script_witness.clone() {
    witness
  } else {
    return Err(Error::WitnessEmpty);
  };

  let encoded_signature = witness.to_vec()[0].clone();

  let (signature, sighash_type) = match encoded_signature.len() {
    65 => (
      Signature::from_slice(&encoded_signature.as_slice()[..64])
        .map_err(|_| error::SignatureInvalid)
        .unwrap(),
      TapSighashType::from_consensus_u8(encoded_signature[64])
        .map_err(|_| error::SigHashTypeInvalid)
        .unwrap(),
    ),
    64 => (
      Signature::from_slice(encoded_signature.as_slice())
        .map_err(|_| error::SignatureInvalid)
        .unwrap(),
      TapSighashType::Default,
    ),
    _ => {
      return Err(Error::SignatureLength {
        length: encoded_signature.len(),
        encoded_signature,
      })
    }
  };

  if !(sighash_type == TapSighashType::All || sighash_type == TapSighashType::Default) {
    return Err(Error::SigHashTypeUnsupported {
      sighash_type: sighash_type.to_string(),
    });
  }

  let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx);

  let sighash = sighash_cache
    .taproot_key_spend_signature_hash(
      0,
      &sighash::Prevouts::All(&[TxOut {
        value: Amount::from_sat(0),
        script_pubkey: to_spend.output[0].clone().script_pubkey,
      }]),
      sighash_type,
    )
    .expect("signature hash should compute");

  let message =
    Message::from_digest_slice(sighash.as_ref()).expect("should be cryptographically secure hash");

  Secp256k1::verification_only()
    .verify_schnorr(&signature, &message, &pub_key)
    .map_err(|_| error::SignatureInvalid.build())
}