@tak-ps/node-tak 12.25.0 → 12.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/lib/api/certificate.d.ts +77 -0
- package/dist/lib/api/certificate.js +159 -0
- package/dist/lib/api/certificate.js.map +1 -1
- package/dist/test/certificate.test.d.ts +1 -0
- package/dist/test/certificate.test.js +301 -0
- package/dist/test/certificate.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/api/certificate.ts +184 -0
- package/package.json +1 -1
- package/test/certificate.test.ts +339 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
### Pending
|
|
14
14
|
|
|
15
|
+
### v12.26.0 - 2026-08-24
|
|
16
|
+
|
|
17
|
+
- :tada: Add `Certificate.listActive()` (`GET /Marti/api/certadmin/cert/active`) & `Certificate.downloadIds()` (`GET /Marti/api/certadmin/cert/download/{ids}`, returns the ZIP archive as a `Readable`)
|
|
18
|
+
- :tada: Add `Certificate.validate(pem)` which evaluates expiry locally and looks up the TAK Server revocation record by SHA-256 fingerprint (the same `hash` consulted by `X509Authenticator`) via the user's certificate list, returning `expired`/`revoked`/`known`/`valid` along with the matching `certificate` record - `CertificateValidation` is exported. The record is fetched with `get(hash)` (a single cached row on the TAK Server) falling back to the user's certificate list when the server answers 500, which is how it reports an unknown hash
|
|
19
|
+
- :tada: Add `Certificate.probe()` which checks whether the TAK Server accepts the API's own credentials via `GET /Marti/api/version` - the cheapest authoritative check as the X509 filter runs (and rethrows) on every request. Returns `{ accepted, version }` or `{ accepted: false, reason: 'revoked' | 'rejected' | 'tls', message }`; non-authentication errors are rethrown. `CertificateProbe` is exported
|
|
20
|
+
|
|
15
21
|
### v12.25.0 - 2026-08-24
|
|
16
22
|
|
|
17
23
|
- :tada: TAK Server HTML error pages (Tomcat/Spring "Exception Report" documents) returned by the API are now parsed into a `TAKServerError` instead of the raw HTML being used as the error message - `message`/`safe` contain the human readable `Message` (falling back to `Description`, the Exception's message, the page `<title>` or the visible text) while `details` contains a plain-text breakdown (status line, labelled fields & exception trace) suitable for an "Advanced" UI section and `html` retains the unmodified page. Pages are parsed with the existing `@tak-ps/xml-js` dependency (Tomcat emits well-formed XHTML) with a visible-text fallback for malformed documents. `TAKServerError`, `parseHTMLError()`, `isHTML()`, `summarizeHTMLError()` & `formatHTMLError()` are exported
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Commands from '../commands.js';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
2
3
|
import type { Static } from '@sinclair/typebox';
|
|
3
4
|
export declare const Certificate: import("@sinclair/typebox").TObject<{
|
|
4
5
|
id: import("@sinclair/typebox").TInteger;
|
|
@@ -57,6 +58,39 @@ export declare const TAKItem_Certificate: import("@sinclair/typebox").TObject<{
|
|
|
57
58
|
messages: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
58
59
|
nodeId: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
59
60
|
}>;
|
|
61
|
+
export declare const CertificateValidation: import("@sinclair/typebox").TObject<{
|
|
62
|
+
fingerprint: import("@sinclair/typebox").TString;
|
|
63
|
+
subject: import("@sinclair/typebox").TString;
|
|
64
|
+
username: import("@sinclair/typebox").TString;
|
|
65
|
+
validFrom: import("@sinclair/typebox").TString;
|
|
66
|
+
validTo: import("@sinclair/typebox").TString;
|
|
67
|
+
expired: import("@sinclair/typebox").TBoolean;
|
|
68
|
+
known: import("@sinclair/typebox").TBoolean;
|
|
69
|
+
revoked: import("@sinclair/typebox").TBoolean;
|
|
70
|
+
revocationDate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
71
|
+
valid: import("@sinclair/typebox").TBoolean;
|
|
72
|
+
certificate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
73
|
+
id: import("@sinclair/typebox").TInteger;
|
|
74
|
+
creatorDn: import("@sinclair/typebox").TString;
|
|
75
|
+
subjectDn: import("@sinclair/typebox").TString;
|
|
76
|
+
userDn: import("@sinclair/typebox").TString;
|
|
77
|
+
certificate: import("@sinclair/typebox").TString;
|
|
78
|
+
hash: import("@sinclair/typebox").TString;
|
|
79
|
+
clientUid: import("@sinclair/typebox").TString;
|
|
80
|
+
issuanceDate: import("@sinclair/typebox").TString;
|
|
81
|
+
expirationDate: import("@sinclair/typebox").TString;
|
|
82
|
+
effectiveDate: import("@sinclair/typebox").TString;
|
|
83
|
+
revocationDate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
84
|
+
token: import("@sinclair/typebox").TString;
|
|
85
|
+
serialNumber: import("@sinclair/typebox").TString;
|
|
86
|
+
}>>;
|
|
87
|
+
}>;
|
|
88
|
+
export declare const CertificateProbe: import("@sinclair/typebox").TObject<{
|
|
89
|
+
accepted: import("@sinclair/typebox").TBoolean;
|
|
90
|
+
version: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
91
|
+
reason: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"revoked">, import("@sinclair/typebox").TLiteral<"rejected">, import("@sinclair/typebox").TLiteral<"tls">]>>;
|
|
92
|
+
message: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
93
|
+
}>;
|
|
60
94
|
export default class CertificateCommands extends Commands {
|
|
61
95
|
schema: {};
|
|
62
96
|
cli(): Promise<object | string>;
|
|
@@ -66,6 +100,14 @@ export default class CertificateCommands extends Commands {
|
|
|
66
100
|
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/getAll_1 TAK Server Docs}.
|
|
67
101
|
*/
|
|
68
102
|
list(username?: string): Promise<Static<typeof TAKList_Certificate>>;
|
|
103
|
+
/**
|
|
104
|
+
* List Active Certificates
|
|
105
|
+
*
|
|
106
|
+
* The most recently issued certificate for each Client UID / User DN pair
|
|
107
|
+
*
|
|
108
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/getActive TAK Server Docs}.
|
|
109
|
+
*/
|
|
110
|
+
listActive(): Promise<Static<typeof TAKList_Certificate>>;
|
|
69
111
|
/**
|
|
70
112
|
* List Revoked Certificates
|
|
71
113
|
*
|
|
@@ -84,6 +126,33 @@ export default class CertificateCommands extends Commands {
|
|
|
84
126
|
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/getExpired TAK Server Docs}.
|
|
85
127
|
*/
|
|
86
128
|
listExpired(): Promise<Static<typeof TAKList_Certificate>>;
|
|
129
|
+
/**
|
|
130
|
+
* Probe whether the TAK Server accepts the credentials this API instance was created with
|
|
131
|
+
*
|
|
132
|
+
* The X509 filter runs on every request and rethrows on failure, so `GET /Marti/api/version`
|
|
133
|
+
* (an anonymous, DB-free endpoint returning a short string) is the cheapest authoritative
|
|
134
|
+
* check - unlike {@link validate} it reflects the server's actual enforcement (e.g. whether
|
|
135
|
+
* `x509checkRevocation` is enabled) and requires no admin credentials. An expired client
|
|
136
|
+
* certificate is rejected during the TLS handshake and surfaces as `reason: 'tls'`.
|
|
137
|
+
*
|
|
138
|
+
* Only authentication failures are returned as a verdict - any other error (server
|
|
139
|
+
* unreachable, unexpected status) is rethrown so it is not mistaken for a rejection
|
|
140
|
+
*/
|
|
141
|
+
probe(): Promise<Static<typeof CertificateProbe>>;
|
|
142
|
+
/**
|
|
143
|
+
* Validate a PEM encoded client certificate against local expiry and the TAK Server revocation record
|
|
144
|
+
*
|
|
145
|
+
* Expiry is evaluated locally from the certificate. Revocation is looked up via the
|
|
146
|
+
* Certificate Admin API using the same SHA-256 fingerprint the TAK Server `X509Authenticator`
|
|
147
|
+
* consults - `get(hash)` first (a single cached row) falling back to the user's certificate
|
|
148
|
+
* list when the TAK Server answers 500, which is how it reports an unknown hash.
|
|
149
|
+
*
|
|
150
|
+
* Note: The TAK Server only enforces revocation when `auth.x509checkRevocation` (or
|
|
151
|
+
* `x509TokenAuth`) is enabled in CoreConfig - `revoked` reports the stored state regardless
|
|
152
|
+
*/
|
|
153
|
+
validate(pem: string, opts?: {
|
|
154
|
+
now?: Date;
|
|
155
|
+
}): Promise<Static<typeof CertificateValidation>>;
|
|
87
156
|
/**
|
|
88
157
|
* Get Single Certificate
|
|
89
158
|
*
|
|
@@ -96,6 +165,14 @@ export default class CertificateCommands extends Commands {
|
|
|
96
165
|
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/downloadCertificate TAK Server Docs}.
|
|
97
166
|
*/
|
|
98
167
|
download(hash: string): Promise<string>;
|
|
168
|
+
/**
|
|
169
|
+
* Download Certificates by Id
|
|
170
|
+
*
|
|
171
|
+
* Returns a ZIP archive stream containing one `{n}_{userDn}_ClientCert.pem` entry per certificate
|
|
172
|
+
*
|
|
173
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/downloadCertificates TAK Server Docs}.
|
|
174
|
+
*/
|
|
175
|
+
downloadIds(ids: Array<string | number>): Promise<Readable>;
|
|
99
176
|
/**
|
|
100
177
|
* Revoke Single Certificate
|
|
101
178
|
*
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import Commands from '../commands.js';
|
|
2
|
+
import Err from '@openaddresses/batch-error';
|
|
3
|
+
import { X509Certificate } from 'node:crypto';
|
|
4
|
+
import { TAKServerError } from '../utils/html-error.js';
|
|
2
5
|
import { Type } from '@sinclair/typebox';
|
|
3
6
|
import { TAKItem, TAKList } from './types.js';
|
|
4
7
|
export const Certificate = Type.Object({
|
|
@@ -18,6 +21,41 @@ export const Certificate = Type.Object({
|
|
|
18
21
|
});
|
|
19
22
|
export const TAKList_Certificate = TAKList(Certificate);
|
|
20
23
|
export const TAKItem_Certificate = TAKItem(Certificate);
|
|
24
|
+
export const CertificateValidation = Type.Object({
|
|
25
|
+
fingerprint: Type.String({ description: 'SHA-256 fingerprint of the DER encoded certificate - the TAK Server `hash`' }),
|
|
26
|
+
subject: Type.String(),
|
|
27
|
+
username: Type.String({ description: 'Common Name of the certificate subject - the TAK Server `userDn`' }),
|
|
28
|
+
validFrom: Type.String(),
|
|
29
|
+
validTo: Type.String(),
|
|
30
|
+
expired: Type.Boolean({ description: 'validTo is in the past' }),
|
|
31
|
+
known: Type.Boolean({ description: 'The TAK Server has a record for this fingerprint' }),
|
|
32
|
+
revoked: Type.Boolean({ description: 'The TAK Server record carries a revocationDate' }),
|
|
33
|
+
revocationDate: Type.Optional(Type.String({ format: 'date-time' })),
|
|
34
|
+
valid: Type.Boolean({ description: 'Neither expired nor revoked' }),
|
|
35
|
+
certificate: Type.Optional(Certificate)
|
|
36
|
+
});
|
|
37
|
+
export const CertificateProbe = Type.Object({
|
|
38
|
+
accepted: Type.Boolean({ description: 'The TAK Server authenticated the credentials' }),
|
|
39
|
+
version: Type.Optional(Type.String({ description: 'TAK Server version string when accepted' })),
|
|
40
|
+
reason: Type.Optional(Type.Union([
|
|
41
|
+
Type.Literal('revoked', { description: 'X509Authenticator raised RevokedException' }),
|
|
42
|
+
Type.Literal('rejected', { description: 'Authentication failed for another reason (BadCredentialsException)' }),
|
|
43
|
+
Type.Literal('tls', { description: 'The TLS handshake failed - typically an expired or untrusted client certificate' })
|
|
44
|
+
])),
|
|
45
|
+
message: Type.Optional(Type.String())
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Extract the Common Name from an X509 subject string as reported by Node
|
|
49
|
+
* (newline separated `key=value` pairs)
|
|
50
|
+
*/
|
|
51
|
+
function commonName(subject) {
|
|
52
|
+
for (const line of subject.split('\n')) {
|
|
53
|
+
const [key, ...rest] = line.split('=');
|
|
54
|
+
if (key.trim() === 'CN')
|
|
55
|
+
return rest.join('=').trim();
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
21
59
|
export default class CertificateCommands extends Commands {
|
|
22
60
|
schema = {};
|
|
23
61
|
async cli() {
|
|
@@ -35,6 +73,17 @@ export default class CertificateCommands extends Commands {
|
|
|
35
73
|
}
|
|
36
74
|
return await this.api.fetch(url);
|
|
37
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* List Active Certificates
|
|
78
|
+
*
|
|
79
|
+
* The most recently issued certificate for each Client UID / User DN pair
|
|
80
|
+
*
|
|
81
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/getActive TAK Server Docs}.
|
|
82
|
+
*/
|
|
83
|
+
async listActive() {
|
|
84
|
+
const url = new URL('/Marti/api/certadmin/cert/active', this.api.url);
|
|
85
|
+
return await this.api.fetch(url);
|
|
86
|
+
}
|
|
38
87
|
/**
|
|
39
88
|
* List Revoked Certificates
|
|
40
89
|
*
|
|
@@ -62,6 +111,93 @@ export default class CertificateCommands extends Commands {
|
|
|
62
111
|
const url = new URL('/Marti/api/certadmin/cert/expired', this.api.url);
|
|
63
112
|
return await this.api.fetch(url);
|
|
64
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Probe whether the TAK Server accepts the credentials this API instance was created with
|
|
116
|
+
*
|
|
117
|
+
* The X509 filter runs on every request and rethrows on failure, so `GET /Marti/api/version`
|
|
118
|
+
* (an anonymous, DB-free endpoint returning a short string) is the cheapest authoritative
|
|
119
|
+
* check - unlike {@link validate} it reflects the server's actual enforcement (e.g. whether
|
|
120
|
+
* `x509checkRevocation` is enabled) and requires no admin credentials. An expired client
|
|
121
|
+
* certificate is rejected during the TLS handshake and surfaces as `reason: 'tls'`.
|
|
122
|
+
*
|
|
123
|
+
* Only authentication failures are returned as a verdict - any other error (server
|
|
124
|
+
* unreachable, unexpected status) is rethrown so it is not mistaken for a rejection
|
|
125
|
+
*/
|
|
126
|
+
async probe() {
|
|
127
|
+
const url = new URL('/Marti/api/version', this.api.url);
|
|
128
|
+
try {
|
|
129
|
+
const version = String(await this.api.fetch(url)).trim();
|
|
130
|
+
return { accepted: true, version };
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
134
|
+
const details = err instanceof TAKServerError ? err.details : '';
|
|
135
|
+
const haystack = `${message}\n${details}`;
|
|
136
|
+
let reason;
|
|
137
|
+
if (/RevokedException|revoked certificate/i.test(haystack)) {
|
|
138
|
+
reason = 'revoked';
|
|
139
|
+
}
|
|
140
|
+
else if (/BadCredentialsException|AuthenticationException|TAK Server authentication/i.test(haystack)) {
|
|
141
|
+
reason = 'rejected';
|
|
142
|
+
}
|
|
143
|
+
else if (err instanceof Err && err.status === 400 && !(err instanceof TAKServerError)
|
|
144
|
+
&& /certificate|handshake|alert|EPROTO|ECONNRESET|SSL|TLS/i.test(haystack)) {
|
|
145
|
+
reason = 'tls';
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
return { accepted: false, reason, message };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate a PEM encoded client certificate against local expiry and the TAK Server revocation record
|
|
155
|
+
*
|
|
156
|
+
* Expiry is evaluated locally from the certificate. Revocation is looked up via the
|
|
157
|
+
* Certificate Admin API using the same SHA-256 fingerprint the TAK Server `X509Authenticator`
|
|
158
|
+
* consults - `get(hash)` first (a single cached row) falling back to the user's certificate
|
|
159
|
+
* list when the TAK Server answers 500, which is how it reports an unknown hash.
|
|
160
|
+
*
|
|
161
|
+
* Note: The TAK Server only enforces revocation when `auth.x509checkRevocation` (or
|
|
162
|
+
* `x509TokenAuth`) is enabled in CoreConfig - `revoked` reports the stored state regardless
|
|
163
|
+
*/
|
|
164
|
+
async validate(pem, opts = {}) {
|
|
165
|
+
const x509 = new X509Certificate(pem);
|
|
166
|
+
const now = opts.now ?? new Date();
|
|
167
|
+
const username = commonName(x509.subject);
|
|
168
|
+
if (!username)
|
|
169
|
+
throw new Error('Certificate subject does not contain a Common Name');
|
|
170
|
+
const fingerprint = x509.fingerprint256.toUpperCase();
|
|
171
|
+
let certificate;
|
|
172
|
+
try {
|
|
173
|
+
// Single cached row lookup - the same query X509Authenticator performs
|
|
174
|
+
certificate = (await this.get(fingerprint)).data;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// The TAK Server reports an unknown hash via sendError(500), which is
|
|
178
|
+
// indistinguishable from a genuine failure - disambiguate against the
|
|
179
|
+
// (uncached, but authoritative) list of the user's certificates
|
|
180
|
+
if (!(err instanceof Err) || err.status < 500)
|
|
181
|
+
throw err;
|
|
182
|
+
certificate = (await this.list(username)).data
|
|
183
|
+
.find((cert) => cert.hash.toUpperCase() === fingerprint);
|
|
184
|
+
}
|
|
185
|
+
const expired = Date.parse(x509.validTo) < now.getTime();
|
|
186
|
+
const revoked = !!certificate?.revocationDate;
|
|
187
|
+
return {
|
|
188
|
+
fingerprint,
|
|
189
|
+
subject: x509.subject,
|
|
190
|
+
username,
|
|
191
|
+
validFrom: x509.validFrom,
|
|
192
|
+
validTo: x509.validTo,
|
|
193
|
+
expired,
|
|
194
|
+
known: !!certificate,
|
|
195
|
+
revoked,
|
|
196
|
+
revocationDate: certificate?.revocationDate,
|
|
197
|
+
valid: !expired && !revoked,
|
|
198
|
+
certificate
|
|
199
|
+
};
|
|
200
|
+
}
|
|
65
201
|
/**
|
|
66
202
|
* Get Single Certificate
|
|
67
203
|
*
|
|
@@ -82,6 +218,29 @@ export default class CertificateCommands extends Commands {
|
|
|
82
218
|
method: 'GET',
|
|
83
219
|
});
|
|
84
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Download Certificates by Id
|
|
223
|
+
*
|
|
224
|
+
* Returns a ZIP archive stream containing one `{n}_{userDn}_ClientCert.pem` entry per certificate
|
|
225
|
+
*
|
|
226
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/downloadCertificates TAK Server Docs}.
|
|
227
|
+
*/
|
|
228
|
+
async downloadIds(ids) {
|
|
229
|
+
if (ids.length === 0) {
|
|
230
|
+
throw new Error('At least one ID must be provided');
|
|
231
|
+
}
|
|
232
|
+
const url = new URL(`/Marti/api/certadmin/cert/download/${ids.map((id) => encodeURIComponent(String(id))).join(',')}`, this.api.url);
|
|
233
|
+
const res = await this.api.fetch(url, {
|
|
234
|
+
method: 'GET'
|
|
235
|
+
}, true);
|
|
236
|
+
// The raw fetch path skips status validation - without this check a
|
|
237
|
+
// TAK Server error page would be streamed as if it were the ZIP archive
|
|
238
|
+
if (res.status < 200 || res.status >= 400) {
|
|
239
|
+
const body = await res.text().catch(() => '');
|
|
240
|
+
throw new Err(res.status, null, body || `Status Code: ${res.status}`);
|
|
241
|
+
}
|
|
242
|
+
return res.body;
|
|
243
|
+
}
|
|
85
244
|
/**
|
|
86
245
|
* Revoke Single Certificate
|
|
87
246
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../../../lib/api/certificate.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE;IAClB,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACxB,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACxB,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;IACrB,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE;IAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE;IACnB,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACxB,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAClD,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACpD,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACnD,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACnE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE;IACpB,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE;CAC9B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AAExD,MAAM,CAAC,OAAO,OAAO,mBAAoB,SAAQ,QAAQ;IACrD,MAAM,GAAG,EAAE,CAAA;IAEX,KAAK,CAAC,GAAG;QACL,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CACN,QAAiB;QAEjB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,2BAA2B,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAE/D,IAAI,QAAQ,EAAE,CAAC;YACX,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAClD,CAAC;QAED,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAuC,CAAC;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,WAAW;QACb,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,mCAAmC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvE,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAuC,CAAC;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,YAAY;QACd,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,oCAAoC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAExE,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAuC,CAAC;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,WAAW;QACb,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,mCAAmC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvE,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAuC,CAAC;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAG,CACL,IAAY;QAEZ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,6BAA6B,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvE,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAuC,CAAC;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CACV,IAAY;QAEZ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,6BAA6B,IAAI,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEhF,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;YAC7B,MAAM,EAAE,KAAK;SAChB,CAAW,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM,CACR,IAAY;QAEZ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,6BAA6B,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvE,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;YAC7B,MAAM,EAAE,QAAQ;SACnB,CAAuC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,SAAS,CACX,GAAa;QAEb,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACxD,CAAC;aAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,yEAAyE;YACzE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,oCAAoC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEhF,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;YAC7B,MAAM,EAAE,QAAQ;SACnB,CAAuC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,SAAS,CACX,GAAa;QAEb,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACxD,CAAC;aAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,yEAAyE;YACzE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,oCAAoC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEhF,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;YAC7B,MAAM,EAAE,QAAQ;SACnB,CAAuC,CAAC;IAC7C,CAAC;CACJ"}
|
|
1
|
+
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../../../lib/api/certificate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { Client } from 'undici';
|
|
5
|
+
import TAKAPI from '../lib/api.js';
|
|
6
|
+
import { APIAuthCertificate } from '../lib/auth.js';
|
|
7
|
+
function mockResponse(statusCode, contentType, body) {
|
|
8
|
+
return {
|
|
9
|
+
statusCode,
|
|
10
|
+
headers: { 'content-type': contentType },
|
|
11
|
+
body: Readable.from([Buffer.from(body)]),
|
|
12
|
+
trailers: {}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Self-signed test certificate: O=CO-TAK, OU=WILDFIRE, CN=alice@example.com, expires 2036-08-22
|
|
16
|
+
const FIXTURE_PEM = `-----BEGIN CERTIFICATE-----
|
|
17
|
+
MIIB1TCCAXugAwIBAgIUHPumta9q/l2whKqG1nGD9pJ/uogwCgYIKoZIzj0EAwIw
|
|
18
|
+
QDEPMA0GA1UECgwGQ08tVEFLMREwDwYDVQQLDAhXSUxERklSRTEaMBgGA1UEAwwR
|
|
19
|
+
YWxpY2VAZXhhbXBsZS5jb20wHhcNMjYwODI1MDEwNTQxWhcNMzYwODIyMDEwNTQx
|
|
20
|
+
WjBAMQ8wDQYDVQQKDAZDTy1UQUsxETAPBgNVBAsMCFdJTERGSVJFMRowGAYDVQQD
|
|
21
|
+
DBFhbGljZUBleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB45
|
|
22
|
+
OnCT2nVQVxlgKDLttbhLW9Yjs2/elFjAZkil+iI0QLM+vtRjdoCdI+NhADggvJ65
|
|
23
|
+
+++JRYr8Kv2zwv2joSKjUzBRMB0GA1UdDgQWBBS8Depzqkj88/9GNHWY8GmyreFB
|
|
24
|
+
VTAfBgNVHSMEGDAWgBS8Depzqkj88/9GNHWY8GmyreFBVTAPBgNVHRMBAf8EBTAD
|
|
25
|
+
AQH/MAoGCCqGSM49BAMCA0gAMEUCIQCU1mg7e/Eu4A84SemDc0bIQHTnM9aVP8h1
|
|
26
|
+
LaburcgKWwIgYeKSRCQGrM1cEhJQbqJC6UWpJUGxGcclcMzGjqx7WpQ=
|
|
27
|
+
-----END CERTIFICATE-----`;
|
|
28
|
+
const FIXTURE_FINGERPRINT = '37:CA:5A:CD:2E:AC:F5:41:09:4B:DE:21:34:9F:A8:83:C4:43:20:71:7E:7F:CC:EC:97:2C:9E:AE:E9:5F:83:40';
|
|
29
|
+
const GET_PATH = `/Marti/api/certadmin/cert/${FIXTURE_FINGERPRINT}`;
|
|
30
|
+
const LIST_PATH = '/Marti/api/certadmin/cert?username=alice%40example.com';
|
|
31
|
+
function certList(certs) {
|
|
32
|
+
return JSON.stringify({ version: '3', type: 'TakCert', data: certs, nodeId: 'node' });
|
|
33
|
+
}
|
|
34
|
+
function certItem(cert) {
|
|
35
|
+
return JSON.stringify({ version: '3', type: 'TakCert', data: cert, nodeId: 'node' });
|
|
36
|
+
}
|
|
37
|
+
// Tomcat's default page for response.sendError(500) - what the TAK Server
|
|
38
|
+
// returns from getCertificate() when the hash is unknown
|
|
39
|
+
const TOMCAT_500 = '<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body><h1>HTTP Status 500 – Internal Server Error</h1></body></html>';
|
|
40
|
+
async function collect(stream) {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
for await (const chunk of stream)
|
|
43
|
+
chunks.push(Buffer.from(chunk));
|
|
44
|
+
return Buffer.concat(chunks);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Install a path-keyed mock; returns the list of paths requested in order
|
|
48
|
+
*/
|
|
49
|
+
function mockRoutes(routes) {
|
|
50
|
+
const originalRequest = Client.prototype.request;
|
|
51
|
+
const paths = [];
|
|
52
|
+
Client.prototype.request = async function (opts) {
|
|
53
|
+
const path = String(opts.path);
|
|
54
|
+
paths.push(path);
|
|
55
|
+
const route = routes[path];
|
|
56
|
+
if (!route)
|
|
57
|
+
throw new Error(`Unexpected request: ${opts.method ?? 'GET'} ${path}`);
|
|
58
|
+
return route();
|
|
59
|
+
};
|
|
60
|
+
return { paths, restore: () => { Client.prototype.request = originalRequest; } };
|
|
61
|
+
}
|
|
62
|
+
function api() {
|
|
63
|
+
return new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
64
|
+
}
|
|
65
|
+
test('Certificate.listActive requests the active certificate list', async () => {
|
|
66
|
+
const mock = mockRoutes({
|
|
67
|
+
'/Marti/api/certadmin/cert/active': () => mockResponse(200, 'application/json', certList([{ id: 1, hash: 'abc', userDn: 'alice' }]))
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const res = await api().Certificate.listActive();
|
|
71
|
+
assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/active']);
|
|
72
|
+
assert.equal(res.data.length, 1);
|
|
73
|
+
assert.equal(res.data[0].hash, 'abc');
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
mock.restore();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
test('Certificate.validate resolves a known certificate with a single get(hash) call', async () => {
|
|
80
|
+
const mock = mockRoutes({
|
|
81
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
85
|
+
assert.deepEqual(mock.paths, [GET_PATH]);
|
|
86
|
+
assert.equal(res.fingerprint, FIXTURE_FINGERPRINT);
|
|
87
|
+
assert.equal(res.username, 'alice@example.com');
|
|
88
|
+
assert.equal(res.expired, false);
|
|
89
|
+
assert.equal(res.known, true);
|
|
90
|
+
assert.equal(res.revoked, false);
|
|
91
|
+
assert.equal(res.revocationDate, undefined);
|
|
92
|
+
assert.equal(res.valid, true);
|
|
93
|
+
assert.equal(res.certificate?.id, 2);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
mock.restore();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
test('Certificate.validate reports a revoked certificate', async () => {
|
|
100
|
+
const mock = mockRoutes({
|
|
101
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com', revocationDate: '2026-08-24T21:00:00.000Z' }))
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
105
|
+
assert.equal(res.known, true);
|
|
106
|
+
assert.equal(res.revoked, true);
|
|
107
|
+
assert.equal(res.revocationDate, '2026-08-24T21:00:00.000Z');
|
|
108
|
+
assert.equal(res.expired, false);
|
|
109
|
+
assert.equal(res.valid, false);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
mock.restore();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
test('Certificate.validate falls back to the user list when get(hash) answers 500 and matches case-insensitively', async () => {
|
|
116
|
+
const mock = mockRoutes({
|
|
117
|
+
[GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
|
|
118
|
+
[LIST_PATH]: () => mockResponse(200, 'application/json', certList([
|
|
119
|
+
{ id: 1, hash: 'AA:BB', userDn: 'alice@example.com', revocationDate: '2026-01-01T00:00:00.000Z' },
|
|
120
|
+
{ id: 2, hash: FIXTURE_FINGERPRINT.toLowerCase(), userDn: 'alice@example.com' }
|
|
121
|
+
]))
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
125
|
+
assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
|
|
126
|
+
assert.equal(res.known, true);
|
|
127
|
+
assert.equal(res.revoked, false);
|
|
128
|
+
assert.equal(res.valid, true);
|
|
129
|
+
assert.equal(res.certificate?.id, 2);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
mock.restore();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
test('Certificate.validate reports an unknown certificate as not revoked but not known', async () => {
|
|
136
|
+
const mock = mockRoutes({
|
|
137
|
+
[GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
|
|
138
|
+
[LIST_PATH]: () => mockResponse(200, 'application/json', certList([]))
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
142
|
+
assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
|
|
143
|
+
assert.equal(res.known, false);
|
|
144
|
+
assert.equal(res.revoked, false);
|
|
145
|
+
assert.equal(res.certificate, undefined);
|
|
146
|
+
assert.equal(res.valid, true);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
mock.restore();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
test('Certificate.validate rethrows non-500 failures from get(hash) without consulting the list', async () => {
|
|
153
|
+
const mock = mockRoutes({
|
|
154
|
+
[GET_PATH]: () => mockResponse(403, 'text/plain', 'Forbidden')
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await assert.rejects(() => api().Certificate.validate(FIXTURE_PEM), (err) => {
|
|
158
|
+
assert.equal(err.status, 403);
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
assert.deepEqual(mock.paths, [GET_PATH]);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
mock.restore();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
test('Certificate.validate evaluates expiry locally against the supplied clock', async () => {
|
|
168
|
+
const mock = mockRoutes({
|
|
169
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
const res = await api().Certificate.validate(FIXTURE_PEM, { now: new Date('2040-01-01T00:00:00Z') });
|
|
173
|
+
assert.equal(res.expired, true);
|
|
174
|
+
assert.equal(res.revoked, false);
|
|
175
|
+
assert.equal(res.valid, false);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
mock.restore();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
test('Certificate.validate rejects a PEM that is not a certificate', async () => {
|
|
182
|
+
await assert.rejects(() => api().Certificate.validate('not a certificate'));
|
|
183
|
+
});
|
|
184
|
+
test('Certificate.probe reports accepted credentials with the server version', async () => {
|
|
185
|
+
const mock = mockRoutes({
|
|
186
|
+
'/Marti/api/version': () => mockResponse(200, 'text/plain', 'TAK Server 5.4-RELEASE-1\n')
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
const res = await api().Certificate.probe();
|
|
190
|
+
assert.deepEqual(mock.paths, ['/Marti/api/version']);
|
|
191
|
+
assert.deepEqual(res, { accepted: true, version: 'TAK Server 5.4-RELEASE-1' });
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
mock.restore();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
test('Certificate.probe classifies a RevokedException error page as revoked', async () => {
|
|
198
|
+
const mock = mockRoutes({
|
|
199
|
+
'/Marti/api/version': () => mockResponse(500, 'text/html', [
|
|
200
|
+
'<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body>',
|
|
201
|
+
'<h1>HTTP Status 500 – Internal Server Error</h1><p><b>Type</b> Exception Report</p>',
|
|
202
|
+
'<p><b>Message</b> Exception performing TAK Server authentication</p>',
|
|
203
|
+
'<p><b>Exception</b></p><pre>org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication</pre>',
|
|
204
|
+
'<p><b>Root Cause</b></p><pre>com.bbn.marti.remote.exception.RevokedException: Attempt to use revoked certificate : CN=alice@example.com,OU=WILDFIRE,O=CO-TAK</pre>',
|
|
205
|
+
'</body></html>'
|
|
206
|
+
].join(''))
|
|
207
|
+
});
|
|
208
|
+
try {
|
|
209
|
+
const res = await api().Certificate.probe();
|
|
210
|
+
assert.equal(res.accepted, false);
|
|
211
|
+
assert.equal(res.reason, 'revoked');
|
|
212
|
+
assert.ok(res.message);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
mock.restore();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
test('Certificate.probe classifies a BadCredentialsException without a revocation cause as rejected', async () => {
|
|
219
|
+
const mock = mockRoutes({
|
|
220
|
+
'/Marti/api/version': () => mockResponse(500, 'text/plain', 'org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication')
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
const res = await api().Certificate.probe();
|
|
224
|
+
assert.equal(res.accepted, false);
|
|
225
|
+
assert.equal(res.reason, 'rejected');
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
mock.restore();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
test('Certificate.probe classifies a TLS handshake failure as tls', async () => {
|
|
232
|
+
const originalRequest = Client.prototype.request;
|
|
233
|
+
Client.prototype.request = async function () {
|
|
234
|
+
const err = new Error('fetch failed');
|
|
235
|
+
err.cause = Object.assign(new Error('write EPROTO SSL routines:ssl3_read_bytes:sslv3 alert bad certificate'), { code: 'EPROTO' });
|
|
236
|
+
throw err;
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
const res = await api().Certificate.probe();
|
|
240
|
+
assert.equal(res.accepted, false);
|
|
241
|
+
assert.equal(res.reason, 'tls');
|
|
242
|
+
assert.match(res.message ?? '', /EPROTO/);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
Client.prototype.request = originalRequest;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
test('Certificate.probe rethrows errors that are not authentication failures', async () => {
|
|
249
|
+
const mock = mockRoutes({
|
|
250
|
+
'/Marti/api/version': () => mockResponse(503, 'text/plain', 'Service Unavailable')
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
await assert.rejects(() => api().Certificate.probe(), (err) => {
|
|
254
|
+
assert.equal(err.status, 503);
|
|
255
|
+
return true;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
mock.restore();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
test('Certificate.downloadIds streams the ZIP archive for the given IDs', async () => {
|
|
263
|
+
const mock = mockRoutes({
|
|
264
|
+
'/Marti/api/certadmin/cert/download/1,2,3': () => mockResponse(200, 'application/zip', Buffer.from('zip-bytes'))
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
const body = await collect(await api().Certificate.downloadIds([1, '2', 3]));
|
|
268
|
+
assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/download/1,2,3']);
|
|
269
|
+
assert.equal(body.toString(), 'zip-bytes');
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
mock.restore();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
test('Certificate.downloadIds rejects an empty ID list without a request', async () => {
|
|
276
|
+
const mock = mockRoutes({});
|
|
277
|
+
try {
|
|
278
|
+
await assert.rejects(() => api().Certificate.downloadIds([]), /At least one ID must be provided/);
|
|
279
|
+
assert.deepEqual(mock.paths, []);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
mock.restore();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
test('Certificate.downloadIds throws on a TAK Server error instead of streaming the error page', async () => {
|
|
286
|
+
const mock = mockRoutes({
|
|
287
|
+
'/Marti/api/certadmin/cert/download/1': () => mockResponse(500, 'text/plain', 'exception in downloadCertificates!')
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
await assert.rejects(() => api().Certificate.downloadIds([1]), (err) => {
|
|
291
|
+
assert.ok(err instanceof Error);
|
|
292
|
+
assert.equal(err.status, 500);
|
|
293
|
+
assert.match(err.message, /downloadCertificates/);
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
mock.restore();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
//# sourceMappingURL=certificate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certificate.test.js","sourceRoot":"","sources":["../../test/certificate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../cli.ts","../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/html-error.test.ts","../test/mission-layer.test.ts","../test/mission.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/multipart.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/video.ts","../lib/utils/html-error.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
|
|
1
|
+
{"root":["../cli.ts","../index.ts","../test/certificate.test.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/html-error.test.ts","../test/mission-layer.test.ts","../test/mission.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/multipart.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/video.ts","../lib/utils/html-error.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
|
package/lib/api/certificate.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import Commands from '../commands.js';
|
|
2
|
+
import Err from '@openaddresses/batch-error';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { X509Certificate } from 'node:crypto';
|
|
5
|
+
import { TAKServerError } from '../utils/html-error.js';
|
|
2
6
|
import { Type } from '@sinclair/typebox';
|
|
3
7
|
import type { Static } from '@sinclair/typebox';
|
|
4
8
|
import { TAKItem, TAKList } from './types.js';
|
|
@@ -22,6 +26,44 @@ export const Certificate = Type.Object({
|
|
|
22
26
|
export const TAKList_Certificate = TAKList(Certificate);
|
|
23
27
|
export const TAKItem_Certificate = TAKItem(Certificate);
|
|
24
28
|
|
|
29
|
+
export const CertificateValidation = Type.Object({
|
|
30
|
+
fingerprint: Type.String({ description: 'SHA-256 fingerprint of the DER encoded certificate - the TAK Server `hash`' }),
|
|
31
|
+
subject: Type.String(),
|
|
32
|
+
username: Type.String({ description: 'Common Name of the certificate subject - the TAK Server `userDn`' }),
|
|
33
|
+
validFrom: Type.String(),
|
|
34
|
+
validTo: Type.String(),
|
|
35
|
+
expired: Type.Boolean({ description: 'validTo is in the past' }),
|
|
36
|
+
known: Type.Boolean({ description: 'The TAK Server has a record for this fingerprint' }),
|
|
37
|
+
revoked: Type.Boolean({ description: 'The TAK Server record carries a revocationDate' }),
|
|
38
|
+
revocationDate: Type.Optional(Type.String({ format: 'date-time' })),
|
|
39
|
+
valid: Type.Boolean({ description: 'Neither expired nor revoked' }),
|
|
40
|
+
certificate: Type.Optional(Certificate)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const CertificateProbe = Type.Object({
|
|
44
|
+
accepted: Type.Boolean({ description: 'The TAK Server authenticated the credentials' }),
|
|
45
|
+
version: Type.Optional(Type.String({ description: 'TAK Server version string when accepted' })),
|
|
46
|
+
reason: Type.Optional(Type.Union([
|
|
47
|
+
Type.Literal('revoked', { description: 'X509Authenticator raised RevokedException' }),
|
|
48
|
+
Type.Literal('rejected', { description: 'Authentication failed for another reason (BadCredentialsException)' }),
|
|
49
|
+
Type.Literal('tls', { description: 'The TLS handshake failed - typically an expired or untrusted client certificate' })
|
|
50
|
+
])),
|
|
51
|
+
message: Type.Optional(Type.String())
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract the Common Name from an X509 subject string as reported by Node
|
|
56
|
+
* (newline separated `key=value` pairs)
|
|
57
|
+
*/
|
|
58
|
+
function commonName(subject: string): string | undefined {
|
|
59
|
+
for (const line of subject.split('\n')) {
|
|
60
|
+
const [key, ...rest] = line.split('=');
|
|
61
|
+
if (key.trim() === 'CN') return rest.join('=').trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
25
67
|
export default class CertificateCommands extends Commands {
|
|
26
68
|
schema = {}
|
|
27
69
|
|
|
@@ -46,6 +88,19 @@ export default class CertificateCommands extends Commands {
|
|
|
46
88
|
return await this.api.fetch(url) as Static<typeof TAKList_Certificate>;
|
|
47
89
|
}
|
|
48
90
|
|
|
91
|
+
/**
|
|
92
|
+
* List Active Certificates
|
|
93
|
+
*
|
|
94
|
+
* The most recently issued certificate for each Client UID / User DN pair
|
|
95
|
+
*
|
|
96
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/getActive TAK Server Docs}.
|
|
97
|
+
*/
|
|
98
|
+
async listActive(): Promise<Static<typeof TAKList_Certificate>> {
|
|
99
|
+
const url = new URL('/Marti/api/certadmin/cert/active', this.api.url);
|
|
100
|
+
|
|
101
|
+
return await this.api.fetch(url) as Static<typeof TAKList_Certificate>;
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
/**
|
|
50
105
|
* List Revoked Certificates
|
|
51
106
|
*
|
|
@@ -79,6 +134,105 @@ export default class CertificateCommands extends Commands {
|
|
|
79
134
|
return await this.api.fetch(url) as Static<typeof TAKList_Certificate>;
|
|
80
135
|
}
|
|
81
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Probe whether the TAK Server accepts the credentials this API instance was created with
|
|
139
|
+
*
|
|
140
|
+
* The X509 filter runs on every request and rethrows on failure, so `GET /Marti/api/version`
|
|
141
|
+
* (an anonymous, DB-free endpoint returning a short string) is the cheapest authoritative
|
|
142
|
+
* check - unlike {@link validate} it reflects the server's actual enforcement (e.g. whether
|
|
143
|
+
* `x509checkRevocation` is enabled) and requires no admin credentials. An expired client
|
|
144
|
+
* certificate is rejected during the TLS handshake and surfaces as `reason: 'tls'`.
|
|
145
|
+
*
|
|
146
|
+
* Only authentication failures are returned as a verdict - any other error (server
|
|
147
|
+
* unreachable, unexpected status) is rethrown so it is not mistaken for a rejection
|
|
148
|
+
*/
|
|
149
|
+
async probe(): Promise<Static<typeof CertificateProbe>> {
|
|
150
|
+
const url = new URL('/Marti/api/version', this.api.url);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const version = String(await this.api.fetch(url)).trim();
|
|
154
|
+
|
|
155
|
+
return { accepted: true, version };
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
const details = err instanceof TAKServerError ? err.details : '';
|
|
159
|
+
const haystack = `${message}\n${details}`;
|
|
160
|
+
|
|
161
|
+
let reason: Static<typeof CertificateProbe>['reason'];
|
|
162
|
+
if (/RevokedException|revoked certificate/i.test(haystack)) {
|
|
163
|
+
reason = 'revoked';
|
|
164
|
+
} else if (/BadCredentialsException|AuthenticationException|TAK Server authentication/i.test(haystack)) {
|
|
165
|
+
reason = 'rejected';
|
|
166
|
+
} else if (
|
|
167
|
+
err instanceof Err && err.status === 400 && !(err instanceof TAKServerError)
|
|
168
|
+
&& /certificate|handshake|alert|EPROTO|ECONNRESET|SSL|TLS/i.test(haystack)
|
|
169
|
+
) {
|
|
170
|
+
reason = 'tls';
|
|
171
|
+
} else {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { accepted: false, reason, message };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate a PEM encoded client certificate against local expiry and the TAK Server revocation record
|
|
181
|
+
*
|
|
182
|
+
* Expiry is evaluated locally from the certificate. Revocation is looked up via the
|
|
183
|
+
* Certificate Admin API using the same SHA-256 fingerprint the TAK Server `X509Authenticator`
|
|
184
|
+
* consults - `get(hash)` first (a single cached row) falling back to the user's certificate
|
|
185
|
+
* list when the TAK Server answers 500, which is how it reports an unknown hash.
|
|
186
|
+
*
|
|
187
|
+
* Note: The TAK Server only enforces revocation when `auth.x509checkRevocation` (or
|
|
188
|
+
* `x509TokenAuth`) is enabled in CoreConfig - `revoked` reports the stored state regardless
|
|
189
|
+
*/
|
|
190
|
+
async validate(
|
|
191
|
+
pem: string,
|
|
192
|
+
opts: {
|
|
193
|
+
now?: Date;
|
|
194
|
+
} = {}
|
|
195
|
+
): Promise<Static<typeof CertificateValidation>> {
|
|
196
|
+
const x509 = new X509Certificate(pem);
|
|
197
|
+
const now = opts.now ?? new Date();
|
|
198
|
+
|
|
199
|
+
const username = commonName(x509.subject);
|
|
200
|
+
if (!username) throw new Error('Certificate subject does not contain a Common Name');
|
|
201
|
+
|
|
202
|
+
const fingerprint = x509.fingerprint256.toUpperCase();
|
|
203
|
+
|
|
204
|
+
let certificate: Static<typeof Certificate> | undefined;
|
|
205
|
+
try {
|
|
206
|
+
// Single cached row lookup - the same query X509Authenticator performs
|
|
207
|
+
certificate = (await this.get(fingerprint)).data;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// The TAK Server reports an unknown hash via sendError(500), which is
|
|
210
|
+
// indistinguishable from a genuine failure - disambiguate against the
|
|
211
|
+
// (uncached, but authoritative) list of the user's certificates
|
|
212
|
+
if (!(err instanceof Err) || err.status < 500) throw err;
|
|
213
|
+
|
|
214
|
+
certificate = (await this.list(username)).data
|
|
215
|
+
.find((cert) => cert.hash.toUpperCase() === fingerprint);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const expired = Date.parse(x509.validTo) < now.getTime();
|
|
219
|
+
const revoked = !!certificate?.revocationDate;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
fingerprint,
|
|
223
|
+
subject: x509.subject,
|
|
224
|
+
username,
|
|
225
|
+
validFrom: x509.validFrom,
|
|
226
|
+
validTo: x509.validTo,
|
|
227
|
+
expired,
|
|
228
|
+
known: !!certificate,
|
|
229
|
+
revoked,
|
|
230
|
+
revocationDate: certificate?.revocationDate,
|
|
231
|
+
valid: !expired && !revoked,
|
|
232
|
+
certificate
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
82
236
|
/**
|
|
83
237
|
* Get Single Certificate
|
|
84
238
|
*
|
|
@@ -107,6 +261,36 @@ export default class CertificateCommands extends Commands {
|
|
|
107
261
|
}) as string;
|
|
108
262
|
}
|
|
109
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Download Certificates by Id
|
|
266
|
+
*
|
|
267
|
+
* Returns a ZIP archive stream containing one `{n}_{userDn}_ClientCert.pem` entry per certificate
|
|
268
|
+
*
|
|
269
|
+
* {@link https://docs.tak.gov/api/takserver/redoc#tag/cert-manager-admin-api/operation/downloadCertificates TAK Server Docs}.
|
|
270
|
+
*/
|
|
271
|
+
async downloadIds(
|
|
272
|
+
ids: Array<string | number>
|
|
273
|
+
): Promise<Readable> {
|
|
274
|
+
if (ids.length === 0) {
|
|
275
|
+
throw new Error('At least one ID must be provided');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const url = new URL(`/Marti/api/certadmin/cert/download/${ids.map((id) => encodeURIComponent(String(id))).join(',')}`, this.api.url);
|
|
279
|
+
|
|
280
|
+
const res = await this.api.fetch(url, {
|
|
281
|
+
method: 'GET'
|
|
282
|
+
}, true);
|
|
283
|
+
|
|
284
|
+
// The raw fetch path skips status validation - without this check a
|
|
285
|
+
// TAK Server error page would be streamed as if it were the ZIP archive
|
|
286
|
+
if (res.status < 200 || res.status >= 400) {
|
|
287
|
+
const body = await res.text().catch(() => '');
|
|
288
|
+
throw new Err(res.status, null, body || `Status Code: ${res.status}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return res.body;
|
|
292
|
+
}
|
|
293
|
+
|
|
110
294
|
/**
|
|
111
295
|
* Revoke Single Certificate
|
|
112
296
|
*
|
package/package.json
CHANGED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { Client } from 'undici';
|
|
5
|
+
import type { Dispatcher } from 'undici';
|
|
6
|
+
import TAKAPI from '../lib/api.js';
|
|
7
|
+
import { APIAuthCertificate } from '../lib/auth.js';
|
|
8
|
+
|
|
9
|
+
type RequestArgs = Parameters<Client['request']>[0];
|
|
10
|
+
|
|
11
|
+
function mockResponse(statusCode: number, contentType: string, body: string | Buffer): Dispatcher.ResponseData {
|
|
12
|
+
return {
|
|
13
|
+
statusCode,
|
|
14
|
+
headers: { 'content-type': contentType },
|
|
15
|
+
body: Readable.from([Buffer.from(body)]),
|
|
16
|
+
trailers: {}
|
|
17
|
+
} as unknown as Dispatcher.ResponseData;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Self-signed test certificate: O=CO-TAK, OU=WILDFIRE, CN=alice@example.com, expires 2036-08-22
|
|
21
|
+
const FIXTURE_PEM = `-----BEGIN CERTIFICATE-----
|
|
22
|
+
MIIB1TCCAXugAwIBAgIUHPumta9q/l2whKqG1nGD9pJ/uogwCgYIKoZIzj0EAwIw
|
|
23
|
+
QDEPMA0GA1UECgwGQ08tVEFLMREwDwYDVQQLDAhXSUxERklSRTEaMBgGA1UEAwwR
|
|
24
|
+
YWxpY2VAZXhhbXBsZS5jb20wHhcNMjYwODI1MDEwNTQxWhcNMzYwODIyMDEwNTQx
|
|
25
|
+
WjBAMQ8wDQYDVQQKDAZDTy1UQUsxETAPBgNVBAsMCFdJTERGSVJFMRowGAYDVQQD
|
|
26
|
+
DBFhbGljZUBleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB45
|
|
27
|
+
OnCT2nVQVxlgKDLttbhLW9Yjs2/elFjAZkil+iI0QLM+vtRjdoCdI+NhADggvJ65
|
|
28
|
+
+++JRYr8Kv2zwv2joSKjUzBRMB0GA1UdDgQWBBS8Depzqkj88/9GNHWY8GmyreFB
|
|
29
|
+
VTAfBgNVHSMEGDAWgBS8Depzqkj88/9GNHWY8GmyreFBVTAPBgNVHRMBAf8EBTAD
|
|
30
|
+
AQH/MAoGCCqGSM49BAMCA0gAMEUCIQCU1mg7e/Eu4A84SemDc0bIQHTnM9aVP8h1
|
|
31
|
+
LaburcgKWwIgYeKSRCQGrM1cEhJQbqJC6UWpJUGxGcclcMzGjqx7WpQ=
|
|
32
|
+
-----END CERTIFICATE-----`;
|
|
33
|
+
const FIXTURE_FINGERPRINT = '37:CA:5A:CD:2E:AC:F5:41:09:4B:DE:21:34:9F:A8:83:C4:43:20:71:7E:7F:CC:EC:97:2C:9E:AE:E9:5F:83:40';
|
|
34
|
+
const GET_PATH = `/Marti/api/certadmin/cert/${FIXTURE_FINGERPRINT}`;
|
|
35
|
+
const LIST_PATH = '/Marti/api/certadmin/cert?username=alice%40example.com';
|
|
36
|
+
|
|
37
|
+
function certList(certs: object[]): string {
|
|
38
|
+
return JSON.stringify({ version: '3', type: 'TakCert', data: certs, nodeId: 'node' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function certItem(cert: object): string {
|
|
42
|
+
return JSON.stringify({ version: '3', type: 'TakCert', data: cert, nodeId: 'node' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tomcat's default page for response.sendError(500) - what the TAK Server
|
|
46
|
+
// returns from getCertificate() when the hash is unknown
|
|
47
|
+
const TOMCAT_500 = '<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body><h1>HTTP Status 500 – Internal Server Error</h1></body></html>';
|
|
48
|
+
|
|
49
|
+
async function collect(stream: Readable): Promise<Buffer> {
|
|
50
|
+
const chunks: Buffer[] = [];
|
|
51
|
+
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
52
|
+
return Buffer.concat(chunks);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Install a path-keyed mock; returns the list of paths requested in order
|
|
57
|
+
*/
|
|
58
|
+
function mockRoutes(routes: Record<string, () => Dispatcher.ResponseData>): { paths: string[]; restore: () => void } {
|
|
59
|
+
const originalRequest = Client.prototype.request;
|
|
60
|
+
const paths: string[] = [];
|
|
61
|
+
|
|
62
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
63
|
+
const path = String(opts.path);
|
|
64
|
+
paths.push(path);
|
|
65
|
+
const route = routes[path];
|
|
66
|
+
if (!route) throw new Error(`Unexpected request: ${opts.method ?? 'GET'} ${path}`);
|
|
67
|
+
return route();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return { paths, restore: () => { Client.prototype.request = originalRequest; } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function api(): TAKAPI {
|
|
74
|
+
return new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('Certificate.listActive requests the active certificate list', async () => {
|
|
78
|
+
const mock = mockRoutes({
|
|
79
|
+
'/Marti/api/certadmin/cert/active': () => mockResponse(200, 'application/json', certList([{ id: 1, hash: 'abc', userDn: 'alice' }]))
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const res = await api().Certificate.listActive();
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/active']);
|
|
86
|
+
assert.equal(res.data.length, 1);
|
|
87
|
+
assert.equal(res.data[0].hash, 'abc');
|
|
88
|
+
} finally {
|
|
89
|
+
mock.restore();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('Certificate.validate resolves a known certificate with a single get(hash) call', async () => {
|
|
94
|
+
const mock = mockRoutes({
|
|
95
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(mock.paths, [GET_PATH]);
|
|
102
|
+
assert.equal(res.fingerprint, FIXTURE_FINGERPRINT);
|
|
103
|
+
assert.equal(res.username, 'alice@example.com');
|
|
104
|
+
assert.equal(res.expired, false);
|
|
105
|
+
assert.equal(res.known, true);
|
|
106
|
+
assert.equal(res.revoked, false);
|
|
107
|
+
assert.equal(res.revocationDate, undefined);
|
|
108
|
+
assert.equal(res.valid, true);
|
|
109
|
+
assert.equal(res.certificate?.id, 2);
|
|
110
|
+
} finally {
|
|
111
|
+
mock.restore();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('Certificate.validate reports a revoked certificate', async () => {
|
|
116
|
+
const mock = mockRoutes({
|
|
117
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com', revocationDate: '2026-08-24T21:00:00.000Z' }))
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
122
|
+
|
|
123
|
+
assert.equal(res.known, true);
|
|
124
|
+
assert.equal(res.revoked, true);
|
|
125
|
+
assert.equal(res.revocationDate, '2026-08-24T21:00:00.000Z');
|
|
126
|
+
assert.equal(res.expired, false);
|
|
127
|
+
assert.equal(res.valid, false);
|
|
128
|
+
} finally {
|
|
129
|
+
mock.restore();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Certificate.validate falls back to the user list when get(hash) answers 500 and matches case-insensitively', async () => {
|
|
134
|
+
const mock = mockRoutes({
|
|
135
|
+
[GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
|
|
136
|
+
[LIST_PATH]: () => mockResponse(200, 'application/json', certList([
|
|
137
|
+
{ id: 1, hash: 'AA:BB', userDn: 'alice@example.com', revocationDate: '2026-01-01T00:00:00.000Z' },
|
|
138
|
+
{ id: 2, hash: FIXTURE_FINGERPRINT.toLowerCase(), userDn: 'alice@example.com' }
|
|
139
|
+
]))
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
|
|
146
|
+
assert.equal(res.known, true);
|
|
147
|
+
assert.equal(res.revoked, false);
|
|
148
|
+
assert.equal(res.valid, true);
|
|
149
|
+
assert.equal(res.certificate?.id, 2);
|
|
150
|
+
} finally {
|
|
151
|
+
mock.restore();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('Certificate.validate reports an unknown certificate as not revoked but not known', async () => {
|
|
156
|
+
const mock = mockRoutes({
|
|
157
|
+
[GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
|
|
158
|
+
[LIST_PATH]: () => mockResponse(200, 'application/json', certList([]))
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const res = await api().Certificate.validate(FIXTURE_PEM);
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
|
|
165
|
+
assert.equal(res.known, false);
|
|
166
|
+
assert.equal(res.revoked, false);
|
|
167
|
+
assert.equal(res.certificate, undefined);
|
|
168
|
+
assert.equal(res.valid, true);
|
|
169
|
+
} finally {
|
|
170
|
+
mock.restore();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Certificate.validate rethrows non-500 failures from get(hash) without consulting the list', async () => {
|
|
175
|
+
const mock = mockRoutes({
|
|
176
|
+
[GET_PATH]: () => mockResponse(403, 'text/plain', 'Forbidden')
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await assert.rejects(() => api().Certificate.validate(FIXTURE_PEM), (err: unknown) => {
|
|
181
|
+
assert.equal((err as { status?: number }).status, 403);
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
assert.deepEqual(mock.paths, [GET_PATH]);
|
|
185
|
+
} finally {
|
|
186
|
+
mock.restore();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('Certificate.validate evaluates expiry locally against the supplied clock', async () => {
|
|
191
|
+
const mock = mockRoutes({
|
|
192
|
+
[GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const res = await api().Certificate.validate(FIXTURE_PEM, { now: new Date('2040-01-01T00:00:00Z') });
|
|
197
|
+
|
|
198
|
+
assert.equal(res.expired, true);
|
|
199
|
+
assert.equal(res.revoked, false);
|
|
200
|
+
assert.equal(res.valid, false);
|
|
201
|
+
} finally {
|
|
202
|
+
mock.restore();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('Certificate.validate rejects a PEM that is not a certificate', async () => {
|
|
207
|
+
await assert.rejects(() => api().Certificate.validate('not a certificate'));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('Certificate.probe reports accepted credentials with the server version', async () => {
|
|
211
|
+
const mock = mockRoutes({
|
|
212
|
+
'/Marti/api/version': () => mockResponse(200, 'text/plain', 'TAK Server 5.4-RELEASE-1\n')
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const res = await api().Certificate.probe();
|
|
217
|
+
|
|
218
|
+
assert.deepEqual(mock.paths, ['/Marti/api/version']);
|
|
219
|
+
assert.deepEqual(res, { accepted: true, version: 'TAK Server 5.4-RELEASE-1' });
|
|
220
|
+
} finally {
|
|
221
|
+
mock.restore();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('Certificate.probe classifies a RevokedException error page as revoked', async () => {
|
|
226
|
+
const mock = mockRoutes({
|
|
227
|
+
'/Marti/api/version': () => mockResponse(500, 'text/html', [
|
|
228
|
+
'<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body>',
|
|
229
|
+
'<h1>HTTP Status 500 – Internal Server Error</h1><p><b>Type</b> Exception Report</p>',
|
|
230
|
+
'<p><b>Message</b> Exception performing TAK Server authentication</p>',
|
|
231
|
+
'<p><b>Exception</b></p><pre>org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication</pre>',
|
|
232
|
+
'<p><b>Root Cause</b></p><pre>com.bbn.marti.remote.exception.RevokedException: Attempt to use revoked certificate : CN=alice@example.com,OU=WILDFIRE,O=CO-TAK</pre>',
|
|
233
|
+
'</body></html>'
|
|
234
|
+
].join(''))
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const res = await api().Certificate.probe();
|
|
239
|
+
|
|
240
|
+
assert.equal(res.accepted, false);
|
|
241
|
+
assert.equal(res.reason, 'revoked');
|
|
242
|
+
assert.ok(res.message);
|
|
243
|
+
} finally {
|
|
244
|
+
mock.restore();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('Certificate.probe classifies a BadCredentialsException without a revocation cause as rejected', async () => {
|
|
249
|
+
const mock = mockRoutes({
|
|
250
|
+
'/Marti/api/version': () => mockResponse(500, 'text/plain', 'org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication')
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const res = await api().Certificate.probe();
|
|
255
|
+
|
|
256
|
+
assert.equal(res.accepted, false);
|
|
257
|
+
assert.equal(res.reason, 'rejected');
|
|
258
|
+
} finally {
|
|
259
|
+
mock.restore();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('Certificate.probe classifies a TLS handshake failure as tls', async () => {
|
|
264
|
+
const originalRequest = Client.prototype.request;
|
|
265
|
+
|
|
266
|
+
Client.prototype.request = async function(): Promise<Dispatcher.ResponseData> {
|
|
267
|
+
const err = new Error('fetch failed') as Error & { cause?: Error };
|
|
268
|
+
err.cause = Object.assign(new Error('write EPROTO SSL routines:ssl3_read_bytes:sslv3 alert bad certificate'), { code: 'EPROTO' });
|
|
269
|
+
throw err;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const res = await api().Certificate.probe();
|
|
274
|
+
|
|
275
|
+
assert.equal(res.accepted, false);
|
|
276
|
+
assert.equal(res.reason, 'tls');
|
|
277
|
+
assert.match(res.message ?? '', /EPROTO/);
|
|
278
|
+
} finally {
|
|
279
|
+
Client.prototype.request = originalRequest;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('Certificate.probe rethrows errors that are not authentication failures', async () => {
|
|
284
|
+
const mock = mockRoutes({
|
|
285
|
+
'/Marti/api/version': () => mockResponse(503, 'text/plain', 'Service Unavailable')
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await assert.rejects(() => api().Certificate.probe(), (err: unknown) => {
|
|
290
|
+
assert.equal((err as { status?: number }).status, 503);
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
} finally {
|
|
294
|
+
mock.restore();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('Certificate.downloadIds streams the ZIP archive for the given IDs', async () => {
|
|
299
|
+
const mock = mockRoutes({
|
|
300
|
+
'/Marti/api/certadmin/cert/download/1,2,3': () => mockResponse(200, 'application/zip', Buffer.from('zip-bytes'))
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const body = await collect(await api().Certificate.downloadIds([1, '2', 3]));
|
|
305
|
+
|
|
306
|
+
assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/download/1,2,3']);
|
|
307
|
+
assert.equal(body.toString(), 'zip-bytes');
|
|
308
|
+
} finally {
|
|
309
|
+
mock.restore();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('Certificate.downloadIds rejects an empty ID list without a request', async () => {
|
|
314
|
+
const mock = mockRoutes({});
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await assert.rejects(() => api().Certificate.downloadIds([]), /At least one ID must be provided/);
|
|
318
|
+
assert.deepEqual(mock.paths, []);
|
|
319
|
+
} finally {
|
|
320
|
+
mock.restore();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('Certificate.downloadIds throws on a TAK Server error instead of streaming the error page', async () => {
|
|
325
|
+
const mock = mockRoutes({
|
|
326
|
+
'/Marti/api/certadmin/cert/download/1': () => mockResponse(500, 'text/plain', 'exception in downloadCertificates!')
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await assert.rejects(() => api().Certificate.downloadIds([1]), (err: unknown) => {
|
|
331
|
+
assert.ok(err instanceof Error);
|
|
332
|
+
assert.equal((err as { status?: number }).status, 500);
|
|
333
|
+
assert.match(err.message, /downloadCertificates/);
|
|
334
|
+
return true;
|
|
335
|
+
});
|
|
336
|
+
} finally {
|
|
337
|
+
mock.restore();
|
|
338
|
+
}
|
|
339
|
+
});
|