I am trying to implement a DTLS server in nodejs. When I try to decrypt the EncryptedHandshakeMessage
from the client, I am getting the following error.
Error: Unsupported state or unable to authenticate data
What am I doing wrong? Any help would be much appreciated.
I am trying to implement a DTLS server in nodejs.
My cipher suite is, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
My server sends ServerKeyExchange
with the following params
EC Diffie-Hellman Server Params
Curve Type: named_curve (0x03)
Named Curve: secp256r1 (0x0017)
Pubkey Length: 65
Pubkey: 04581008311d54c64afad46c931c92911e8df9b0edc6dd1d9703ab678412cf5af53c11d8…
Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
Signature Length: 72
Signature: 3046022100a986cc75f373f6400e7223c28e4c770acbe17ff6c85f09476c25948aa151c6…
I generate the public key and private key using the following code. Using prime256v1
since it represents the same as secp256r1
. RFC 4492, Appendix-A
const ecdh = crypto.createECDH('prime256v1');
ecdh.generateKeys();
const publicKey = ecdh.getPublicKey(null, 'uncompressed');
const privateKey = ecdh.getPrivateKey();
Now I have the following on the server.
clientRandom
- Got from ClientHello
serverRandom
- My server generated it. Sent it in ServerHello
.
serverPrivateKey
- Got from above block
serverPublicKey
- Got from above block. Sent in ServerKeyExchange
.
clientPublicKey
- Got from ClientKeyExchange
After this, I compute pre-master secret using the following code
const ecdh = crypto.createECDH('prime256v1');
ecdh.setPrivateKey(serverPrivateKey);
const preMasterSecret = ecdh.computeSecret(clientPublicKey);
Then compute master secret
const masterSecret = PRF(
preMasterSecret,
Buffer.from("master secret"),
Buffer.concat([clientRandom, serverRandom]),
48
);
Then compute the key block. Not computing MAC keys because RFC 5246, 6.2.3.3 says "No MAC key is used" for AEAD ciphers.
const seed = Buffer.concat([clientRandom, serverRandom]);
const length =
(2 * 16) + // client_write_key + server_write_key
(2 * 4); // client_write_IV + server_write_IV
const keyBlock = PRF(
masterSecret,
Buffer.from('key expansion'),
seed,
length
);
const keyLength = 16;
const ivLength = 4;
let start = 0, end = start + keyLength;
const clientWriteKey = keyBlock.slice(start, end);
start = end, end = start + keyLength;
const serverWriteKey = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const clientWriteIV = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const serverWriteIV = keyBlock.slice(start, end);
After I receive EncryptedHandshakeMessage from server, I extract the first 8 bytes from the payload to get the nonce_explicit
value as defined in RFC 5246, 6.2.3.3
I concat clientWriteIV
as nonce_implicit
(in that order) as stated in the same above link and generate nonce
.
const nonce = Buffer.concat([
clientWriteIV,
encryptedHandshakeMessage.slice(0, 8)
]);
Then, I construct the Additional Authenticated Data as per RFC 5246, 6.2.3.3. Since I use DTLS, seq_num is generated as per RFC 6347, 4.1.2.1.
const aad = [
// epoch
0x00, 0x01,
//sequence number
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// content type
0x16,
// version
0xfe, 0xfd,
// length (48 bytes)
0x00, 0x30
]
Then I performed the decryption operation as follows
const decipher = crypto.createDecipheriv('aes-128-gcm', clientWriteKey, nonce);
decipher.setAuthTag(encryptedHandshakeMessage.slice(-16));
decipher.setAAD(Buffer.from(aad));
const decrypted = decipher.update(encryptedHandshakeMessage.slice(0, -16), null, 'utf8');
decipher.final();
The line decipher.final()
is failing with the following error.
Error: Unsupported state or unable to authenticate data
I use OpenSSL s_client as my client in DTLS 1.2 mode. I had logged the keys using -keylogfile
argument and the output is as follows
CLIENT_RANDOM 6e0ed5aec5691fc199d1019ecfc1e4a8629c9ed4208dcad30e8005c82364099c d228852f0c13a0b3122065519c3d4b66b8c2674232686221f77c5cfb49001e99528be5900ea1d88a23c533f8c544c6d9
If my knowledge is correct, the second part indicates the master secret which does not match with master secret I computed in my server.
What am I doing wrong?
Full code for reference.
const crypto = require('crypto');
const clientRandom = Buffer.from("6e0ed5aec5691fc199d1019ecfc1e4a8629c9ed4208dcad30e8005c82364099c", 'hex')
const serverRandom = Buffer.from("9c27d9ef09af50cf0c9aba9e535714ed07f2f522ec672a4c17ff550604f950e4", 'hex')
const serverPrivateKey = Buffer.from("4ac2c6a823a455c19101b2b61d3fd6b4c923cb6512f7b1488a717a86c4b1e9c0", 'hex')
const serverPublicKey = Buffer.from("04581008311d54c64afad46c931c92911e8df9b0edc6dd1d9703ab678412cf5af53c11d81935b463e3da85f56dabb0aa998f3dd09ea3629a05cff3a0c801f7bdc7", 'hex')
const clientPublicKey = Buffer.from("04ad753ed35f557cf9419d7d03d51510b4c1fa282ddd0c432b43de1256cf561df16c7b355b17328f2f72ec9ce4361d7855d92e643904e501387b46a4edfe1c67d6", 'hex')
const encryptedHandshakeMessage = Buffer.from("713a474b4484511c7de96486fa9bc8d1716ed73717c42c0b56277ab6453d349770db77bb40353fb622c5c3422d79b981", 'hex')
const aad = [
// epoch
0x00, 0x01,
//sequence number
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// content type
0x16,
// version
0xfe, 0xfd,
// length (48 bytes)
0x00, 0x30
]
function PRF(secret, label, seed, length){
const labelSeedConcat = Buffer.concat([label, seed]);
function hmacHash(secret, seed){
const hmac = crypto.createHmac('sha256', Buffer.from(secret));
hmac.update(seed);
return hmac.digest();
}
function A(i){
if(i === 0){
return labelSeedConcat;
}
else{
return hmacHash(secret, A(i-1));
}
}
return Buffer.concat([
hmacHash(secret, Buffer.concat([A(1), labelSeedConcat])),
hmacHash(secret, Buffer.concat([A(2), labelSeedConcat])),
hmacHash(secret, Buffer.concat([A(3), labelSeedConcat])),
hmacHash(secret, Buffer.concat([A(4), labelSeedConcat])),
]).slice(0, length);
}
const ecdh = crypto.createECDH('prime256v1');
ecdh.setPrivateKey(serverPrivateKey);
const preMasterSecret = ecdh.computeSecret(clientPublicKey);
const masterSecret = PRF(
preMasterSecret,
Buffer.from("master secret"),
Buffer.concat([clientRandom, serverRandom]),
48
);
console.log('Master secret:', masterSecret.toString('hex'));
function computeSymmetricKeysFromMasterSecret(masterSecret, clientRandom, serverRandom){
const seed = Buffer.concat([clientRandom, serverRandom]);
const length =
(2 * 16) + // client_write_key + server_write_key
(2 * 4); // client_write_IV + server_write_IV
const keyBlock = PRF(
masterSecret,
Buffer.from('key expansion'),
seed,
length
);
const macKeyLength = 32;
const keyLength = 16;
const ivLength = 4;
let start = 0, end = 0;
start = end, end = start + keyLength;
const clientWriteKey = keyBlock.slice(start, end);
start = end, end = start + keyLength;
const serverWriteKey = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const clientWriteIV = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const serverWriteIV = keyBlock.slice(start, end);
return {clientWriteKey, serverWriteKey, clientWriteIV, serverWriteIV};
}
const {clientWriteKey, serverWriteKey, clientWriteIV, serverWriteIV} = computeSymmetricKeysFromMasterSecret(masterSecret, clientRandom, serverRandom);
/*
nonce = implicit nonce (4 bytes) + explicit nonce (8 bytes)
*/
const nonce = Buffer.concat([
clientWriteIV,
encryptedHandshakeMessage.slice(0, 8)
]);
const decipher = crypto.createDecipheriv('aes-128-gcm', clientWriteKey, nonce);
decipher.setAuthTag(encryptedHandshakeMessage.slice(-16));
decipher.setAAD(Buffer.from(aad));
const decrypted = decipher.update(encryptedHandshakeMessage.slice(0, -16), null, 'utf8');
decipher.final();
console.log('Decrypted:', decrypted);
Then I thought, during key block generation, maybe I should actually compute the MAC keys, but not use it? I tried that too. It did not work either.
const seed = Buffer.concat([clientRandom, serverRandom]);
const length =
(2 * 32) + // client_write_MAC_key + server_write_MAC_key
(2 * 16) + // client_write_key + server_write_key
(2 * 4); // client_write_IV + server_write_IV
const keyBlock = PRF(
masterSecret,
Buffer.from('key expansion'),
seed,
length
);
const macKeyLength = 32;
const keyLength = 16;
const ivLength = 4;
let start = 0, end = 0;
start = end, end = start + macKeyLength;
const clientWriteMACKey = keyBlock.slice(start, end);
start = end, end = start + macKeyLength;
const serverWriteMACKey = keyBlock.slice(start, end);
start = end, end = start + keyLength;
const clientWriteKey = keyBlock.slice(start, end);
start = end, end = start + keyLength;
const serverWriteKey = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const clientWriteIV = keyBlock.slice(start, end);
start = end, end = start + ivLength;
const serverWriteIV = keyBlock.slice(start, end);
Edit:
Please find the wireshark dump of the handshake process here
I have a permanent certificate and key (pem files) for my server. This certificate is trusted by the client. I use this key file (corresponding to that certificate) to sign the serverPublicKey
value and send it via ServerKeyExchange. Am I doing this correct?
I had done several mistakes which caused the failure in decryption. Will list them below.
master secret
is wrong. server_random
comes first and client_random
comes next.key_block = PRF(SecurityParameters.master_secret,
"key expansion",
SecurityParameters.server_random +
SecurityParameters.client_random);
Client Explicit nonce was included with the encrypted_block
input for decryption. It should not have been added.
I had negotiated extended master secret
extension RFC 7627. But not calculated the master secret according to that extension. So, removed the extension temporarily.
The length specified in Additional authenticated data should be TLSCompressed.length
. i.e, the length of payload BEFORE encryption. I used record length instead of fragment length.
Fixing all the above stated issues fixed my issue and I am now able to decrypt the payload.
Thanks to all the commenters and answerers who helped me.