Skip to content
Open
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
183 changes: 182 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Establish TCP connection to a MX server. This module takes a target domain or email address, resolves appropriate MX servers for this target and tries to get a connection, starting from higher priority servers.

Supports unicode hostnames and IPv6.
Supports unicode hostnames, IPv6, MTA-STS, and DANE/TLSA verification.

```
npm install mx-connect
Expand Down Expand Up @@ -84,6 +84,7 @@ You can use a domain name or an email address as the target, for additional conf
- **priority** (defaults to 0) is the MX priority number that is used to sort available MX servers (servers with higher priority are tried first)
- **A** is an array of IPv4 addresses. Optional, resolved from exchange hostname if not set
- **AAAA** is an array of IPv6 addresses. Optional, resolved from exchange hostname if not set
- **tlsaRecords** is an array of pre-resolved TLSA records for DANE verification. Optional, resolved automatically if DANE is enabled
- **ignoreMXHosts** is an array of IP addresses to skip when connecting
- **mxLastError** is an error object to use if all MX hosts are filtered out by `ignoreMXHosts`
- **connectHook** _function (delivery, options, callback)_ is a function handler to run before establishing a tcp connection to current target (defined in `options`). If the `options` object has a `socket` property after the callback then connection is not established. Useful if you want to divert the connection in some cases, for example if the target domain is in the Onion network then you could create a socket against a SOCKS proxy yourself.
Expand All @@ -94,6 +95,11 @@ You can use a domain name or an email address as the target, for additional conf
- **cache** - an object to manage MTA-STS policy cache
- **get(domain)** -> returns cached policy object
- **set(domain, policyObj)** -> caches a policy object
- **dane** is an object for DANE/TLSA configuration (see [DANE Support](#dane-support) section below)
- **enabled** - if `true` then enables DANE verification. Auto-detected based on resolver availability if not specified
- **resolveTlsa(hostname)** - custom async function to resolve TLSA records. If not provided, uses native `dns.resolveTlsa` when available
- **logger(logObj)** - method to log DANE information, logging is disabled by default
- **verify** - if `true` (default), enforces DANE verification and rejects connections that fail. If `false`, only logs failures

### Connection object

Expand All @@ -106,6 +112,181 @@ Function callback or promise resolution provides a connection object with the fo
- **localAddress** is the local IP address used for the connection
- **localHostname** is the local hostname used for the connection
- **localPort** is the local port used for the connection
- **daneEnabled** is `true` if DANE verification is active for this connection
- **daneVerifier** is the DANE certificate verification function (for use during TLS upgrade)
- **tlsaRecords** is an array of TLSA records for this MX host (if DANE is enabled)
- **requireTls** is `true` if TLS is required (set when DANE records exist)

## DANE Support

DANE (DNS-based Authentication of Named Entities) provides a way to authenticate TLS certificates using DNSSEC. This module supports DANE verification for outbound SMTP connections by looking up TLSA records and verifying server certificates against them.

### Security Considerations

> **Important**: DANE security relies on DNSSEC validation. Without DNSSEC, a DNS attacker could potentially inject fake TLSA records and pin a malicious certificate, introducing new security vulnerabilities rather than preventing them.

Currently, Node.js does not expose the DNSSEC AD (Authenticated Data) flag from DNS responses, which means applications cannot verify that TLSA records were DNSSEC-validated by the resolver. This is tracked in [nodejs/node#57159](https://github.com/nodejs/node/issues/57159).

**Recommendations for production use:**

1. **Use a DNSSEC-validating resolver** - Configure your system to use a resolver that performs DNSSEC validation (e.g., Cloudflare's 1.1.1.1, Google's 8.8.8.8, or a local validating resolver like Unbound)
2. **Use DNS-over-HTTPS (DoH)** - [Tangerine](https://github.com/forwardemail/tangerine) provides transport security via HTTPS, which protects against on-path attackers (though this is not a substitute for DNSSEC validation)
3. **Monitor nodejs/node#57159** - When Node.js adds AD flag support, this module will be updated to optionally require DNSSEC validation

For domains with properly configured DNSSEC, DANE provides strong protection against certificate misissuance and man-in-the-middle attacks. For domains without DNSSEC, consider using MTA-STS as an alternative or complementary security mechanism.

### Node.js Version Support

Native `dns.resolveTlsa` support was added in:

| Node.js Version | TLSA Support |
| --------------- | ------------ |
| v24.x (Current) | ✅ Native |
| v23.9.0+ | ✅ Native |
| v22.15.0+ (LTS) | ✅ Native |
| v22.0.0-v22.14 | ❌ None |
| v20.x (LTS) | ❌ None |
| v18.x | ❌ None |

### Automatic Detection

When you enable DANE without providing a custom resolver, mx-connect automatically detects whether native `dns.resolveTlsa` is available:

- If native support exists, DANE is enabled automatically
- If native support is not available and no custom resolver is provided, DANE is disabled with a log message

### Using Tangerine for Older Node.js Versions

For Node.js versions without native TLSA support, you can use [Tangerine](https://github.com/forwardemail/tangerine), a DNS-over-HTTPS resolver that provides `resolveTlsa` functionality:

```javascript
const mxConnect = require('mx-connect');
const Tangerine = require('tangerine');

