Skip to content

Commit 3d7d0a6

Browse files
authored
fix(api-dkim): DKIM now supports ED25519 keys, both in PEM and raw format as input ZMS-125 (#617)
* dkim, add support for ed25519 * add support for raw ED25519 private key * magic value make variable. Remove unnecessary variables. Refactor * add new tests for ED25519
1 parent 6f0e4b5 commit 3d7d0a6

File tree

3 files changed

+74
-15
lines changed

3 files changed

+74
-15
lines changed

lib/api/dkim.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,14 @@ module.exports = (db, server) => {
198198
//.hostname()
199199
.trim()
200200
.required(),
201-
privateKey: Joi.string()
202-
.empty('')
203-
.trim()
204-
.regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'DKIM key format'),
201+
privateKey: Joi.alternatives().try(
202+
Joi.string()
203+
.empty('')
204+
.trim()
205+
.regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'DKIM key format')
206+
.description('PEM format RSA or ED25519 string'),
207+
Joi.string().empty('').trim().base64().length(44).description('Raw ED25519 key 44 bytes long if using base64')
208+
),
205209
description: Joi.string()
206210
.max(255)
207211
//.hostname()

lib/dkim-handler.js

+31-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
const ObjectId = require('mongodb').ObjectId;
44
const fingerprint = require('key-fingerprint').fingerprint;
5-
const forge = require('node-forge');
65
const crypto = require('crypto');
76
const tools = require('./tools');
87
const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events');
@@ -11,6 +10,8 @@ const { encrypt, decrypt } = require('./encrypt');
1110
const { promisify } = require('util');
1211
const generateKeyPair = promisify(crypto.generateKeyPair);
1312

13+
const ASN1_PADDING = 'MC4CAQAwBQYDK2VwBCIEIA==';
14+
1415
class DkimHandler {
1516
constructor(options) {
1617
options = options || {};
@@ -47,6 +48,7 @@ class DkimHandler {
4748

4849
let privateKeyPem = options.privateKey;
4950
let publicKeyPem;
51+
let publicKeyDer;
5052

5153
if (!privateKeyPem) {
5254
let keyPair = await this.generateKey();
@@ -61,12 +63,28 @@ class DkimHandler {
6163
}
6264

6365
if (!publicKeyPem) {
64-
// extract public key from private key using Forge
65-
let privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
66-
let publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
67-
publicKeyPem = forge.pki.publicKeyToPem(publicKey);
66+
// extract public key from private key
67+
68+
// 1) check that privateKeyPem is ED25519 raw key, which length is 44
69+
if (privateKeyPem.length === 44) {
70+
// privateKeyPem is actually a raw ED25519 base64 string with length of 44
71+
// convert raw ED25519 key to PEM formatted private key
72+
privateKeyPem = `-----BEGIN PRIVATE KEY-----
73+
${Buffer.concat([Buffer.from(ASN1_PADDING, 'base64'), Buffer.from(privateKeyPem, 'base64')]).toString('base64')}
74+
-----END PRIVATE KEY-----`;
75+
}
76+
77+
const publicKey = crypto.createPublicKey({ key: privateKeyPem, format: 'pem' });
78+
79+
publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
6880

69-
if (!publicKeyPem) {
81+
if (publicKey.asymmetricKeyType === 'ed25519') {
82+
publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).subarray(12).toString('base64');
83+
} else if (publicKey.asymmetricKeyType === 'rsa') {
84+
publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).toString('base64');
85+
}
86+
87+
if (!publicKeyPem && !publicKeyDer) {
7088
let err = new Error('Failed to generate public key');
7189
err.responseCode = 500;
7290
err.code = 'KeyGenereateError';
@@ -78,9 +96,11 @@ class DkimHandler {
7896
try {
7997
fp = fingerprint(privateKeyPem, 'sha256', true);
8098

81-
let ciphered = crypto.publicEncrypt(publicKeyPem, Buffer.from('secretvalue'));
82-
let deciphered = crypto.privateDecrypt(privateKeyPem, ciphered);
83-
if (deciphered.toString() !== 'secretvalue') {
99+
const testData = Buffer.from('secretvalue');
100+
const signature = crypto.sign(null, testData, privateKeyPem);
101+
const verificationResult = crypto.verify(null, testData, publicKeyPem, signature);
102+
103+
if (!verificationResult) {
84104
throw new Error('Was not able to use key for encryption');
85105
}
86106
} catch (E) {
@@ -98,6 +118,7 @@ class DkimHandler {
98118
selector,
99119
privateKey: privateKeyPem,
100120
publicKey: publicKeyPem,
121+
publicKeyDer,
101122
fingerprint: fp,
102123
created: new Date(),
103124
latest: true
@@ -170,7 +191,7 @@ class DkimHandler {
170191
publicKey: dkimData.publicKey,
171192
dnsTxt: {
172193
name: dkimData.selector + '._domainkey.' + dkimData.domain,
173-
value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
194+
value: 'v=DKIM1;t=s;p=' + dkimData.publicKeyDer
174195
}
175196
};
176197
}

test/api/dkim-test.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('API DKIM', function () {
1616

1717
this.timeout(10000); // eslint-disable-line no-invalid-this
1818

19-
it('should POST /dkim expect success', async () => {
19+
it('should POST /dkim expect success / RSA pem', async () => {
2020
const response = await server
2121
.post('/dkim')
2222
.send({
@@ -34,6 +34,40 @@ describe('API DKIM', function () {
3434
dkim = response.body.id;
3535
});
3636

37+
it('should POST /dkim expect success / ED25519 pem', async () => {
38+
const response = await server
39+
.post('/dkim')
40+
.send({
41+
domain: 'example.com',
42+
selector: 'wildduck',
43+
privateKey: '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIOQu92qofG/p0yAHDTNAawKchxOf/3MpDiPaCPk2xSPg\n-----END PRIVATE KEY-----',
44+
description: 'Some text about this DKIM certificate',
45+
sess: '12345',
46+
ip: '127.0.0.1'
47+
})
48+
.expect(200);
49+
expect(response.body.success).to.be.true;
50+
expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true;
51+
dkim = response.body.id;
52+
});
53+
54+
it('should POST /dkim expect success / ED25519 raw', async () => {
55+
const response = await server
56+
.post('/dkim')
57+
.send({
58+
domain: 'example.com',
59+
selector: 'wildduck',
60+
privateKey: 'nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=',
61+
description: 'Some text about this DKIM certificate',
62+
sess: '12345',
63+
ip: '127.0.0.1'
64+
})
65+
.expect(200);
66+
expect(response.body.success).to.be.true;
67+
expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true;
68+
dkim = response.body.id;
69+
});
70+
3771
it('should GET /dkim/:dkim expect success', async () => {
3872
const response = await server.get(`/dkim/${dkim}`).expect(200);
3973

0 commit comments

Comments
 (0)