@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 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":"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"}
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"}
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tak-ps/node-tak",
3
3
  "type": "module",
4
- "version": "12.25.0",
4
+ "version": "12.26.0",
5
5
  "description": "Lightweight JavaScript library for communicating with TAK Server",
6
6
  "author": "Nick Ingalls <nick@ingalls.ca>",
7
7
  "main": "dist/index.js",
@@ -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
+ });