Stratum V2 employs a type of encryption scheme called AEAD (authenticated encryption with associated data) to address the security aspects of all communication that occurs between clients and servers. This provides both confidentiality and integrity for the ciphertexts (i.e. encrypted data) being transferred, as well as providing integrity for associated data which is not encrypted. Prior to opening any Stratum V2 channels for mining, clients MUST first initiate the cryptographic session state that is used to encrypt all messages sent between themselves and servers. Thus, the cryptographic session state is independent of V2 messaging conventions.
At the same time, this specification proposes optional use of a particular handshake protocol based on the Noise Protocol framework (opens new window). The client and server establish secure communication using Diffie-Hellman (DH) key agreement, as described in greater detail in the Authenticated Key Agreement Handshake section below.
Using the handshake protocol to establish secured communication is optional on the local network (e.g. local mining devices talking to a local mining proxy). However, it is mandatory for remote access to the upstream nodes, whether they be pool mining services, job declarating services or template distributors.
Data transferred by the mining protocol MUST not provide adversary information that they can use to estimate the performance of any particular miner. Any intelligence about submitted shares can be directly converted to estimations of a miner’s earnings and can be associated with a particular username. This is unacceptable privacy leakage that needs to be addressed.
The reasons why Noise Protocol Framework has been chosen are listed below:
Noise encrypted session requires Elliptic Curve (EC), Hash function (HASH()
) and cipher function that supports AEAD mode1.
This specification describes mandatory cryptographic primitives that each implementation needs to support. These primitives are chosen so that Noise Encryption layer for Stratum V2 can be implemented using primitives already present in Bitcoin Core project at the time of writing this spec.
Secp256k1 curve points, which includes Public Keys and ECDH results, are points with of X- and Y-coordinate, 32-bytes each. There are several possibilities how to serialize them:
We choose the 32-byte serialization for public key and 64-byte for signatures with implicit Y-coordinate.
The parity of Y-coordinate is always assumed to be even.
Key generation algorithm:
sk
d' = int(sk)
d = 0
or d' > n
where n
is group order of secp256k1 curveP
as d'⋅G
(sk, bytes(P.x))
Such system has the following properties:
(sk, bytes(P.x))
there is another keypair (n - sk, bytes(P.x))
, where n
is group order of secp256k1 curveECDH(sk, Q)
is equal to ECDH(n - sk, Q)
for some EC point Q
where n
is group order of secp256k1 curveThese properties don't reduce security.
For more information refer to BIP3403
SHA-256()
is used as a HASH()
k
, nonce n
, associated_data ad
, plaintext pt
and ciphertext ct
ENCRYPT(k, n, ad, pt)
DECRYPT(k, n, ad, ct)
Object that encapsulates encryption and decryption operations with underlying AEAD mode cipher functions using 32-byte encryption key k
and 8-byte nonce n
.
CipherState has the following interface:
InitializeKey(key)
:
k = key
, n = 0
EncryptWithAd(ad, plaintext)
k
is non-empty, performs ENCRYPT(k, n++, ad, plaintext)
on the underlying cipher function, otherwise returns plaintext
ENCRYPT
is an evaluation of ChaCha20-Poly1305
(IETF variant) or AES-GCM
with the passed arguments, with nonce n
encoded as 32 zero bits, followed by a little-endian 64-bit value. Note: this follows the Noise Protocol convention, rather than our normal endian.DecryptWithAd(ad, ciphertext)
k
is non-empty performs DECRYPT(k, n++, ad, plaintext)
on the underlying cipher function, otherwise returns ciphertext. If an authentication failure occurs in DECRYPT()
then n
is not incremented and an error is signaled to the caller.DECRYPT
is an evaluation of ChaCha20-Poly1305
(IETF variant) or AES-GCM
with the passed arguments, with nonce n
encoded as 32 zero bits, followed by a little-endian 64-bit value.Throughout the handshake process, each side maintains these variables:
ck
: chaining key. Accumulated hash of all previous ECDH outputs. At the end of the handshake ck
is used to derive encryption key k
.h
: handshake hash. Accumulated hash of all handshake data that has been sent and received so far during the handshake processe
, re
ephemeral keys. Ephemeral key and remote party's ephemeral key, respectively.s
, rs
static keys. Static key and remote party's static key, respectively.The following functions will also be referenced:
generateKey()
: generates and returns a fresh secp256k1
keypair
generateKey
has two attributes:
.public_key
, which returns an abstract object representing the public key.private_key
, which represents the private key used to generate the public key.serializeImplicit()
that outputs a 32-byte serialization of the X-coordinate of EC point (implicit Y-coordinate)a || b
denotes the concatenation of two byte strings a
and b
HMAC-HASH(key, data)
RFC 2104
5k' = k || <zero-bytes>
temp = SHA-256((k' XOR ipad) || data)
where ipad is repeated 0x36 byteSHA-256((k' XOR opad) || temp)
where opad is repeated 0x5c byteHKDF(chaining_key, input_key_material, num_output)
: a function defined in RFC 5869
6, evaluated with a zero-length info
field:
temp_key = HMAC-HASH(chaining_key, input_key_material)
output1 = HMAC-HASH(temp_key, byte(0x01))
output2 = HMAC-HASH(temp_key, output1 || byte(0x02))
num_outputs == 2
then returns the pair (output1, output2)
output3 = HMAC-HASH(temp_key, output2 || byte(0x03))
(output1, output2, output3)
MixKey(input_key_material)
: Executes the following steps:
(ck, temp_k) = HKDF(ck, input_key_material, 2)
InitializeKey(temp_k)
MixHash(data)
: Sets h = HASH(h || data)
EncryptAndHash(plaintext)
:
k
is non-empty sets ciphertext = EncryptWithAd(h, plaintext)
, otherwise ciphertext = plaintext
MixHash(ciphertext)
ciphertext
DecryptAndHash(ciphertext)
:
k
is non-empty sets plaintext = DecryptWithAd(h, ciphertext)
, otherwise plaintext = ciphertext
MixHash(ciphertext)
plaintext
ECDH(k, rk)
: performs an Elliptic-Curve Diffie-Hellman operation using k
, which is a valid secp256k1
private key, and rk
, which is a valid public key
The handshake chosen for the authenticated key exchange is an Noise_NX
augmented by algorithm negotiation prior to handshake itself and server authentication with simple 2 level public key infrastructure.
The complete authenticated key agreement (Noise NX
) is performed in five distinct steps (acts).
-> e
<- e, ee, s, es, SIGNATURE_NOISE_MESSAGE
SIGNATURE_NOISE_MESSAGE
Should the decryption (i.e. authentication code validation) fail at any point, the session must be terminated.
-> e
Prior to starting first round of NX-handshake, both initiator and responder initializes handshake variables h
(hash output), ck
(chaining key) and k
(encryption key):
h = protocolName || <zero-padding>
or h = HASH(protocolName)
protocolName
is less than or equal to 32 bytes in length, use protocolName
with zero bytes appended to make 32 bytes. Otherwise, apply HASH
to it.protocolName
is official noise protocol name such as Noise_NX_secp256k1_ChaChaPoly_SHA256
encoded as an ASCII stringck = h
h = HASH(h)
k
emptyInitiator generates ephemeral keypair and sends the public key to the responder:
e
, appends e.public_key
to the buffer (32 bytes plaintext public key)MixHash(e.public_key)
EncryptAndHash()
with empty payload and appends the ciphertext to the buffer (note that k is empty at this point, so this effectively reduces down to MixHash()
on empty data)Field name | Description |
---|---|
PUBKEY | Initiator's ephemeral public key |
Message length: 32 bytes
re.public_key
MixHash(re.public_key)
DecryptAndHash()
on remaining bytes (i.e. on empty data with empty k, thus effectively only calls MixHash()
on empty data)<- e, ee, s, es, SIGNATURE_NOISE_MESSAGE
Responder provides its ephemeral, encrypted static public keys and encrypted SIGNATURE_NOISE_MESSAGE
to the initiator, performs Elliptic-Curve Diffie-Hellman operations.
Field Name | Data Type | Description |
---|---|---|
version | U16 | Version of the certificate format |
valid_from | U32 | Validity start time (unix timestamp) |
not_valid_after | U32 | Signature is invalid after this point in time (unix timestamp) |
signature | SIGNATURE | Certificate signature |
Length: 74 bytes
e
, appends e.public_key
to the buffer (32 bytes plaintext public key)MixHash(e.public_key)
MixKey(ECDH(e.private_key, re.public_key))
EncryptAndHash(s.public_key)
(32 bytes encrypted public key, 16 bytes MAC)MixKey(ECDH(s.private_key, re.public_key))
EncryptAndHash(SIGNATURE_NOISE_MESSAGE)
to the buffertemp_k1, temp_k2 = HKDF(ck, zerolen, 2)
c1
and c2
c1.InitializeKey(temp_k1)
and c2.InitializeKey(temp_k2)
(c1, c2)
Field name | Description |
---|---|
PUBKEY | Responder's plaintext ephemeral public key |
PUBKEY | Responder's encrypted static public key |
MAC | Message authentication code for responder's static public key |
SIGNATURE_NOISE_MESSAGE | Signed message containing Responder's static key. Signature is issued by authority that is generally known to operate the server acting as the noise responder |
MAC | Message authentication code for SIGNATURE_NOISE_MESSAGE |
Message length: 170 bytes
re.public_key
MixHash(re.public_key)
MixKey(ECDH(e.private_key, re.public_key))
DecryptAndHash()
and stores the results as rs.public_key
which is server's static public key (note that 32 bytes is the public key and 16 bytes is MAC)MixKey(ECDH(e.private_key, rs.public_key)
DecryptAndHash()
and deserialize plaintext into SIGNATURE_NOISE_MESSAGE
(74 bytes data + 16 bytes MAC)temp_k1, temp_k2 = HKDF(ck, zerolen, 2)
c1
and c2
c1.InitializeKey(temp_k1)
and c2.InitializeKey(temp_k2)
(c1, c2)
During the handshake, initiator receives SIGNATURE_NOISE_MESSAGE
and server's static public key. These parts make up a CERTIFICATE
signed by an authority whose public key is generally known (for example from pool's website). Initiator confirms the identity of the server by verifying the signature in the certificate.
Field Name | Data Type | Description | Signed field |
---|---|---|---|
version | U16 | Version of the certificate format | YES |
valid_from | U32 | Validity start time (unix timestamp) | YES |
not_valid_after | U32 | Signature is invalid after this point in time (unix timestamp) | YES |
server_public_key | PUBKEY | Server's static public key that was used during NX handshake | YES |
authority_public_key | PUBKEY | Certificate authority's public key that signed this message | NO |
signature | SIGNATURE | Signature over the serialized fields marked for signing | NO |
This message is not sent directly. Instead, it is constructed from SIGNATURE_NOISE_MESSAGE and server's static public key that are sent during the handshake process
Schnorr signature with key prefixing is used3
signature is constructed for
m
, where m
is HASH
of the serialized fields of the CERTIFICATE
that are marked for signing, i.e. m = SHA-256(version || valid_from || not_valid_after || server_public_key)
P
that is Certificate AuthoritySignature itself is concatenation of an EC point R
and an integer s
(note that each item is serialized as 32 bytes array) for which identity s⋅G = R + HASH(R || P || m)⋅P
holds.
-> AEAD_CIPHERS
Initiator provides list of AEAD ciphers other than ChaChaPoly that it supports
Field name | Description |
---|---|
SEQ0_32[u32] | List of AEAD cipher functions other than ChaChaPoly that the client supports |
Message length: 1 + n * 4 bytes, where n is the length byte of the SEQ0_32 field, at most 129
possible cipher codes:
cipher code | Cipher description |
---|---|
0x47534541 (b"AESG") | AES-256 with with GCM from [7] |
[7] - Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC
<- CIPHER_CHOICE
Responder acknowledges receiving AEAD_CIPHERS
message with CIPHER_CHOICE
. There are two possible cases
CIPHER_CHOICE
is empty: In this case continue using current established encrypted sessionCIPHER_CHOICE
is non-empty - Restart encrypted session using the new AEAD-cipherField name | Description |
---|---|
OPTION[u32] | Request to upgrade to a given AEAD-cipher |
Message length: 1 or 5 bytes
If the server provides a non-empty CIPHER_CHOICE
:
key_new
are derived from the original CipherState keys key_orig
by taking the first 32 bytes from ENCRYPT(key_orig, maxnonce, zero_len, zeros)
using the negotiated cipher function where maxnonce
is 264 - 1, zerolen
is a zero-length byte sequence, and zeros
is a sequence of 32 bytes filled with zeros. (see Rekey(k)
function8)InitializeKey(key_new)
.After handshake process is finished, both initiator and responder have CipherState objects for encryption and decryption and after initiator validated server's identity, any subsequent traffic is encrypted and decrypted with EncryptWithAd()
and DecryptWithAd()
methods of the respective CipherState objects with zero-length associated data.
Maximum transport message length (ciphertext) is for noise protocol message 65535 bytes.
Since Stratum Message Frame consists of
Stratum Message header and stratum message payload are processed separately.
message_length
being length of payload ciphertext*EncryptWithAd([], header)
- 22 bytes
5. EncryptWithAd([], payload)
- variable length encrypted message*converting plaintext length to ciphertext length:
#define MAX_CT_LEN 65535
#define MAC_LEN 16
#define MAX_PT_LEN (MAX_CT_LEN - MAC_LEN)
uint pt_len_to_ct_len(uint pt_len) {
uint remainder;
remainder = pt_len % MAX_PT_LEN;
if (remainder > 0) {
remainder += MAC_LEN;
}
return pt_len / MAX_PT_LEN * MAX_CT_LEN + remainder;
}
frame.message_length
number of bytes and decrypt into plaintext payload or failframe.extension_type
and frame.message_type
or fail+--------------------------------------------------+-------------------------------------------------------------------+
| Extended noise header | Encrypted stratum-message payload |
+--------------------------------------------------+-------------------+-------------------+---------------------------+
| Header AEAD ciphertext | Noise block 1 | Noise block 2 | Last Noise block |
| 22 Bytes | 65535 Bytes | 65535 Bytes | 17 - 65535 Bytes |
+----------------------------------------+---------+-----------+-------+-----------+-------+---------------+-----------+
| Encrypted Stratum message Header | MAC | ct_pld_1 | MAC_1 | ct_pld_2 | MAC_2 | ct_pld_rest | MAC_rest |
| 6 Bytes | 16 B | 65519 B | 16 B | 65519 B | 16 B | 1 - 65519 B | 16 Bytes |
+================+==========+============+=========+===========+=======+===========+=======+===============+===========+
| extension_type | msg_type | pld_length | <padd | pt_pld_1 | <padd | pt_pld_2 | <padd | pt_pld_rest | <padding> |
| U16 | U8 | U24 | ing> | 65519 B | ing> | 65519 B | ing> | 1 - 65519 B | |
+----------------+----------+------------+---------+-------------------------------------------------------------------+
Serialized stratum-v2 body (payload) is split into 65519-byte chunks and encrypted to form 65535-bytes AEAD ciphertexts,
where `ct_pld_N` is the N-th ciphertext block of payload and `pt_pld_N` is the N-th plaintext block of payload.
Downstream nodes that want to use the above outlined security scheme need to have configured the Pool Authority Public Key of the pool that they intend to connect to. It is provided by the target pool and communicated to its users via a trusted channel. At least, it can be published on the pool's public website.
The key can be embedded into the mining URL as part of the path.
Authority Public key is base58-check (opens new window) encoded 32-byte secp256k1 public key (with implicit Y coordinate) prefixed with a LE u16 version prefix, currently [1, 0]
:
[1, 0] | 2 bytes prefix |
---|---|
PUBKEY | 32 bytes authority public key |
URL example:
stratum2+tcp://thepool.com/9bXiEd8boQVhq7WddEcERUL5tyyJVFYdU8th3HfbNXK3Yw6GRXh
raw_ca_public_key = [118, 99, 112, 0, 151, 156, 28, 17, 175, 12, 48, 11, 205, 140, 127, 228, 134, 16, 252, 233, 185, 193, 30, 61, 174, 227, 90, 224, 176, 138, 116, 85]
prefixed_base58check = "9bXiEd8boQVhq7WddEcERUL5tyyJVFYdU8th3HfbNXK3Yw6GRXh"