⚠️ Draft Standards Track: Interface

# EIP-6051: Private Key Encapsulation

### defines a specification for encapsulating private keys.

Authors Base Labs (@Base-Labs), Weiji Guo (@weiji-cryptonatty) 2022-11-21 https://ethereum-magicians.org/t/private-key-encapsulation-to-move-around-securely-without-entering-seed/11604

## Abstract

This EIP proposes a mechanism to encapsulate a private key so that it could be securely relocated to another application without providing the seed. This EIP combines ECIES (Elliptic Curve Integrated Encryption Scheme) and optional signature verification under various choices to ensure that the private key is encapsulated for a known or trusted party.

## Motivation

There are various cases in which we might want to export one of many private keys from a much more secure but less convenient wallet, which is controlled with a seed or passphrase.

1. We might dedicate one of many private keys for messaging purposes, and that private key is probably managed in a not-so-secure manner;
2. We might want to export one of many private keys from a hardware wallet, and split it with MPC technology so that a 3rd party service could help us identify potential frauds or known bad addresses, enforce 2FA, etc., meanwhile we can initiate transactions from a mobile device with much better UX and without carrying a hardware wallet.

In both cases, it is safer not to provide the seed which controls the whole wallet and might contains many addresses in multiple chains.

This EIP aims to enable such use cases.

## Specification

### Sender and Recipient

We hereby define:

• Sender as the party who holds in custody the private key to be encapsulated; Sender Application as the client-side application that said Sender uses to send the encapsulated private key.

• Recipient as the party who accepts the encapsulated private key, unwraps, and then uses it; Recipient Application as the client-side application that Recipient uses to receive the encapsulated private key.

### Core Algorithms

The basic idea is to encapsulate the private key with ECIES. To ensure that the ephemeral public key to encapsulate the private key is indeed generated from a trusted party and has not been tampered with, we also provided an option to sign that ephemeral public key in this standard.

There should be a mandatory version parameter. This allows various kinds of Key Encapsulation Mechanisms to be adopted depending on security considerations or preferences. The list shall be short to minimize compatibility issues among different vendors.

In addition to a version parameter, the following keys and functions are involved:

1. The Sender’s private key sk, which is to be encapsulated to the Recipient, and the corresponding address account.
2. The ephemeral Recipient key pair (r, R) such that R = [r]G. G denotes the base point of the elliptic curve, and [r]G denotes scalar multiplication. Optionally, R could be signed, and signerPubKey and signature are then provided for Sender to verify if R could be trusted or not.
3. The ephemeral Sender key pair (s, S) such that S = [s]G.
4. The share secret ss := [s]R = [r]S according to ECDH. Note that for secp256k1 this EIP follows RFC5903 and uses compact representation, which means to use only the x coordinate as the shared secret. For Curve25519 this EIP follows RFC7748.
5. The out-of-band data oob, optional. This could be digits or an alpha-numeric string entered by the user.
6. Let derivedKey := HKDF(hash=SHA256, ikm=ss, info=oob, salt, length). HKDF is defined in RFC5869. The length should be determined by skey and IV requirements such that the symmetric key skey = derivedKey[0:keySize], and IV = derivedKey[keySize:length]. keySize denotes the key size of the underlying symmetric algorithm, for example, 16 (bytes) for AES-128, and 32 (bytes) for Chacha20. See Security Considerations for the use of salt.
7. Let cipher := authenticated_encryption(symAlg, skey, IV, data=sk). The symmetric cipher algorithm symAlg and authentication scheme are decided by the version parameter. No additional authentication data aad is used.

A much-simplified example flow without signature and verification is:

1. Recipient Application generates (r, R).
2. User inputs R to Sender Application, along with a six-digit code “123456” as oob.
3. Sender Application generates (s, S), and computes cipher, then returns S || cipher.
4. Recipient Application scans to read S and cipher. The user enters “123456” as oob to Recipient Application.
5. Recipient Application decrypts cipher to get sk.
6. Recipient Application derives the address corresponding to sk so that the user can confirm the correctness.

