Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 48 additions & 34 deletions documentation/language-reference/domain-modifiers/DKIM_BUILDER.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
---
name: DKIM_BUILDER
parameters:
- label
- selector
- pubkey
- flags
- label
- version
- hashtypes
- keytype
- servicetypes
- note
- servicetypes
- flags
- ttl
parameters_object: true
parameter_types:
label: string?
selector: string
pubkey: string
flags: string[]?
hashtypes: string[]?
pubkey: string?
label: string?
version: string?
hashtypes: string|string[]?
keytype: string?
servicetypes: string[]?
note: string?
servicetypes: string|string[]?
flags: string|string[]?
ttl: Duration?
---

DNSControl contains a `DKIM_BUILDER` which can be used to simply create
DKIM policies for your domains.

DNSControl contains a `DKIM_BUILDER` helper function that generates DKIM DNS TXT records according to RFC 6376 (DomainKeys Identified Mail) and its updates.

## Example
## Examples

### Simple example

Expand All @@ -54,13 +54,15 @@ s1._domainkey IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
DKIM_BUILDER({
label: "alerts",
selector: "k2",
pubkey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L",
flags: ['y'],
hashtypes: ['sha256'],
keytype: 'rsa',
label: "subdomain",
version: "DKIM1",
hashtypes: ['sha1', 'sha256'],
keytype: "rsa",
note: "some human-readable notes",
servicetypes: ['email'],
flags: ['y', 's'],
ttl: 150
}),
);
Expand All @@ -70,23 +72,35 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
This yields the following record:

```text

k2._domainkey.alerts IN TXT "v=DKIM1; k=rsa; s=email; t=y; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L" ttl=150

k2._domainkey.subdomain IN TXT "v=DKIM1; h=sha1:sha256; k=rsa; n=some=20human-readable=20notes; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC5/z4L; s=email; t=y:s" ttl=150
```

### Parameters

* `label:` The DNS label for the DKIM record (`[selector]._domainkey` prefix is added; default: `'@'`)
* `selector:` Selector used for the label. e.g. `s1` or `mail`
* `pubkey:` Public key `p` to be used for DKIM.
* `keytype:` Key type `k`. Defaults to `'rsa'` if omitted (optional)
* `flags:` Which types `t` of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
* `hashtypes:` Acceptable hash algorithms `h` (optional)
* `servicetypes:` Record-applicable service types (optional)
* `note:` Note field `n` for admins. Avoid if possible to keep record length short. (optional)
* `ttl:` Input for `TTL` method (optional)

### Caveats

* DKIM (TXT) records are automatically split using `AUTOSPLIT`.
## Parameters

* `selector` (string, required): The selector subdividing the namespace for the domain.
* `pubkey` (string, optional): The base64-encoded public key (RSA or Ed25519). Default: empty (key revocation or non-sending domain).
* `label` (string, optional): The DNS label for the DKIM record. Default: `@`.
* `version` (string, optional): DKIM version. Maps to the `v=` tag. Default: `DKIM1` (currently the only supported value).
* `hashtypes` (array, optional): Acceptable hash algorithms for signing. Maps to the `h=` tag.
* Supported values for RSA key:
* `sha1`
* `sha256`
* Supported values for Ed25519 key:
* `sha256`
* `keytype` (string, optional): Key algorithm type. Maps to the `k=` tag. Default: `rsa`. Supported values:
* `rsa`
* `ed25519`
* `notes` (string, optional): Human-readable notes intended for administrators. Pass normal text here; DKIM-Quoted-Printable encoding will be applied automatically. Maps to the `n=` tag.
* `servicetypes` (array, optional): Service types using this key. Maps to the `s=` tag. Supported values:
* `*`: explicity allows all service types
* `email`: restricts key to email service only
* `flags` (array, optional): Flags to modify the interpretation of the selector. Maps to the `t=` tag. Supported values:
* `y`: Testing mode.
* `s`: Subdomain restriction.
* `ttl` (number, optional): DNS TTL value in seconds

## Related RFCs

* RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
* RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM
* RFC 8463: A New Cryptographic Signature Method for DKIM (Ed25519)
224 changes: 185 additions & 39 deletions pkg/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1815,69 +1815,215 @@ function CAA_BUILDER(value) {
return r;
}

