Skip to content

Commit

Permalink
feat: Implement test utilities (#107)
Browse files Browse the repository at this point in the history
For when users of this lib can't mock this library (e.g., when using Jest).

- [x] Documentation
  • Loading branch information
gnarea authored Dec 29, 2022
1 parent 928147a commit 584b801
Show file tree
Hide file tree
Showing 22 changed files with 344 additions and 84 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,48 @@ dnssecLookUp(QUESTION, RESOLVER, { trustAnchors: [customTrustAnchor] });

If your DNS lookup library parses responses eagerly and doesn't give you access to the original response in wire format, you will have to convert their messages to `Message` instances. Refer to our API docs to learn how to initialise `Message`s.

### API documentation
## Testing

To facilitate the simulation of the various outcomes of DNSSEC validation, we provide the `MockChain` utility so that you can pass a custom resolver and trust anchor to `dnssecLookUp()`. This is particularly useful in unit tests where you aren't able to mock this module (e.g., Jest doesn't support mocking our ESM as of this writing).

The following example shows how to generate a verified RRset:

```javascript
import {
dnssecLookUp,
DnsRecord,
MockChain,
RrSet,
SecurityStatus,
} from '@relaycorp/dnssec';

const RECORD = new DnsRecord(
`example.com.`,
'TXT',
DnsClass.IN,
42,
'The data',
);
const QUESTION = RECORD.makeQuestion();
const RRSET = RrSet.init(QUESTION, [RECORD]);

test('Generating a SECURE result', async () => {
const mockChain = await MockChain.generate(RECORD.name);

const { resolver, trustAnchors } = mockChain.generateFixture(
RRSET,
SecurityStatus.SECURE,
);

const result = await dnssecLookUp(QUESTION, resolver, { trustAnchors });
expect(result).toStrictEqual({
status: SecurityStatus.SECURE,
result: RRSET,
});
});
```

## API documentation

