Skip to main content

Playing with ethereum secp256k1 keys

ยท 4 min read

In this article, we will generate a secp256k1 key with openssl, analyse the content of the public key .pem file and generate the Ethereum address from it.

Generate keys with OpenSSLโ€‹

Let's generate a private key using OpenSSL:

openssl ecparam -name secp256k1 -genkey -noout -out priv-key.pem

and create the corresponding public key:

openssl ec -in priv-key.pem -pubout > pub-key.pem

You can also generate this file:

openssl ec -in priv-key.pem -text -noout -out priv-pub-key

The -noout option prevents output of the encoded version of the request.

You should have a file like this:

public-key.pem
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE95AekWG5tT8uHyew8eRxH8yPI0qQ9V/S
BopnsVKUg4nA7h5A90oOGU73wrWWZicLFtUs9YX9jmX8AJWPeK93sA==
-----END PUBLIC KEY-----

From .pem to uncompressed hex public keyโ€‹

The content of the file above is encoded in base64, let's get our public key from it in hex.

const pem =
"-----BEGIN PUBLIC KEY-----\n" +
"MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE95AekWG5tT8uHyew8eRxH8yPI0qQ9V/S\n" +
"BopnsVKUg4nA7h5A90oOGU73wrWWZicLFtUs9YX9jmX8AJWPeK93sA==\n" +
"-----END PUBLIC KEY-----\n";

const b64Final = pem
.replace(/\n/g, "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "");

console.log(b64Final);
// MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE95AekWG5tT8uHyew8eRxH8yPI0qQ9V/SBopnsVKUg4nA7h5A90oOGU73wrWWZicLFtUs9YX9jmX8AJWPeK93sA==

According to RFC5480, the structure is:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}

In our case, the AlgorithmIdentifier is[^1], [^2]:

SEQUENCE { OID 1.2.840.10045.2.1 (ecPublicKey), OID 1.3.132.0.10 (secp256k1) }

Let's check the hex value of the key:

const b64Final =
"MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE95AekWG5tT8uHyew8eRxH8yPI0qQ9V/SBopnsVKUg4nA7h5A90oOGU73wrWWZicLFtUs9YX9jmX8AJWPeK93sA==";

const buffer = Buffer.from(b64Final, "base64");
const pemHex = buffer.toString("hex");

console.log(pemHex);
// 3056301006072a8648ce3d020106052b8104000a03420004f7901e9161b9b53f2e1f27b0f1e4711fcc8f234a90f55fd2068a67b152948389c0ee1e40f74a0e194ef7c2b59666270b16d52cf585fd8e65fc00958f78af77b0

We can analyse this hexadecimal string in an hexadecimal editor:

hex editor

In hexadecimal, each group of 2 characters represents one byte. The algorithm (AlgorithmIdentifier) is encoded in the first 24 bytes. You can use an Online Object Identifier DER Encoder to find values above:

NameOIDHEX
ecPublicKey1.2.840.10045.2.106 07 2A 86 48 CE 3D 02 01
secp256k11.3.132.0.1006 05 2B 81 04 00 0A

The subjectPublicKey is encoded in the remaining 26 bytes. To remove the first 24 bytes, we can remove the first 2*24 hexadecimal characters from the pemHex string above.

Finally, we need to add the 0x04 prefix (0x is for hexadecimal, but why 04?) - (0x04 | PubKeyX(32B) | PubKeyY(32B)) and we get the uncompressed public key:

Uncompressed public key
0x04f7901e9161b9b53f2e1f27b0f1e4711fcc8f234a90f55fd2068a67b152948389c0ee1e40f74a0e194ef7c2b59666270b16d52cf585fd8e65fc00958f78af77b0

You can check if the hex value above matches the pub: value you have in the priv-pub-key file generated earlier.

Private-Key: (256 bit)
priv:
00:...
pub:
04:f7:90:1e:91:61:b9:b5:3f:2e:1f:27:b0:f1:e4:
71:1f:cc:8f:23:4a:90:f5:5f:d2:06:8a:67:b1:52:
94:83:89:c0:ee:1e:40:f7:4a:0e:19:4e:f7:c2:b5:
96:66:27:0b:16:d5:2c:f5:85:fd:8e:65:fc:00:95:
8f:78:af:77:b0
ASN1 OID: secp256k1

Uncompressed public key -> Ethereum addressโ€‹

So now, we have an uncompressed public key in hex, how to get the Ethereum address from it?

According to the Ethereum docs[^3]:

The address consists of the rightmost 160 bits of the 256-bit Keccak-256 hash of the serialized public key. This is equivalent to the rightmost 20 bytes of the 32-byte hash.

In Ethereum we only use uncompressed key (0x04 prefix). Public key K (x and y coordinates concatenated and shown as hex):

f7901e9161b9b53f2e1f27b0f1e4711fcc8f234a90f55fd2068a67b152948389c0ee1e40f74a0e194ef7c2b59666270b16d52cf585fd8e65fc00958f78af77b0

It is worth noting that the public key is not formatted with the prefix (hex) 04 when the address is calculated.

You need to hash the bytes of the public key (not the public key hex string).

const pemHexWithoutHeader =
"f7901e9161b9b53f2e1f27b0f1e4711fcc8f234a90f55fd2068a67b152948389c0ee1e40f74a0e194ef7c2b59666270b16d52cf585fd8e65fc00958f78af77b0";

const hash = createKeccakHash("keccak256")
.update(Buffer.from(pemHexWithoutHeader, "hex"))
.digest();
const address = "0x" + hash.slice(-20).toString("hex");

console.log("Checksum address:", web3.utils.toChecksumAddress(address));
// 0x039DC3795a6702F2B53FA0A29534Ec90B10F8d6a

Done :)

References