// DKIM_BUILDER takes an object:
// label: The DNS label for the DKIM record ([selector]._domainkey prefix is added; default: '@')
// selector: Selector used for the label. e.g. s1 or mail
// pubkey: Public key (p) to be used for DKIM (optional)
// keytype: Key type (k). Defaults to 'rsa' if missing (optional)
// flags: Which types (t) of flags to activate, ie. 'y' and/or 's'. Array, defaults to 's' (optional)
// hashtypes: Acceptable hash algorithma (h) (optional)
// servicetypes: Record-applicable service types (optional)
// note: Note field fo admins. Avoid if possible to keep record length short. (optional)
// ttl: The time for TTL, integer or string. (default: not defined, using DefaultTTL)
/**
* Encodes a string into DKIM-specific quoted-printable format.
*
* This function converts characters that are outside the range of printable ASCII
* characters, semicolons, DEL, or above ASCII 127 into their quoted-printable
* hex representation, prefixed by '='. This encoding is used in DKIM signatures
* to handle characters safely.
*
* @param {string} str - The input string to encode.
* @returns {string} The DKIM quoted-printable encoded string.
*/
function _encodeDKIMQuotedPrintable(str) {
var hexChars = '0123456789ABCDEF'.split('');
var result = '';

for (var i = 0; i < str.length; i++) {
var charCode = str.charCodeAt(i);
if (
charCode < 0x21 ||
charCode === 0x3b ||
charCode === 0x3d ||
charCode === 0x7f ||
charCode > 0x7f
) {
result +=
'=' + hexChars[(charCode >>> 4) & 15] + hexChars[charCode & 15];
} else {
result += str.charAt(i);
}
}
return result;
}

/**
* Builds a DKIM DNS TXT record according to RFC 6376 its updates
* @param {Object} value - Configuration object for the DKIM record.
* @param {string} value.selector - The selector subdividing the namespace for the domain. **(Required)**
* @param {string} [value.pubkey] - The base64-encoded public key (RSA or Ed25519).
* May be empty for key revocation or non-sending domains.
* @param {string} [value.label='@'] - The DNS label for the DKIM record (`[selector]._domainkey` prefix is added).
* @param {string} [value.version='DKIM1'] - The DKIM version (`v=` tag). Currently, only `"DKIM1"` is supported.
* @param {string|string[]} [value.hashtypes] - Acceptable hash algorithms for signing (`h=` tag).
* - Supported values for RSA: `'sha1'`, `'sha256'`
* - Supported values for Ed25519: `'sha256'`
* @param {string} [value.keytype='rsa'] - Key algorithm type (`k=` tag).
* - Supported values: `'rsa'`, `'ed25519'`
* @param {string|string[]} [value.servicetypes] - Service types using this key (`s=` tag).
* - Supported values: `'*'`, `'email'`
* - `'*'` allows all services; `'email'` restricts usage to email only.
* @param {string|string[]} [value.flags] - Flags modifying selector interpretation (`t=` tag).
* - Supported values: `'y'` (testing mode), `'s'` (subdomain restriction)
* @param {string} [value.note] - Human-readable note for the record (`n=` tag).
* @param {number} [value.ttl] - DNS TTL value in seconds.
*
* @throws {Error} If a required field is missing or a value is invalid.
* @returns {Object} DNS TXT record entries for DKIM
*/