The API documentation is available on [docs.relaycorp.tech](https://docs.relaycorp.tech/dnssec-js/).

Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ export { DigestType } from './lib/DigestType.js';
export { DnssecAlgorithm } from './lib/DnssecAlgorithm.js';
export type { Resolver } from './lib/Resolver.js';
export { dnssecLookUp } from './lib/lookup.js';
export type { ChainVerificationResult, FailureResult, VerifiedRrSet } from './lib/results.js';
export type {
ChainVerificationResult,
FailureResult,
FailureStatus,
VerifiedRrSet,
} from './lib/results.js';
export { SecurityStatus } from './lib/SecurityStatus.js';
export type { TrustAnchor } from './lib/TrustAnchor.js';
export type { VerificationOptions } from './lib/VerificationOptions.js';
export { MockChain } from './lib/testing/MockChain.js';
4 changes: 2 additions & 2 deletions src/integration_tests/dnssecAlgorithms.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { addSeconds, subSeconds } from 'date-fns';

import { DnssecAlgorithm } from '../lib/DnssecAlgorithm.js';
import { ZoneSigner } from '../testUtils/dnssec/ZoneSigner.js';
import { ZoneSigner } from '../lib/utils/dnssec/ZoneSigner.js';
import { Zone } from '../lib/Zone.js';
import { DatePeriod } from '../lib/DatePeriod.js';
import { SecurityStatus } from '../lib/SecurityStatus.js';
import type { SignatureOptions } from '../testUtils/dnssec/SignatureOptions.js';
import type { SignatureOptions } from '../lib/utils/dnssec/SignatureOptions.js';

const NOW = new Date();
const VALIDITY_PERIOD = DatePeriod.init(subSeconds(NOW, 1), addSeconds(NOW, 1));
Expand Down
9 changes: 4 additions & 5 deletions src/lib/SecurityStatus.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/prefer-enum-initializers */
/**
* DNSSEC security status.
*
* See https://www.rfc-editor.org/rfc/rfc4035#section-4.3
*/
export enum SecurityStatus {
SECURE,
INSECURE,
BOGUS,
INDETERMINATE,
SECURE = 'SECURE',
INSECURE = 'INSECURE',
BOGUS = 'BOGUS',
INDETERMINATE = 'INDETERMINATE',
}
4 changes: 2 additions & 2 deletions src/lib/SignedRrSet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { jest } from '@jest/globals';
import { addSeconds, setMilliseconds, subSeconds } from 'date-fns';

import { QUESTION, RECORD, RECORD_TLD, RRSET } from '../testUtils/dnsStubs.js';
import { ZoneSigner } from '../testUtils/dnssec/ZoneSigner.js';
import type { SignatureOptions } from '../testUtils/dnssec/SignatureOptions.js';

import { ZoneSigner } from './utils/dnssec/ZoneSigner.js';
import type { SignatureOptions } from './utils/dnssec/SignatureOptions.js';
import { SignedRrSet } from './SignedRrSet.js';
import { DnssecAlgorithm } from './DnssecAlgorithm.js';
import { RrSet } from './dns/RrSet.js';
Expand Down
8 changes: 4 additions & 4 deletions src/lib/UnverifiedChain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { jest } from '@jest/globals';
import { encode } from '@leichtgewicht/dns-packet';
import { addSeconds, subSeconds } from 'date-fns';

import { ZoneSigner } from '../testUtils/dnssec/ZoneSigner.js';
import { QUESTION, RECORD, RECORD_TLD, RRSET } from '../testUtils/dnsStubs.js';
import type { ZoneResponseSet } from '../testUtils/dnssec/responses.js';
import type { SignatureOptions } from '../testUtils/dnssec/SignatureOptions.js';

import { ZoneSigner } from './utils/dnssec/ZoneSigner.js';
import type { ZoneResponseSet } from './utils/dnssec/responses.js';
import type { SignatureOptions } from './utils/dnssec/SignatureOptions.js';
import { DnssecAlgorithm } from './DnssecAlgorithm.js';
import { Message } from './dns/Message.js';
import { UnverifiedChain } from './UnverifiedChain.js';
Expand Down Expand Up @@ -183,7 +183,7 @@ describe('retrieve', () => {
const chain = await UnverifiedChain.retrieve(QUESTION, resolver);

expect(chain.response.header.rcode).toStrictEqual(rcode);
expect(chain.zoneMessageByKey[`./${DnssecRecordType.DNSKEY}`].header.rcode).toStrictEqual(
expect(chain.zoneMessageByKey[`./${DnssecRecordType.DNSKEY}`]!.header.rcode).toStrictEqual(
rcode,
);
});
Expand Down
37 changes: 14 additions & 23 deletions src/lib/UnverifiedChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,14 @@ import type { Resolver } from './Resolver.js';
import type { DnsClass } from './dns/ianaClasses.js';
import type { DsData } from './rdata/DsData.js';
import type { RrSet } from './dns/RrSet.js';
import { getZonesInChain } from './utils/dns.js';

interface MessageByKey {
readonly [key: string]: Message;
readonly [key: string]: Message | undefined;
}

type FinalResolver = (question: Question) => Promise<Message>;

function getZonesInChain(zoneName: string, shouldIncludeRoot = true): readonly string[] {
if (zoneName === '') {
return shouldIncludeRoot ? ['.'] : [];
}
const parentZoneName = zoneName.replace(/^[^.]+\./u, '');
const parentZones = getZonesInChain(parentZoneName, shouldIncludeRoot);
return [...parentZones, zoneName];
}

async function retrieveZoneMessages(
zoneNames: readonly string[],
recordType: DnssecRecordType,
Expand Down Expand Up @@ -62,17 +54,18 @@ export class UnverifiedChain {
};
}, {});

if (!(query.key in allMessages)) {
const queryResponse = allMessages[query.key];
if (!queryResponse) {
throw new Error(`At least one message must answer ${query.key}`);
}

return new UnverifiedChain(query, allMessages[query.key], messageByKey);
return new UnverifiedChain(query, queryResponse, messageByKey);
}

public static async retrieve(question: Question, resolver: Resolver): Promise<UnverifiedChain> {
const finalResolver: FinalResolver = async (currentQuestion) => {
const message = await resolver(currentQuestion);
return message instanceof Message ? message : Message.deserialise(message);
return message instanceof Buffer ? Message.deserialise(message) : message;
};
const zoneNames = getZonesInChain(question.name);
const dnskeyMessages = await retrieveZoneMessages(
Expand Down Expand Up @@ -119,13 +112,14 @@ export class UnverifiedChain {
datePeriod: DatePeriod,
): VerificationResult<Zone> {
const rootDnskeyKey = `./${DnssecRecordType.DNSKEY}`;
if (!(rootDnskeyKey in this.zoneMessageByKey)) {
const rootDnskeyResponse = this.zoneMessageByKey[rootDnskeyKey];
if (!rootDnskeyResponse) {
return {
status: SecurityStatus.INDETERMINATE,
reasonChain: ['Cannot initialise root zone without a DNSKEY response'],
};
}
const result = Zone.initRoot(this.zoneMessageByKey[rootDnskeyKey], trustAnchors, datePeriod);
const result = Zone.initRoot(rootDnskeyResponse, trustAnchors, datePeriod);
if (result.status !== SecurityStatus.SECURE) {
return augmentFailureResult(result, 'Got invalid DNSKEY for root zone');
}
Expand All @@ -140,26 +134,23 @@ export class UnverifiedChain {
let zones = [rootZone];
for (const zoneName of getZonesInChain(apexZoneName, false)) {
const dnskeyKey = `${zoneName}/${DnssecRecordType.DNSKEY}`;
if (!(dnskeyKey in this.zoneMessageByKey)) {
const dnskeyResponse = this.zoneMessageByKey[dnskeyKey];
if (!dnskeyResponse) {
return {
status: SecurityStatus.INDETERMINATE,
reasonChain: [`Cannot verify zone ${zoneName} without a DNSKEY response`],
};
}
const dsKey = `${zoneName}/${DnssecRecordType.DS}`;
if (!(dsKey in this.zoneMessageByKey)) {
const dsResponse = this.zoneMessageByKey[dsKey];
if (dsResponse === undefined) {
return {
status: SecurityStatus.INDETERMINATE,
reasonChain: [`Cannot verify zone ${zoneName} without a DS response`],
};
}
const parent = zones[zones.length - 1];
const zoneResult = parent.initChild(
zoneName,
this.zoneMessageByKey[dnskeyKey],
this.zoneMessageByKey[dsKey],
datePeriod,
);
const zoneResult = parent.initChild(zoneName, dnskeyResponse, dsResponse, datePeriod);
if (zoneResult.status !== SecurityStatus.SECURE) {
return augmentFailureResult(zoneResult, `Failed to verify zone ${zoneName}`);
}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/Zone.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { addSeconds, subSeconds } from 'date-fns';

import { ZoneSigner } from '../testUtils/dnssec/ZoneSigner.js';
import { QUESTION, RECORD, RECORD_TLD } from '../testUtils/dnsStubs.js';
import type { DnskeyResponse, DsResponse } from '../testUtils/dnssec/responses.js';
import type { SignatureOptions } from '../testUtils/dnssec/SignatureOptions.js';

import { ZoneSigner } from './utils/dnssec/ZoneSigner.js';
import type { DnskeyResponse, DsResponse } from './utils/dnssec/responses.js';
import type { SignatureOptions } from './utils/dnssec/SignatureOptions.js';
import { DnssecAlgorithm } from './DnssecAlgorithm.js';
import { Zone } from './Zone.js';
import { Message } from './dns/Message.js';
Expand Down
9 changes: 3 additions & 6 deletions src/lib/rdata/DnskeyData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import type { DNSKeyData } from '@leichtgewicht/dns-packet';
import { addMinutes, addSeconds, setMilliseconds, subSeconds } from 'date-fns';

import { DnssecAlgorithm } from '../DnssecAlgorithm.js';
import { ZoneSigner } from '../../testUtils/dnssec/ZoneSigner.js';
import { ZoneSigner } from '../utils/dnssec/ZoneSigner.js';
import { RECORD_TLD, RRSET } from '../../testUtils/dnsStubs.js';
import { DatePeriod } from '../DatePeriod.js';
import {
DNSSEC_ROOT_DNSKEY_DATA,
DNSSEC_ROOT_DNSKEY_KEY_TAG,
} from '../../testUtils/dnssec/iana.js';
import type { SignatureOptions } from '../../testUtils/dnssec/SignatureOptions.js';
import { DNSSEC_ROOT_DNSKEY_DATA, DNSSEC_ROOT_DNSKEY_KEY_TAG } from '../utils/dnssec/iana.js';
import type { SignatureOptions } from '../utils/dnssec/SignatureOptions.js';

import { DnskeyData } from './DnskeyData.js';

Expand Down
4 changes: 2 additions & 2 deletions src/lib/rdata/DsData.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { DigestData } from '@leichtgewicht/dns-packet';

import { ZoneSigner } from '../../testUtils/dnssec/ZoneSigner.js';
import { ZoneSigner } from '../utils/dnssec/ZoneSigner.js';
import { DnssecAlgorithm } from '../DnssecAlgorithm.js';
import { DigestType } from '../DigestType.js';
import type { DnskeyRecord, DsRecord } from '../dnssecRecords.js';
import { serialiseName } from '../dns/name.js';
import { generateDigest } from '../utils/crypto/hashing.js';
import { copyDnssecRecordData } from '../../testUtils/dnssec/records.js';
import { copyDnssecRecordData } from '../utils/dnssec/records.js';
import { RECORD_TLD } from '../../testUtils/dnsStubs.js';

import { DnskeyData } from './DnskeyData.js';
Expand Down
4 changes: 2 additions & 2 deletions src/lib/rdata/RrsigData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { addMinutes, setMilliseconds } from 'date-fns';
import type { RRSigData } from '@leichtgewicht/dns-packet';

import { DnssecAlgorithm } from '../DnssecAlgorithm.js';
import { ZoneSigner } from '../../testUtils/dnssec/ZoneSigner.js';
import { ZoneSigner } from '../utils/dnssec/ZoneSigner.js';
import { RrSet } from '../dns/RrSet.js';
import { QUESTION, RECORD, RRSET } from '../../testUtils/dnsStubs.js';
import { IANA_RR_TYPE_IDS } from '../dns/ianaRrTypes.js';
import type { SignatureOptions } from '../../testUtils/dnssec/SignatureOptions.js';
import type { SignatureOptions } from '../utils/dnssec/SignatureOptions.js';

import { RrsigData } from './RrsigData.js';

Expand Down
7 changes: 6 additions & 1 deletion src/lib/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ export interface SuccessfulResult<Result> extends BaseResult {
readonly result: Result;
}

export type FailureStatus =
| SecurityStatus.BOGUS
| SecurityStatus.INDETERMINATE
| SecurityStatus.INSECURE;

export interface FailureResult extends BaseResult {
readonly status: SecurityStatus.BOGUS | SecurityStatus.INDETERMINATE | SecurityStatus.INSECURE;
readonly status: FailureStatus;
readonly reasonChain: readonly string[];
}

Expand Down
90 changes: 90 additions & 0 deletions src/lib/testing/MockChain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { addMinutes, addSeconds, setMilliseconds, subSeconds } from 'date-fns';

import { QUESTION, RECORD, RRSET } from '../../testUtils/dnsStubs.js';
import { SecurityStatus } from '../SecurityStatus.js';
import { dnssecLookUp } from '../lookup.js';
import { type FailureStatus } from '../results.js';
import { DatePeriod } from '../DatePeriod.js';

import { MockChain } from './MockChain.js';

describe('MockChain', () => {
describe('generateResolver', () => {
test('SECURE result should be generated if given an RRset', async () => {
const mockChain = await MockChain.generate(RECORD.name);

const { resolver, trustAnchors } = mockChain.generateFixture(RRSET, SecurityStatus.SECURE);

const result = await dnssecLookUp(QUESTION, resolver, { trustAnchors });
expect(result).toStrictEqual({
status: SecurityStatus.SECURE,
result: RRSET,
});
});

test.each<FailureStatus>([
SecurityStatus.INSECURE,
SecurityStatus.BOGUS,
SecurityStatus.INDETERMINATE,
])('%s result should be generated if requested', async (status) => {
const mockChain = await MockChain.generate(RECORD.name);

const { resolver, trustAnchors } = mockChain.generateFixture(RRSET, status);

const result = await dnssecLookUp(QUESTION, resolver, { trustAnchors });
expect(result.status).toStrictEqual(status);
});

test('Signatures should be valid for 60 seconds by default', async () => {
const testStartDate = new Date();
const mockChain = await MockChain.generate(RECORD.name);

const { resolver, trustAnchors } = mockChain.generateFixture(RRSET, SecurityStatus.SECURE);

await expect(dnssecLookUp(QUESTION, resolver, { trustAnchors })).resolves.toHaveProperty(
'status',
SecurityStatus.SECURE,
);
await expect(
dnssecLookUp(QUESTION, resolver, {
dateOrPeriod: subSeconds(testStartDate, 1),
trustAnchors,
}),
).resolves.toHaveProperty('status', SecurityStatus.BOGUS);
await expect(
dnssecLookUp(QUESTION, resolver, {
dateOrPeriod: addMinutes(addSeconds(testStartDate, 1), 5),
trustAnchors,
}),
).resolves.toHaveProperty('status', SecurityStatus.BOGUS);
});

test('Explicit signature period should be honoured', async () => {
const now = setMilliseconds(new Date(), 0);
const period = DatePeriod.init(subSeconds(now, 120), subSeconds(now, 60));
const mockChain = await MockChain.generate(RECORD.name);

const { resolver, trustAnchors } = mockChain.generateFixture(
RRSET,
SecurityStatus.SECURE,
period,
);

await expect(
dnssecLookUp(QUESTION, resolver, { dateOrPeriod: period.end, trustAnchors }),
).resolves.toHaveProperty('status', SecurityStatus.SECURE);
await expect(
dnssecLookUp(QUESTION, resolver, {
dateOrPeriod: subSeconds(period.start, 1),
trustAnchors,
}),
).resolves.toHaveProperty('status', SecurityStatus.BOGUS);
await expect(
dnssecLookUp(QUESTION, resolver, {
dateOrPeriod: addSeconds(period.end, 1),
trustAnchors,
}),
).resolves.toHaveProperty('status', SecurityStatus.BOGUS);
});
});
});
Loading

0 comments on commit 584b801

Please sign in to comment.