Skip to content

Commit

Permalink
fix(api-dkim): DKIM now supports ED25519 keys, both in PEM and raw fo…
Browse files Browse the repository at this point in the history
…rmat 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
  • Loading branch information
NickOvt committed Feb 19, 2024
1 parent 6f0e4b5 commit 3d7d0a6
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 15 deletions.
12 changes: 8 additions & 4 deletions lib/api/dkim.js
Expand Up @@ -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()
Expand Down
41 changes: 31 additions & 10 deletions lib/dkim-handler.js
Expand Up @@ -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');
Expand All @@ -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 || {};
Expand Down Expand Up @@ -47,6 +48,7 @@ class DkimHandler {

let privateKeyPem = options.privateKey;
let publicKeyPem;
let publicKeyDer;

if (!privateKeyPem) {
let keyPair = await this.generateKey();
Expand All @@ -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';
Expand All @@ -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) {
Expand All @@ -98,6 +118,7 @@ class DkimHandler {
selector,
privateKey: privateKeyPem,
publicKey: publicKeyPem,
publicKeyDer,
fingerprint: fp,
created: new Date(),
latest: true
Expand Down Expand Up @@ -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
}
};
}
Expand Down
36 changes: 35 additions & 1 deletion test/api/dkim-test.js
Expand Up @@ -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({
Expand All @@ -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);

Expand Down

0 comments on commit 3d7d0a6

Please sign in to comment.