With signature and verification, the signature to R by singerPubKey is appended to R. signerPubKey itself could have been already signed by trustedPubKey, and that signature is appended to signerPubKey. Note that the signature is applied to the byte array data instead of its string representation, which might lead to confusion and interoperability issues (such as hex or base64, lower case v.s. upper case, etc.). See Requests and Test Cases for further clarification and examples.

### Requests

#### Encoding of data and messages

• Raw bytes are encoded in hex and prefixed with ‘0x’.
• Unless specified otherwise, all parameters and return values are hex-encoded bytes.
• cipher is encoded into a single byte buffer as: [IV || encrypted_sk || tag].
• R, S, signerPubKey, and trustedPubKey are compressed if applicable.
• R or signerPubKey could be followed by a signature to it: [pub || sig]. Note that for the secp256k1 curve, the signature is just 64 bytes without the v indicator as found in a typical Ethereum signature.

#### R1. Request for Recipient to generate ephemeral key pair

request({
method: 'eth_generateEphemeralKeyPair',
params: [version, signerPubKey],
})
// expected return value: R


signerPubKey is optional. If provided, it is assumed that the implementation has the corresponding private key and the implementation MUST sign the ephemeral public key (in the form of what is to be returned). The signature algorithm is determined by the curve part of the version parameter, that is, ECDSA for secp256k1, and Ed25519 for Curve25519. And in this situation, it should be the case that Sender trusts signerPubKey, no matter how this trust is maintained. If not, the next request WILL be rejected by Sender Application. Also, see Security Considerations.

The implementation then MUST generate random private key r with a cryptographic secure random number generator (CSRNG), and derive ephemeral public key R = [r]G. The implementation SHOULD keep the generated key pair (r, R) in a secure manner in accordance with the circumstances, and SHOULD keep it only for a limited duration, but the specific duration is left to individual implementations. The implementation SHOULD be able to retrieve r when given back the corresponding public key R if within the said duration.

The return value is R, compressed if applicable. If signerPubKey is provided, then the signature is appended to R, also hex-encoded.

Alternatively, signature could be calculated separately, and then appended to the returned data.

#### R2. Request for Sender to encapsulate the private key

request({
method: 'eth_encapsulatePrivateKey',
params: [
version,
recipient, // public key, may be followed by its signature, see signerPubKey
signerPubKey,
oob,
salt,
account
],
})
// expected return value: S || cipher


recipient is the return value from the call to generate ephemeral key pair, with the optional signature appended either as returned or separately.

oob and salt are just byte arrays.

account is used to identify which private key to be encapsulated. With Ethereum, it is an address. Also, see Encoding of data and messages.

If signerPubKey is provided or recipient contains signature data, the implementation MUST perform signature verification. Missing data or incorrect format MUST either fail the call or result in an empty return and optional error logs.

signerPubKey could have been further signed by another key pair (trusted, trustedPubKey), which is trusted by Sender Application. In that case, signerPubKey is appended with the corresponding signature data, which SHOULD be verified against trustedPubKey. See Test Cases for further clarification.

The implementation shall then proceed to retrieve the private key sk corresponding to account, and follow the Core Algorithms to encrypt it.

The return data is a byte array that contains first Sender’s ephemeral public key S (compressed if applicable), then cipher including any authentication tag, that is, S || cipher.

#### R3. Request for Recipient to unwrap and intake the private key

request({
method: 'eth_intakePrivateKey',
params: [
version,
recipientPublicKey, //  no signature this time
oob,
salt,
data
],
})
// expected return value: account


This time recipientPublicKey is only the ephemeral public key R generated earlier in the Recipient side, just for the implementation to retrieve the corresponding private key r. data is the return value from the call to encapsulate private key, which is S || cipher.

When the encapsulated private key sk is decrypted successfully, the implementation can process it further according to the designated purposes. Some general security guidelines SHALL be followed, for example, do not log the value, do securely wipe it after use, etc.

The return value is the corresponding Ethereum address for sk, or empty if any error.

### Options and Parameters

Available elliptic curves are:

• secp256k1 (mandatory)
• Curve25519

Available authenticated encryption schemes are:

• AES-128-GCM (mandatory)
• AES-256-GCM
• Chacha20-Poly1305