function DKIM_BUILDER(value) {
if (!value) {
value = {};
value = value || {};

// ========================================
// PHASE 1: NORMALIZATION
// ========================================

// Apply defaults using _.defaults()
value = _.defaults(value, {
version: 'DKIM1',
pubkey: '',
label: '@',
});

// Normalize string|array fields to always be arrays
if (!_.isEmpty(value.hashtypes)) {
value.hashtypes = _.isString(value.hashtypes)
? [value.hashtypes]
: value.hashtypes;
}
kvs = [];

if (!value.selector) {
if (!_.isEmpty(value.servicetypes)) {
value.servicetypes = _.isString(value.servicetypes)
? [value.servicetypes]
: value.servicetypes;
}

if (!_.isEmpty(value.flags)) {
value.flags = _.isString(value.flags) ? [value.flags] : value.flags;
}

// ========================================
// PHASE 2: VALIDATION (Fail Fast)
// ========================================

// Static allowed values
var ALLOWED_VERSIONS = ['DKIM1'];
var ALLOWED_KEYTYPES = ['rsa', 'ed25519'];
var ALLOWED_HASHTYPES = {
rsa: ['sha1', 'sha256'],
ed25519: ['sha256'],
};
var ALLOWED_SERVICETYPES = ['*', 'email'];
var ALLOWED_FLAGS = ['y', 's'];

// Required fields
if (_.isEmpty(value.selector)) {
throw 'DKIM_BUILDER selector cannot be empty';
}

// build the label
if (!value.label) {
value.label = '@';
// Version validation
if (!_.contains(ALLOWED_VERSIONS, value.version)) {
throw (
'DKIM_BUILDER version must be one of: ' +
ALLOWED_VERSIONS.join(', ')
);
}

if (value.label !== '@') {
value.label = value.selector + '._domainkey' + '.' + value.label;
} else {
value.label = value.selector + '._domainkey';
// Keytype validation
if (
!_.isEmpty(value.keytype) &&
!_.contains(ALLOWED_KEYTYPES, value.keytype)
) {
throw (
'DKIM_BUILDER keytype must be one of: ' +
ALLOWED_KEYTYPES.join(', ') +
', ' +
value.keytype +
' given'
);
}

kvs.push('v=DKIM1');
if (value.keytype) {
kvs.push('k=' + value.keytype);
// Hashtypes validation (now always an array after normalization)
if (!_.isEmpty(value.hashtypes)) {
var allowedHashtypes = ALLOWED_HASHTYPES[value.keytype || 'rsa'];
var invalidHashtypes = _.difference(value.hashtypes, allowedHashtypes);
if (invalidHashtypes.length > 0) {
throw (
'DKIM_BUILDER hashtypes for ' +
value.keytype +
' must be one of: ' +
allowedHashtypes.join(', ')
);
}
}

if (value.servicetypes) {
kvs.push('s=' + value.servicetypes);
// Servicetypes validation (now always an array after normalization)
if (!_.isEmpty(value.servicetypes)) {
var invalidServicetypes = _.difference(
value.servicetypes,
ALLOWED_SERVICETYPES
);
if (invalidServicetypes.length > 0) {
throw (
'DKIM_BUILDER servicetypes must be one of: ' +
ALLOWED_SERVICETYPES.join(', ')
);
}
}

// Flags validation (now always an array after normalization)
if (!_.isEmpty(value.flags)) {
var invalidFlags = _.difference(value.flags, ALLOWED_FLAGS);
if (invalidFlags.length > 0) {
throw (
'DKIM_BUILDER flags must be one of: ' + ALLOWED_FLAGS.join(', ')
);
}
}

if (value.flags && value.flags.length > 0) {
kvs.push('t=' + value.flags.join(':'));
// ========================================
// PHASE 3: BUILD OUTPUT
// ========================================

// Build record RFC 6376 order: v=, h=, k=, n=, p=, s=, t=
var record = [];

record.push('v=' + value.version);

if (value.hashtypes) {
record.push('h=' + value.hashtypes.join(':'));
}

if (value.hashtypes && value.hashtypes.length > 0) {
kvs.push('h=' + value.hashtypes.join(':'));
if (value.keytype) {
record.push('k=' + value.keytype);
}

if (value.note) {
kvs.push('n=' + value.note);
if (!_.isEmpty(value.note)) {
record.push('n=' + _encodeDKIMQuotedPrintable(value.note));
}

kvs.push('p=' + value.pubkey);
record.push('p=' + value.pubkey);

var DKIM_TTL = function () {};
if (value.ttl) {
DKIM_TTL = TTL(value.ttl);
if (value.servicetypes) {
record.push('s=' + value.servicetypes.join(':'));
}

r = []; // The list of records to return.
r.push(TXT(value.label, kvs.join('\; '), DKIM_TTL));
return r;
if (value.flags) {
record.push('t=' + value.flags.join(':'));
}

// Build label
var fullLabel = value.selector + '._domainkey';
if (value.label !== '@') {
fullLabel += '.' + value.label;
}

// Handle TTL
var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () {};

return TXT(fullLabel, record.join('; '), DKIM_TTL);
}

// DMARC_BUILDER takes an object:
Expand Down