Skip to content

Commit bdc0a2a

Browse files
committed
ndncert: ServerDnsChallenge
1 parent 63c489f commit bdc0a2a

7 files changed

Lines changed: 235 additions & 19 deletions

File tree

pkg/ndncert/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"ajv": "^8.20.0",
3939
"b64-lite": "^1.4.0",
4040
"imap-emails": "^1.0.4",
41+
"is-valid-hostname": "^1.0.2",
4142
"nodemailer": "^9.0.1",
4243
"p-timeout": "^7.0.1",
4344
"tslib": "^2.8.1",
@@ -48,6 +49,9 @@
4849
"@ndn/fw": "workspace:^",
4950
"@ndn/repo": "workspace:*",
5051
"@types/b64-lite": "^1.4.2",
51-
"@types/mailparser": "^3.4.6"
52+
"@types/koa": "^3.0.3",
53+
"@types/mailparser": "^3.4.6",
54+
"cf-doh": "^1.1.1",
55+
"koa": "^3.2.1"
5256
}
5357
}

pkg/ndncert/src/client/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function requestCertificate({
4949
}: ClientOptions): Promise<Certificate> {
5050
cOpts = {
5151
describe: `NDNCERT-client(${profile.prefix}, REQUEST, ${privateKey.name})`,
52-
retx: 4,
52+
retx: 4, // XXX retransmission may cause server rejection for duplicate SigNonce
5353
...cOpts,
5454
verifier: profile.publicKey,
5555
};

pkg/ndncert/src/packet/challenge-response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function checkFieldsByStatus({
6666
];
6767
}
6868
default: {
69-
if (!challengeStatus || !remainingTries || !remainingTime) {
69+
if (!challengeStatus || remainingTries === undefined || remainingTime === undefined) {
7070
throw new Error("challengeStatus, remainingTries, remainingTime missing");
7171
}
7272
return () => [
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { SigInfo } from "@ndn/packet";
2+
import { fromUtf8, toHex, toUtf8 } from "@ndn/util";
3+
import type { DOHResponse } from "cf-doh";
4+
import isValidHostname from "is-valid-hostname";
5+
6+
import type { ChallengeRequest } from "../packet/mod";
7+
import type { ServerChallenge, ServerChallengeContext, ServerChallengeResponse } from "./challenge";
8+
9+
interface State {
10+
record: string;
11+
token: string;
12+
}
13+
14+
/** The "dns" challenge where client creates a DNS TXT record. */
15+
export class ServerDnsChallenge implements ServerChallenge<State> {
16+
public readonly challengeId = "dns";
17+
public readonly timeLimit = 300000;
18+
public readonly retryLimit = 3;
19+
20+
private readonly dohServer: string;
21+
22+
constructor({
23+
dohServer = "https://cloudflare-dns.com/dns-query",
24+
}: ServerDnsChallenge.Options = {}) {
25+
this.dohServer = dohServer;
26+
}
27+
28+
public async process(request: ChallengeRequest, context: ServerChallengeContext<State>): Promise<ServerChallengeResponse> {
29+
if (!context.challengeState) {
30+
return this.process0(request, context);
31+
}
32+
return this.process1(request, context);
33+
}
34+
35+
private async process0(request: ChallengeRequest, context: ServerChallengeContext<State>): Promise<ServerChallengeResponse> {
36+
const { domain: domainWire } = request.parameters;
37+
if (!domainWire) {
38+
return { challengeStatus: "missing-domain" };
39+
}
40+
const domain = fromUtf8(domainWire);
41+
if (!isValidHostname(domain)) {
42+
return { challengeStatus: "invalid-domain" };
43+
}
44+
45+
const record = `_ndncert-challenge.${domain}`;
46+
const token = toHex(SigInfo.generateNonce(16));
47+
context.challengeState = { record, token };
48+
return {
49+
challengeStatus: "need-record",
50+
parameters: {
51+
"record-name": toUtf8(record),
52+
"expected-value": toUtf8(token),
53+
},
54+
};
55+
}
56+
57+
private async process1(
58+
request: ChallengeRequest,
59+
{ challengeState }: ServerChallengeContext<State>,
60+
): Promise<ServerChallengeResponse> {
61+
const { confirmation } = request.parameters;
62+
if (!confirmation) {
63+
return { challengeStatus: "missing-confirmation" };
64+
}
65+
66+
let ok: boolean;
67+
try {
68+
ok = await this.checkRecord(challengeState!);
69+
} catch {
70+
ok = false;
71+
}
72+
return ok ? { success: true } : {
73+
decrementRetry: true,
74+
challengeStatus: "wrong-record",
75+
};
76+
}
77+
78+
private async checkRecord({ record, token }: State): Promise<boolean> {
79+
const url = new URL(this.dohServer);
80+
url.searchParams.set("name", record);
81+
url.searchParams.set("type", "TXT");
82+
83+
console.log("sdccr", url);
84+
const res = await fetch(url, { headers: { Accept: "application/dns-json" } });
85+
console.log("sdccr", res);
86+
if (res.status !== 200) {
87+
return false;
88+
}
89+
const j: DOHResponse = await res.json();
90+
console.log("sdccr", j);
91+
92+
if (Number(j.Status) !== 0) {
93+
return false;
94+
}
95+
for (const answer of j.Answer ?? []) {
96+
if (
97+
[record, `${record}.`].includes(answer.name) &&
98+
Number(answer.type) === 16 &&
99+
[token, `"${token}"`].includes(answer.data)
100+
) {
101+
return true;
102+
}
103+
}
104+
return false;
105+
}
106+
}
107+
108+
export namespace ServerDnsChallenge {
109+
export interface Options {
110+
/**
111+
* DNS-over-HTTPS server with application/dns-json capability.
112+
* Common choices includes:
113+
* - https://cloudflare-dns.com/dns-query
114+
* - https://dns.google/resolve
115+
*/
116+
dohServer?: string;
117+
}
118+
}

pkg/ndncert/src/server/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type * from "./challenge";
2+
export * from "./dns-challenge";
23
export * from "./email-challenge_node";
34
export * from "./nop-challenge";
45
export * from "./pin-challenge";

pkg/ndncert/src/server/server.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,23 @@ export class Server {
245245
context.challengeRemainingTries! -= 1;
246246
}
247247

248+
if (context.challengeRemainingTries! <= 0) {
249+
this.deleteContext(request);
250+
return ErrorMsg.makeData(ErrorCode.OutOfTries, request.interest, this.signer);
251+
}
252+
const remainingTime = context.expiry - now;
253+
if (remainingTime <= 0) {
254+
this.deleteContext(request);
255+
return ErrorMsg.makeData(ErrorCode.OutOfTime, request.interest, this.signer);
256+
}
257+
248258
const response = await ChallengeResponse.build({
249259
...this.makeResponseCommon(request, context),
250260
status: Status.CHALLENGE,
251261
challengeStatus,
252262
parameters,
253263
remainingTries: context.challengeRemainingTries!,
254-
remainingTime: context.expiry - now,
264+
remainingTime,
255265
});
256266
return response.data;
257267
}

0 commit comments

Comments
 (0)