The version string is simply the concatenation of the elliptic curve and AE scheme, for example, secp256k1-AES-128-GCM. The above lists allow a combination of six different concrete schemes. Implementations are encouraged to implement curve-related logic separately from authenticated encryption schemes to avoid duplication and to promote interoperability.

Signature algorithms for each curve are:

• secp256k1 –> ECDSA
• Curve25519 –> Ed25519

## Rationale

A critical difference between this EIP-6051 with EIP-5630 is that, as the purpose of key encapsulation is to transport a private key securely, the public key from the key recipient should be ephemeral, and mostly used only one-time. While in EIP-5630 settings, the public key of the message recipient shall be stable for a while so that message senders can encrypt messages without key discovery every time.

There is security implication to this difference, including perfect forward secrecy. We aim to achieve perfect forward secrecy by generating ephemeral key pairs on both sides every time:

1) first Recipient shall generate an ephemeral key pair, retain the private key securely, and export the public key; 2) then Sender can securely wrap the private key in ECIES, with another ephemeral key pair, then destroy the ephemeral key securely; 3) finally Recipient can unwrap the private key, then destroy its ephemeral key pair securely. After these steps, the cipher text in transport intercepted by a malicious 3rd party is no longer decryptable.

## Backwards Compatibility

No backward compatibility issues for this new proposal.

### Interoperability

To minimize potential compatibility issues among applications (including hardware wallets), this EIP requires that version secp256k1-AES-128-GCM MUST be supported.

The version could be decided by the user or negotiated by both sides. When there is no user input or negotiation, secp256k1-AES-128-GCM is assumed.

It is expected that implementations cover curve supports separately from encryption support, that is, all the versions that could be derived from the supported curve and supported encryption scheme should work.

Signatures to R and signerPubKey are applied to byte array values instead of the encoded string.

### UX Recommendations

salt and/or oob data: both are inputs to the HKDF function (oob as “info” parameter). For better user experiences we suggest to require from users only one of them but this is up to the implementation.

Recipient Application is assumed to be powerful enough. Sender Application could have very limited computing power and user interaction capabilities.

## Test Cases

For review purposes, the program to generate the test vectors is open-sourced and provided in the corresponding discussion thread.

### Data Fixation

Throughout the test cases, we fix values for the below data:

• sk, the private key to be encapsulated, fixed to: 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315. The corresponding address is 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9, called account. Note that these values come from the book Mastering Ethereum by Andreas M. Antonopoulos and Gavin Wood.
• r, the Recipient private key, fixed to 0x6f2dd2a7804705d2d536bee92221051865a639efa23f5ca7c810e77048253a79
• s, the Sender private key, fixed to 0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8
• signer, the private key to sign the ephemeral public key, fixed to 0xac304db075d1685284ba5e10c343f2324ee32df3394fc093c98932517d36e344. When used for Ed25519 signing, however, this value acts as seed, while the actual private key is calculated as SHA512(seed)[:32]. Or put another way, the public key is the scalar multiplication of hashed private key to the base point. Same for trusted.
• trusted, the private key to sign signerPubKey, fixed to 0xda6649d68fc03b807e444e0034b3b59ec60716212007d72c9ddbfd33e25d38d1
• oob, fixed to 0x313233343536 (string value: 123456)
• salt, fixed to 0x6569703a2070726976617465206b657920656e63617073756c6174696f6e (string value: eip: private key encapsulation)

### Case 1

Use version as secp256k1-AES-128-GCM. R1 is provided as:

request({
method: 'eth_generateEphemeralKeyPair',
params: [
version: 'secp256k1-AES-128-GCM',
],
})


Suppose the implementation generates an ephemeral key pair (r, R):

r: '0x6f2dd2a7804705d2d536bee92221051865a639efa23f5ca7c810e77048253a79',
R: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09'


The return value could be:

'0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09536da06b8d9207040ada179dc2c38f701a1a21c9ab5a7d52f5da50ea438e8ccf47dac77547fbdde194f71db52860b9e10ca2b089646f133d172124504ac1996a'


Note that R is compressed and R leads the return value: R || sig.

Therefore R2 could be provided as:

request({
method: 'eth_encapsulatePrivateKey',
params: [
version: 'secp256k1-AES-128-GCM',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9'
],
})


