Skip to content

Commit

Permalink
Merge pull request #8 from ACken2/7-validating-messages
Browse files Browse the repository at this point in the history
Fixed issue #7
  • Loading branch information
ACken2 authored Apr 5, 2024
2 parents 159456f + 9ff43b5 commit 8b684fa
Show file tree
Hide file tree
Showing 6 changed files with 442 additions and 85 deletions.
26 changes: 23 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"license": "MIT",
"devDependencies": {
"@types/chai": "^4.3.5",
"@types/elliptic": "^6.4.18",
"@types/mocha": "^10.0.1",
"@types/node": "^20.2.5",
"@types/secp256k1": "^4.0.3",
Expand All @@ -39,6 +40,7 @@
"bitcoinjs-lib": "^6.1.1",
"bitcoinjs-message": "^2.2.0",
"ecpair": "^2.1.0",
"elliptic": "^6.5.5",
"fast-sha256": "^1.3.0",
"secp256k1": "^5.0.0"
}
Expand Down
122 changes: 90 additions & 32 deletions src/Verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class Verifier {
* @throws If the provided signature fails basic validation, or if unsupported address and signature are provided
*/
public static verifySignature(signerAddress: string, message: string, signatureBase64: string) {
// Check whether the given signerAddress is valid
if (!Address.isValidBitcoinAddress(signerAddress)) {
throw new Error("Invalid Bitcoin address is provided.");
}
// Handle legacy BIP-137 signature
// For P2PKH address, assume the signature is also a legacy signature
if (Address.isP2PKH(signerAddress) || BIP137.isBIP137Signature(signatureBase64)) {
Expand Down Expand Up @@ -121,44 +125,98 @@ class Verifier {
* @throws If the provided signature fails basic validation, or if unsupported address and signature are provided
*/
private static verifyBIP137Signature(signerAddress: string, message: string, signatureBase64: string) {
if (Address.isP2PKH(signerAddress)) {
return bitcoinMessage.verify(message, signerAddress, signatureBase64);
// Recover the public key associated with the signature
const publicKeySignedRaw = BIP137.derivePubKey(message, signatureBase64);
// Compress and uncompress the public key if necessary
let publicKeySignedUncompressed: Buffer;
let publicKeySigned: Buffer;
if (publicKeySignedRaw.byteLength === 65) {
publicKeySignedUncompressed = publicKeySignedRaw; // The key recovered is an uncompressed key
publicKeySigned = Address.compressPublicKey(publicKeySignedRaw);
}
else {
// Recover the public key associated with the signature
const publicKeySigned = BIP137.derivePubKey(message, signatureBase64);
// Set the equivalent legacy address to prepare for validation from bitcoinjs-message
const legacySigningAddress = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2pkh').mainnet;
// Make sure that public key recovered corresponds to the claimed signing address
if (Address.isP2SH(signerAddress)) {
// Assume it is a P2SH-P2WPKH address, derive a P2SH-P2WPKH address based on the public key recovered
const p2shAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2sh-p2wpkh');
// Assert that the derived address is identical to the claimed signing address
if (p2shAddressDerived.mainnet !== signerAddress && p2shAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
publicKeySignedUncompressed = Address.uncompressPublicKey(publicKeySignedRaw);
publicKeySigned = publicKeySignedRaw; // The key recovered is a compressed key
}
// Obtain the equivalent signing address in all address types (except taproot) to prepare for validation from bitcoinjs-message
// Taproot address is not needed since technically BIP-137 signatures does not support taproot address
const p2pkhSigningAddressUncompressed = Address.convertPubKeyIntoAddress(publicKeySignedUncompressed, 'p2pkh').mainnet;
const p2pkhSigningAddressCompressed = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2pkh').mainnet;
const p2shSigningAddress = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2sh-p2wpkh').mainnet;
const p2wpkhSigningAddress = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2wpkh').mainnet;
// Make sure that public key recovered corresponds to the claimed signing address
if (Address.isP2PKH(signerAddress)) {
// Derive P2PKH address from both the uncompressed raw public key, and the compressed public key
const p2pkhAddressDerivedUncompressed = Address.convertPubKeyIntoAddress(publicKeySignedUncompressed, 'p2pkh');
const p2pkhAddressDerivedCompressed = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2pkh');
// Assert that the derived address is identical to the claimed signing address
if (
p2pkhAddressDerivedUncompressed.mainnet !== signerAddress && p2pkhAddressDerivedUncompressed.testnet !== signerAddress &&
p2pkhAddressDerivedCompressed.mainnet !== signerAddress && p2pkhAddressDerivedCompressed.testnet !== signerAddress
) {
return false; // Derived address did not match with the claimed signing address
}
else if (Address.isP2WPKH(signerAddress)) {
// Assume it is a P2WPKH address, derive a P2WPKH address based on the public key recovered
const p2wpkhAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2wpkh');
// Assert that the derived address is identical to the claimed signing address
if (p2wpkhAddressDerived.mainnet !== signerAddress && p2wpkhAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
}
else if (Address.isP2SH(signerAddress)) {
// Assume it is a P2SH-P2WPKH address, derive a P2SH-P2WPKH address based on the public key recovered
const p2shAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2sh-p2wpkh');
// Assert that the derived address is identical to the claimed signing address
if (p2shAddressDerived.mainnet !== signerAddress && p2shAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
else if (Address.isP2TR(signerAddress)) {
// Assume it is a P2TR address, derive a P2TR address based on the public key recovered
const p2trAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2tr');
// Assert that the derived address is identical to the claimed signing address
if (p2trAddressDerived.mainnet !== signerAddress && p2trAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
}
else if (Address.isP2WPKH(signerAddress)) {
// Assume it is a P2WPKH address, derive a P2WPKH address based on the public key recovered
const p2wpkhAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2wpkh');
// Assert that the derived address is identical to the claimed signing address
if (p2wpkhAddressDerived.mainnet !== signerAddress && p2wpkhAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
else {
return false; // Unsupported address type
}
else {
// Assume it is a P2TR address, derive a P2TR address based on the public key recovered
const p2trAddressDerived = Address.convertPubKeyIntoAddress(publicKeySigned, 'p2tr');
// Assert that the derived address is identical to the claimed signing address
if (p2trAddressDerived.mainnet !== signerAddress && p2trAddressDerived.testnet !== signerAddress) {
return false; // Derived address did not match with the claimed signing address
}
// Validate the signature using bitcoinjs-message if address assertion succeeded
return bitcoinMessage.verify(message, legacySigningAddress, signatureBase64);
}
// Validate the signature using bitcoinjs-message if address assertion succeeded
// Accept the signature if it originates from any address derivable from the public key
const validity = (
this.bitcoinMessageVerifyWrap(message, p2pkhSigningAddressUncompressed, signatureBase64) ||
this.bitcoinMessageVerifyWrap(message, p2pkhSigningAddressCompressed, signatureBase64) ||
this.bitcoinMessageVerifyWrap(message, p2shSigningAddress, signatureBase64) ||
this.bitcoinMessageVerifyWrap(message, p2wpkhSigningAddress, signatureBase64)
);
return validity;
}

/**
* Wraps the Bitcoin message verification process to avoid throwing exceptions.
* This method attempts to verify a BIP-137 message using the provided address and
* signature. It encapsulates the verification process within a try-catch block,
* catching any errors that occur during verification and returning false instead
* of allowing the exception to propagate.
*
* The process is as follows:
* 1. The `bitcoinjs-message.verify` function is called with the message, address,
* and signature provided in Base64 encoding.
* 2. If the verification is successful, the method returns true.
* 3. If any error occurs during the verification, the method catches the error
* and returns false, signaling an unsuccessful verification.
*
* @param message The Bitcoin message to be verified.
* @param address The Bitcoin address to which the message is allegedly signed.
* @param signatureBase64 The Base64 encoded signature corresponding to the message.
* @return boolean Returns true if the message is successfully verified, otherwise false.
*/
private static bitcoinMessageVerifyWrap(message: string, address: string, signatureBase64: string) {
try {
return bitcoinMessage.verify(message, address, signatureBase64);
}
catch (err) {
return false; // Instead of throwing, just return false
}
}

Expand Down
116 changes: 116 additions & 0 deletions src/helpers/Address.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Import dependency
import { ec as EC } from 'elliptic';
import * as bitcoin from 'bitcoinjs-lib';

/**
Expand Down Expand Up @@ -199,6 +200,121 @@ class Address {
}
}

/**
* Validates a given Bitcoin address.
* This method checks if the provided Bitcoin address is valid by attempting to decode it
* for different Bitcoin networks: mainnet, testnet, and regtest. The method uses the
* bitcoinjs-lib's address module for decoding.
*
* The process is as follows:
* 1. Attempt to decode the address for the Bitcoin mainnet. If decoding succeeds,
* the method returns true, indicating the address is valid for mainnet.
* 2. If the first step fails, catch the resulting error and attempt to decode the
* address for the Bitcoin testnet. If decoding succeeds, the method returns true,
* indicating the address is valid for testnet.
* 3. If the second step fails, catch the resulting error and attempt to decode the
* address for the Bitcoin regtest network. If decoding succeeds, the method returns
* true, indicating the address is valid for regtest.
* 4. If all attempts fail, the method returns false, indicating the address is not valid
* for any of the checked networks.
*
* @param address The Bitcoin address to validate.
* @return boolean Returns true if the address is valid for any of the Bitcoin networks,
* otherwise returns false.
*/
public static isValidBitcoinAddress(address: string): boolean {
try {
// Attempt to decode the address using bitcoinjs-lib's address module at mainnet
bitcoin.address.toOutputScript(address, bitcoin.networks.bitcoin);
return true; // If decoding succeeds, the address is valid
}
catch (error) { }
try {
// Attempt to decode the address using bitcoinjs-lib's address module at testnet
bitcoin.address.toOutputScript(address, bitcoin.networks.testnet);
return true; // If decoding succeeds, the address is valid
}
catch (error) { }
try {
// Attempt to decode the address using bitcoinjs-lib's address module at regtest
bitcoin.address.toOutputScript(address, bitcoin.networks.regtest);
return true; // If decoding succeeds, the address is valid
}
catch (error) { }
return false; // Probably not a valid address
}

/**
* Compresses an uncompressed public key using the elliptic curve secp256k1.
* This method takes a public key in its uncompressed form and returns a compressed
* representation of the public key. Elliptic curve public keys can be represented in
* a shorter form known as compressed format which saves space and still retains the
* full public key's capabilities. The method uses the elliptic library to convert the
* uncompressed public key into its compressed form.
*
* The steps involved in the process are:
* 1. Initialize a new elliptic curve instance for the secp256k1 curve.
* 2. Create a key pair object from the uncompressed public key buffer.
* 3. Extract the compressed public key from the key pair object.
* 4. Return the compressed public key as a Buffer object.
*
* @param uncompressedPublicKey A Buffer containing the uncompressed public key.
* @return Buffer Returns a Buffer containing the compressed public key.
* @throws Error Throws an error if the provided public key cannot be compressed,
* typically indicating that the key is not valid.
*/
public static compressPublicKey(uncompressedPublicKey: Buffer): Buffer {
// Initialize elliptic curve
const ec = new EC('secp256k1');
// Try to compress the provided public key
try {
// Create a key pair from the uncompressed public key buffer
const keyPair = ec.keyFromPublic(Buffer.from(uncompressedPublicKey));
// Get the compressed public key as a Buffer
const compressedPublicKey = Buffer.from(keyPair.getPublic(true, 'array'));
return compressedPublicKey;
}
catch (err) {
throw new Error('Fails to compress the provided public key. Please check if the provided key is a valid uncompressed public key.');
}
}

/**
* Uncompresses a given public key using the elliptic curve secp256k1.
* This method accepts a compressed public key and attempts to convert it into its
* uncompressed form. Public keys are often compressed to save space, but certain
* operations require the full uncompressed key. This method uses the elliptic
* library to perform the conversion.
*
* The function operates as follows:
* 1. Initialize a new elliptic curve instance using secp256k1.
* 2. Attempt to create a key pair from the compressed public key buffer.
* 3. Extract the uncompressed public key from the key pair object.
* 4. Return the uncompressed public key as a Buffer object.
* If the compressed public key provided is invalid and cannot be uncompressed,
* the method will throw an error with a descriptive message.
*
* @param compressedPublicKey A Buffer containing the compressed public key.
* @return Buffer The uncompressed public key as a Buffer.
* @throws Error Throws an error if the provided public key cannot be uncompressed,
* typically indicating that the key is not valid.
*/
public static uncompressPublicKey(compressedPublicKey: Buffer): Buffer {
// Initialize elliptic curve
const ec = new EC('secp256k1');
// Try to compress the provided public key
try {
// Create a key pair from the compressed public key buffer
const keyPair = ec.keyFromPublic(Buffer.from(compressedPublicKey));
// Get the compressed public key as a Buffer
const uncompressedPublicKey = Buffer.from(keyPair.getPublic(false, 'array'));
return uncompressedPublicKey;
}
catch (err) {
throw new Error('Fails to uncompress the provided public key. Please check if the provided key is a valid compressed public key.');
}
}

}

export default Address;
Loading

1 comment on commit 8b684fa

@5twelve
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm litt

Please sign in to comment.