cryptographyethereumpublic-keygoogle-cloud-kms

How to derive ethereum address from Elliptic Curve secp256k1 - SHA256 Digest


I am using google cloud key management service to generate and manage keys. I have generated the HSM key for Asymmetric signing using Elliptic Curve secp256k1 - SHA256 Digest. The public key is something as below -

{
  pem: '-----BEGIN PUBLIC KEY-----\n' +
    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' +
    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' +
    '-----END PUBLIC KEY-----\n',
  algorithm: 'EC_SIGN_SECP256K1_SHA256',
  pemCrc32c: { value: '12345678' },
  name: 'PATH-TO-KEY-ON-KMS/cryptoKeyVersions/1',
  protectionLevel: 'HSM'
}

I am looking to derive Ethereum address from this so that I can fund the wallet and perform signing. For the same I have written a function as below -

const deriveEthAddress = async () => {
    const publicKey = await getPublicKey(); // this returns same key as show above snippet
    const address = keccak256(publicKey.pem);
    const hexAddress = address.toString('hex');
    return '0x' + hexAddress.toString('hex').substring(hexAddress.length - 40, hexAddress.length)
}

This function gives me ethereum checksum verified address, but not sure is it the correct way to do this. Is this solution correct or needs improvement?

Example public key:

publicKey {
  pem: '-----BEGIN PUBLIC KEY-----\n' +
    'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEeYRv0S7Zf5CNh5/APxiT6xoY+z521DHT\n' +
    'FgLdUPUP2e/3jkYDuZTbCHP8zEHm7nhG6AUOpJCbTF2J2vWkC1i3Yg==\n' +
    '-----END PUBLIC KEY-----\n',
  algorithm: 'EC_SIGN_SECP256K1_SHA256',
  pemCrc32c: { value: '41325621' },
  name: 'PATH-TO-KEY-ON-KMS/cryptoKeyVersions/1',
  protectionLevel: 'HSM'
}

And, ethereum address I am deriving is - 0x8aCd56527DfE9205edf7D6F1EB39A5c9aa8aaE3F


Solution

  • You must not use the PEM encoded public key when determining the Keccak hash, but must apply the raw public key, i.e. the concatenation of the hex encoded x and y value.
    This can be derived most easily from the PEM encoded key by converting it to a DER encoded key. The last 64 bytes of the DER encoded key correspond to the raw public key (at least for secp256k1, the elliptic curve used by Ethereum):

    var publicKey = {
      pem: '-----BEGIN PUBLIC KEY-----\n' +
      ...
    }
    
    // Export raw public key (without 0x04 prefix)
    var x509pem = publicKey.pem;
    var x509der = crypto.createPublicKey(x509pem).export({format: 'der', type: 'spki'});
    var rawXY = x509der.subarray(-64);
    console.log('Raw key: 0x' + rawXY.toString('hex')); // 79846fd12ed97f908d879fc03f1893eb1a18fb3e76d431d31602dd50f50fd9eff78e4603b994db0873fccc41e6ee7846e8050ea4909b4c5d89daf5a40b58b762
    

    For your pem encoded public key the raw public key is (hex encoded):

    79846fd12ed97f908d879fc03f1893eb1a18fb3e76d431d31602dd50f50fd9eff78e4603b994db0873fccc41e6ee7846e8050ea4909b4c5d89daf5a40b58b762
    

    The Ethereum address derived from this is (hex encoded):

    fd55ad0678e9b90d5f5175d7ce5fd1ebd440309d
    

    or with checksum:

    Fd55aD0678E9b90D5f5175d7cE5fD1eBd440309D
    

    which can be verified on https://www.rfctools.com/ethereum-address-test-tool.


    Full code:

    const crypto = require('crypto');
    const keccak256 = require('keccak256');
    
    var publicKey = {
      pem: '-----BEGIN PUBLIC KEY-----\n' +
        'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEeYRv0S7Zf5CNh5/APxiT6xoY+z521DHT\n' +
        'FgLdUPUP2e/3jkYDuZTbCHP8zEHm7nhG6AUOpJCbTF2J2vWkC1i3Yg==\n' +
        '-----END PUBLIC KEY-----\n',
      algorithm: 'EC_SIGN_SECP256K1_SHA256',
      pemCrc32c: { value: '41325621' },
      name: 'PATH-TO-KEY-ON-KMS/cryptoKeyVersions/1',
      protectionLevel: 'HSM'
    }
    
    // Export raw public key (without 0x04 prefix)
    var x509pem = publicKey.pem;
    var x509der = crypto.createPublicKey(x509pem).export({format: 'der', type: 'spki'});
    var rawXY = x509der.subarray(-64);
    console.log('Raw key: 0x' + rawXY.toString('hex')); // 79846fd12ed97f908d879fc03f1893eb1a18fb3e76d431d31602dd50f50fd9eff78e4603b994db0873fccc41e6ee7846e8050ea4909b4c5d89daf5a40b58b762
    
    // Derive address from raw public key
    var hashXY = keccak256(rawXY);
    var address = hashXY.subarray(-20).toString('hex').toLowerCase();
    
    // Calculate checksum (expressed as upper/lower case in the address)
    var addressHash = keccak256(address).toString('hex');
    var addressChecksum = '';
    for (var i = 0; i < address.length; i++){
        if (parseInt(addressHash[i], 16) > 7) {
            addressChecksum += address[i].toUpperCase();
        } else {
            addressChecksum += address[i];
        }
    }
    
    console.log('Derived: 0x' + addressChecksum);                       // 0xFd55aD0678E9b90D5f5175d7cE5fD1eBd440309D
    console.log('Test:    0xFd55aD0678E9b90D5f5175d7cE5fD1eBd440309D'); // from https://www.rfctools.com/ethereum-address-test-tool using the raw key