Sender Application first verifies first layer signature as ECDSA over secp256k1:

// actual message to be signed should be the decoded byte array
msg: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09',
//signerPubKey


Then it proceeds to verify the second layer signature, also as ECDSA over secp256k1:

// actual message to be signed should be the decoded byte array
sig: '0x5bd427c527b7f1012b8edfd179b9002a7f2d7fc326bb6ae9aaf38b44eb93c397631fd8bb05fd78fa16ecca1eb19652b200f9048611265bc81f485cf60f29d6de',
//trustedPubKey
pub: '0x027fb72176f1f9852ce7dd9dc3aa4711675d3d8dc5102b86d758d853002137e839'


Since Sender Application trusts trustedPubKey, the signature verification succeeds.

Suppose the implementation generates an ephemeral key pair (s, S) as:

s: '0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8',


The shared secret, symmetric key, and IV should be:

ss: '0x8e83bc5a9c77b11afc12c9a8262b16e899678d1720459e3b73ca2abcfed1fca3',
skey: '0x6ccc02a61aa16d6c66a1277e5e2434b8',
IV: '0x9c7a0f870d17ced2d2c3d1cf'


Then the return value should be:

'0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c02abff407e8901bb37d13d724a2e3a8a1a5af300adc286aa2ec65ef2a38c10c5cec68a949d0a20dbad2a8e5dfd7a14bbcb'


With compressed public key S leading cipher, which in turn is (added prefix ‘0x’):

'0xabff407e8901bb37d13d724a2e3a8a1a5af300adc286aa2ec65ef2a38c10c5cec68a949d0a20dbad2a8e5dfd7a14bbcb'


Then R3 is provided as:

request({
method: 'eth_intakePrivateKey',
params: [
version: 'secp256k1-AES-128-GCM',
recipientPublicKey: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
],
})


The return value should be 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9. This matches the account parameter in R2.

### Case 2

Use version as secp256k1-AES-256-GCM. The calculated symmetric key skey, IV, and cipher will be different. R1 is provided as:

request({
method: 'eth_generateEphemeralKeyPair',
params: [
version: 'secp256k1-AES-256-GCM',
],
})


Note that only the version is different (AES key size). We keep using the same (r, R) (this is just a test vector).

Therefore R2 is provided as:

request({
method: 'eth_encapsulatePrivateKey',
params: [
version: 'secp256k1-AES-256-GCM',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9'
],
})


Suppose the implementation generates the same (s, S) as Case 1. The shared secret, symmetric key, and IV should be:

ss: '0x8e83bc5a9c77b11afc12c9a8262b16e899678d1720459e3b73ca2abcfed1fca3',
skey: '0x6ccc02a61aa16d6c66a1277e5e2434b89c7a0f870d17ced2d2c3d1cfd0e6f199',
IV: '0x3369b9570b9d207a0a8ebe27'


With shared secret ss remaining the same as Case 1, symmetric key skey contains both the skey and IV from Case 1. IV is changed.

Then the return value should be the following, with the S part the same as Case 1 and the cipher part different:

'0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c0293910a91270b5deb0a645cc33604ed91668daf72328739d52a5af5a4760c4f3a9592b8f6d9b3ebe25127e7bf1c43b839'


Then R3 is provided as:

request({
method: 'eth_intakePrivateKey',
params: [
version: 'secp256k1-AES-256-GCM',
recipientPublicKey: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
],
})


The return value should be 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9. This matches the account parameter in R2.

### Case 3

Use version as: Curve-25519-Chacha20-Poly1305. R1 is provided as:

request({
method: 'eth_generateEphemeralKeyPair',
params: [
version: 'Curve25519-Chacha20-Poly1305',
signerPubKey: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938'
],
})


Note that with Curve25519 the size is 32 (bytes) for both the public key and private key. And there is no compression for the public key. signerPubKey is calculated as:

//signer is '0xac304db075d1685284ba5e10c343f2324ee32df3394fc093c98932517d36e344'
s := SHA512(signer)[:32]
signerPubKey := Curve25519.ScalarBaseMult(s).ToHex()


The same technique applies to trustedPubKey. With r the same as in Case 1 and Case 2 and the curve being changed, the return value is R = [r]G || sig:

