diff --git a/lib/api/dkim.js b/lib/api/dkim.js index dd327cc8..e8b25ffb 100644 --- a/lib/api/dkim.js +++ b/lib/api/dkim.js @@ -198,10 +198,14 @@ module.exports = (db, server) => { //.hostname() .trim() .required(), - privateKey: Joi.string() - .empty('') - .trim() - .regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'DKIM key format'), + privateKey: Joi.alternatives().try( + Joi.string() + .empty('') + .trim() + .regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'DKIM key format') + .description('PEM format RSA or ED25519 string'), + Joi.string().empty('').trim().base64().length(44).description('Raw ED25519 key 44 bytes long if using base64') + ), description: Joi.string() .max(255) //.hostname() diff --git a/lib/dkim-handler.js b/lib/dkim-handler.js index 1f56aa39..47173975 100644 --- a/lib/dkim-handler.js +++ b/lib/dkim-handler.js @@ -2,7 +2,6 @@ const ObjectId = require('mongodb').ObjectId; const fingerprint = require('key-fingerprint').fingerprint; -const forge = require('node-forge'); const crypto = require('crypto'); const tools = require('./tools'); const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events'); @@ -11,6 +10,8 @@ const { encrypt, decrypt } = require('./encrypt'); const { promisify } = require('util'); const generateKeyPair = promisify(crypto.generateKeyPair); +const ASN1_PADDING = 'MC4CAQAwBQYDK2VwBCIEIA=='; + class DkimHandler { constructor(options) { options = options || {}; @@ -47,6 +48,7 @@ class DkimHandler { let privateKeyPem = options.privateKey; let publicKeyPem; + let publicKeyDer; if (!privateKeyPem) { let keyPair = await this.generateKey(); @@ -61,12 +63,28 @@ class DkimHandler { } if (!publicKeyPem) { - // extract public key from private key using Forge - let privateKey = forge.pki.privateKeyFromPem(privateKeyPem); - let publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e); - publicKeyPem = forge.pki.publicKeyToPem(publicKey); + // extract public key from private key + + // 1) check that privateKeyPem is ED25519 raw key, which length is 44 + if (privateKeyPem.length === 44) { + // privateKeyPem is actually a raw ED25519 base64 string with length of 44 + // convert raw ED25519 key to PEM formatted private key + privateKeyPem = `-----BEGIN PRIVATE KEY----- +${Buffer.concat([Buffer.from(ASN1_PADDING, 'base64'), Buffer.from(privateKeyPem, 'base64')]).toString('base64')} +-----END PRIVATE KEY-----`; + } + + const publicKey = crypto.createPublicKey({ key: privateKeyPem, format: 'pem' }); + + publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }); - if (!publicKeyPem) { + if (publicKey.asymmetricKeyType === 'ed25519') { + publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).subarray(12).toString('base64'); + } else if (publicKey.asymmetricKeyType === 'rsa') { + publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).toString('base64'); + } + + if (!publicKeyPem && !publicKeyDer) { let err = new Error('Failed to generate public key'); err.responseCode = 500; err.code = 'KeyGenereateError'; @@ -78,9 +96,11 @@ class DkimHandler { try { fp = fingerprint(privateKeyPem, 'sha256', true); - let ciphered = crypto.publicEncrypt(publicKeyPem, Buffer.from('secretvalue')); - let deciphered = crypto.privateDecrypt(privateKeyPem, ciphered); - if (deciphered.toString() !== 'secretvalue') { + const testData = Buffer.from('secretvalue'); + const signature = crypto.sign(null, testData, privateKeyPem); + const verificationResult = crypto.verify(null, testData, publicKeyPem, signature); + + if (!verificationResult) { throw new Error('Was not able to use key for encryption'); } } catch (E) { @@ -98,6 +118,7 @@ class DkimHandler { selector, privateKey: privateKeyPem, publicKey: publicKeyPem, + publicKeyDer, fingerprint: fp, created: new Date(), latest: true @@ -170,7 +191,7 @@ class DkimHandler { publicKey: dkimData.publicKey, dnsTxt: { name: dkimData.selector + '._domainkey.' + dkimData.domain, - value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '') + value: 'v=DKIM1;t=s;p=' + dkimData.publicKeyDer } }; } diff --git a/test/api/dkim-test.js b/test/api/dkim-test.js index 060b7583..ce61a5a5 100644 --- a/test/api/dkim-test.js +++ b/test/api/dkim-test.js @@ -16,7 +16,7 @@ describe('API DKIM', function () { this.timeout(10000); // eslint-disable-line no-invalid-this - it('should POST /dkim expect success', async () => { + it('should POST /dkim expect success / RSA pem', async () => { const response = await server .post('/dkim') .send({ @@ -34,6 +34,40 @@ describe('API DKIM', function () { dkim = response.body.id; }); + it('should POST /dkim expect success / ED25519 pem', async () => { + const response = await server + .post('/dkim') + .send({ + domain: 'example.com', + selector: 'wildduck', + privateKey: '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIOQu92qofG/p0yAHDTNAawKchxOf/3MpDiPaCPk2xSPg\n-----END PRIVATE KEY-----', + description: 'Some text about this DKIM certificate', + sess: '12345', + ip: '127.0.0.1' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + dkim = response.body.id; + }); + + it('should POST /dkim expect success / ED25519 raw', async () => { + const response = await server + .post('/dkim') + .send({ + domain: 'example.com', + selector: 'wildduck', + privateKey: 'nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=', + description: 'Some text about this DKIM certificate', + sess: '12345', + ip: '127.0.0.1' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + dkim = response.body.id; + }); + it('should GET /dkim/:dkim expect success', async () => { const response = await server.get(`/dkim/${dkim}`).expect(200);