// Create a Tangerine instance
const tangerine = new Tangerine();

const connection = await mxConnect({
target: 'user@example.com',
dane: {
enabled: true,
resolveTlsa: tangerine.resolveTlsa.bind(tangerine),
logger: console.log
}
});

console.log('Connected to %s:%s', connection.hostname, connection.port);
console.log('DANE enabled:', connection.daneEnabled);

if (connection.tlsaRecords) {
console.log('TLSA records:', connection.tlsaRecords.length);
}

// Use connection.daneVerifier during TLS upgrade
// The verifier function can be passed to tls.connect() as checkServerIdentity
```

### DANE with Redis Caching (Tangerine)

For production use, you can configure Tangerine with Redis caching for better performance:

```javascript
const mxConnect = require('mx-connect');
const Tangerine = require('tangerine');
const Redis = require('ioredis');

const cache = new Redis();
const tangerine = new Tangerine({
cache,
setCacheArgs(key, result) {
return ['PX', Math.round(result.ttl * 1000)];
}
});

const connection = await mxConnect({
target: 'user@example.com',
dane: {
enabled: true,
resolveTlsa: tangerine.resolveTlsa.bind(tangerine),
verify: true, // Enforce DANE verification
logger: logObj => {
console.log('[DANE]', logObj.msg, logObj);
}
}
});
```

### DANE Verification Flow

When DANE is enabled, the following flow occurs:

1. **TLSA Lookup**: Before connecting, mx-connect resolves TLSA records for each MX hostname (e.g., `_25._tcp.mail.example.com`)
2. **Connection**: A TCP connection is established to the MX server
3. **TLS Upgrade**: When upgrading to TLS (STARTTLS), use the `connection.daneVerifier` function as the `checkServerIdentity` option
4. **Certificate Verification**: The server's certificate is verified against the TLSA records

### TLSA Record Format

TLSA records returned by the resolver should have the following structure:

```javascript
{
usage: 3, // 0=PKIX-TA, 1=PKIX-EE, 2=DANE-TA, 3=DANE-EE
selector: 1, // 0=Full certificate, 1=SubjectPublicKeyInfo
mtype: 1, // 0=Full data, 1=SHA-256, 2=SHA-512
cert: Buffer, // Certificate association data
ttl: 3600 // TTL in seconds
}
```

### DANE Usage Types

| Usage | Name | Description | Support Status |
| ----- | ------- | -------------------------------------------------------- | -------------- |
| 0 | PKIX-TA | CA constraint - must chain to specified CA | ⚠️ Limited* |
| 1 | PKIX-EE | Service certificate constraint - must match exactly | ✅ Full |
| 2 | DANE-TA | Trust anchor assertion - specified cert is trust anchor | ⚠️ Limited* |
| 3 | DANE-EE | Domain-issued certificate - certificate must match | ✅ Full |

> **\*Note on DANE-TA and PKIX-TA**: These usage types require access to the full certificate chain, which is not available in the standard TLS `checkServerIdentity` callback. Currently, only the end-entity (leaf) certificate is verified. If the TLSA record matches the end-entity certificate, verification will succeed; otherwise, it will fail even if the record matches a CA certificate in the chain. For most SMTP deployments, DANE-EE (usage=3) is recommended as it provides the strongest security guarantees and is fully supported.

### Combining DANE with MTA-STS

DANE and MTA-STS can be used together. DANE provides stronger security guarantees (when DNSSEC is properly configured), while MTA-STS provides a fallback for domains that don't support DNSSEC:

```javascript
const connection = await mxConnect({
target: 'user@example.com',
mtaSts: {
enabled: true,
cache: mtaStsCache
},
dane: {
enabled: true,
resolveTlsa: tangerine.resolveTlsa.bind(tangerine)
}
});
// Both MTA-STS and DANE checks are performed
```

### Accessing DANE Utilities

The DANE module is exported for direct use:

```javascript
const { dane } = require('mx-connect');

// Check if native TLSA resolution is available
console.log('Native TLSA support:', dane.hasNativeResolveTlsa);

// DANE constants
console.log('DANE Usage Types:', dane.DANE_USAGE);
console.log('DANE Selectors:', dane.DANE_SELECTOR);
console.log('DANE Matching Types:', dane.DANE_MATCHING_TYPE);

// Verify a certificate against TLSA records
const result = dane.verifyCertAgainstTlsa(certificate, tlsaRecords);
```

## License

Expand Down
Loading