R = '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79cac06de279ec7f65f6b75f6bee740496df0650a6de61da5e691d7c5da1c7cb1ece61c669dd588a1029c38f11ad1714c1c9742232f9562ca6bbc7bad57882da04'


R2 is provided as:

request({
method: 'eth_encapsulatePrivateKey',
params: [
version: 'Curve25519-Chacha20-Poly1305',
signerPubKey: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938d43e06a0f32c9e5ddb39fce34fac2b6f5314a1b1583134f27426d50af7094b0c101e848737e7f717da8c8497be06bab2a9536856c56eee194e89e94fd1bba509',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9'
],
})


Both recipient and signerPubKey have been signed in Ed25519. Verifying signature to R is carried out as:

// actual message to be signed should be the decoded byte array
msg: '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79',
//signerPubKey
pub: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938'


After successfully verifying the signature (and the one by trustedPubKey), the implementation then generates ephemeral key pair (s, S) in Curve25519:

// s same as Case 1 and Case 2
s = '0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8',
S = '0xd2fd6fcaac231d08363e736e61edb7e7696b13a727e3d2a239415cb8dc6ee278'


The shared secret, symmetric key, and IV should be:

ss: '0xe0b36f56cdb63c27e933a5a67a5e97db4b566c9276a36aeee5dc6e87da118867',
skey: '0x7c6fa749e6df13c8578dc44cb24cdf46a44cb163e1e570c2e590c720aed5783f',
IV: '0x3c98ef6fc34b0d6e7e16bd78'


Then the return value should be S || cipher:

'0xd2fd6fcaac231d08363e736e61edb7e7696b13a727e3d2a239415cb8dc6ee2786a7e2e40efb86dc68f44f3e032bbedb1259fa820e548ac5adbf191784c568d4f642ca5b60c0b2142189dff6ee464b95c'


Then R3 is provided as:

request({
method: 'eth_intakePrivateKey',
params: [
version: 'Curve25519-Chacha20-Poly1305',
recipientPublicKey: '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79',
oob: '0x313233343536',
salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e',
],
})


The return value should be 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9. This matches the account parameter in R2.

## Security Considerations

### Perfect Forward Secrecy

PFS is achieved by using ephemeral key pairs on both sides.

### Optional Signature and Trusted Public Keys

R could be signed so that Sender Application can verify if R could be trusted or not. This involves both signature verification and if the signer could be trusted or not. While signature verification is quite straightforward in itself, the latter should be managed with care. To facilitate this trust management issue, signerPubKey could be further signed, creating a dual-layer trust structure:

R <-- signerPubKey <-- trustedPubKey


This allows various strategies to manage trust. For example:

• A hardware wallet vendor which takes it very seriously about the brand reputation and the fund safety for its customers, could choose to trust only its own public keys, all instances of trustedPubKey. These public keys only sign signerPubKey from selected partners.
• A MPC service could publish its signerPubKey online so that Sender Application won’t verify the signature against a wrong or fake public key.

Note that it is advised that a separate key pair should be used for signing on each curve.

### Security Level

1. We are not considering post-quantum security. If the quantum computer becomes a materialized threat, the underlying cipher of Ethereum and other L1 chains would have been replaced, and this EIP will be outdated then (as the EC part of ECIES is also broken).
2. The security level shall match that of the elliptic curve used by the underlying chains. It does not make much sense to use AES-256 to safeguard a secp256k1 private key but implementations could choose freely.
3. That being said, a key might be used in multiple chains. So the security level shall cover the most demanding requirement and potential future developments.

AES-128, AES-256, and ChaCha20 are provided.

### Randomness

r and s must be generated with a cryptographic secure random number generator (CSRNG).

salt could be random bytes generated the same way as r or s. salt could be in any length but the general suggestion is 12 or 16, which could be displayed as a QR code by the screen of some hardware wallet (so that another application could scan to read). If salt is not provided, this EIP uses the default value as EIP-6051.

### Out of Band Data

oob data is optional. When non-empty, its content is digits or an alpha-numeric string from the user. Sender Application may mandate oob from the user.

Copyright and related rights waived via CC0.