@de-otio/chaoskb-server 0.2.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/dist/lib/admin-handler/index.d.ts +22 -0
- package/dist/lib/admin-handler/index.d.ts.map +1 -0
- package/dist/lib/admin-handler/index.js +92 -0
- package/dist/lib/admin-handler/index.js.map +1 -0
- package/dist/lib/admin-handler/index.ts +123 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts +11 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.js +200 -0
- package/dist/lib/admin-handler/routes/metrics.js.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.ts +234 -0
- package/dist/lib/admin-handler/routes/overview.d.ts +9 -0
- package/dist/lib/admin-handler/routes/overview.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/overview.js +110 -0
- package/dist/lib/admin-handler/routes/overview.js.map +1 -0
- package/dist/lib/admin-handler/routes/overview.ts +133 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts +10 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.js +108 -0
- package/dist/lib/admin-handler/routes/tenants.js.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.ts +134 -0
- package/dist/lib/chaoskb-stack.d.ts +22 -0
- package/dist/lib/chaoskb-stack.d.ts.map +1 -0
- package/dist/lib/chaoskb-stack.js +60 -0
- package/dist/lib/chaoskb-stack.js.map +1 -0
- package/dist/lib/constructs/admin-api.d.ts +16 -0
- package/dist/lib/constructs/admin-api.d.ts.map +1 -0
- package/dist/lib/constructs/admin-api.js +93 -0
- package/dist/lib/constructs/admin-api.js.map +1 -0
- package/dist/lib/constructs/admin-dashboard.d.ts +18 -0
- package/dist/lib/constructs/admin-dashboard.d.ts.map +1 -0
- package/dist/lib/constructs/admin-dashboard.js +172 -0
- package/dist/lib/constructs/admin-dashboard.js.map +1 -0
- package/dist/lib/constructs/api.d.ts +17 -0
- package/dist/lib/constructs/api.d.ts.map +1 -0
- package/dist/lib/constructs/api.js +81 -0
- package/dist/lib/constructs/api.js.map +1 -0
- package/dist/lib/constructs/auth.d.ts +11 -0
- package/dist/lib/constructs/auth.d.ts.map +1 -0
- package/dist/lib/constructs/auth.js +18 -0
- package/dist/lib/constructs/auth.js.map +1 -0
- package/dist/lib/constructs/blob-store.d.ts +10 -0
- package/dist/lib/constructs/blob-store.d.ts.map +1 -0
- package/dist/lib/constructs/blob-store.js +31 -0
- package/dist/lib/constructs/blob-store.js.map +1 -0
- package/dist/lib/deploy-cli.d.ts +3 -0
- package/dist/lib/deploy-cli.d.ts.map +1 -0
- package/dist/lib/deploy-cli.js +49 -0
- package/dist/lib/deploy-cli.js.map +1 -0
- package/dist/lib/handler/index.d.ts +23 -0
- package/dist/lib/handler/index.d.ts.map +1 -0
- package/dist/lib/handler/index.js +276 -0
- package/dist/lib/handler/index.js.map +1 -0
- package/dist/lib/handler/index.ts +372 -0
- package/dist/lib/handler/logger.d.ts +16 -0
- package/dist/lib/handler/logger.d.ts.map +1 -0
- package/dist/lib/handler/logger.js +26 -0
- package/dist/lib/handler/logger.js.map +1 -0
- package/dist/lib/handler/logger.ts +36 -0
- package/dist/lib/handler/middleware/input-validation.d.ts +6 -0
- package/dist/lib/handler/middleware/input-validation.d.ts.map +1 -0
- package/dist/lib/handler/middleware/input-validation.js +36 -0
- package/dist/lib/handler/middleware/input-validation.js.map +1 -0
- package/dist/lib/handler/middleware/input-validation.ts +44 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts +14 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.js +94 -0
- package/dist/lib/handler/middleware/rate-limit.js.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.ts +121 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts +48 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.js +256 -0
- package/dist/lib/handler/middleware/ssh-auth.js.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.ts +300 -0
- package/dist/lib/handler/routes/audit.d.ts +24 -0
- package/dist/lib/handler/routes/audit.d.ts.map +1 -0
- package/dist/lib/handler/routes/audit.js +94 -0
- package/dist/lib/handler/routes/audit.js.map +1 -0
- package/dist/lib/handler/routes/audit.ts +101 -0
- package/dist/lib/handler/routes/blobs.d.ts +13 -0
- package/dist/lib/handler/routes/blobs.d.ts.map +1 -0
- package/dist/lib/handler/routes/blobs.js +298 -0
- package/dist/lib/handler/routes/blobs.js.map +1 -0
- package/dist/lib/handler/routes/blobs.ts +348 -0
- package/dist/lib/handler/routes/devices.d.ts +48 -0
- package/dist/lib/handler/routes/devices.d.ts.map +1 -0
- package/dist/lib/handler/routes/devices.js +394 -0
- package/dist/lib/handler/routes/devices.js.map +1 -0
- package/dist/lib/handler/routes/devices.ts +458 -0
- package/dist/lib/handler/routes/export.d.ts +9 -0
- package/dist/lib/handler/routes/export.d.ts.map +1 -0
- package/dist/lib/handler/routes/export.js +40 -0
- package/dist/lib/handler/routes/export.js.map +1 -0
- package/dist/lib/handler/routes/export.ts +55 -0
- package/dist/lib/handler/routes/github.d.ts +31 -0
- package/dist/lib/handler/routes/github.d.ts.map +1 -0
- package/dist/lib/handler/routes/github.js +118 -0
- package/dist/lib/handler/routes/github.js.map +1 -0
- package/dist/lib/handler/routes/github.ts +162 -0
- package/dist/lib/handler/routes/health.d.ts +6 -0
- package/dist/lib/handler/routes/health.d.ts.map +1 -0
- package/dist/lib/handler/routes/health.js +14 -0
- package/dist/lib/handler/routes/health.js.map +1 -0
- package/dist/lib/handler/routes/health.ts +10 -0
- package/dist/lib/handler/routes/invites.d.ts +24 -0
- package/dist/lib/handler/routes/invites.d.ts.map +1 -0
- package/dist/lib/handler/routes/invites.js +445 -0
- package/dist/lib/handler/routes/invites.js.map +1 -0
- package/dist/lib/handler/routes/invites.ts +527 -0
- package/dist/lib/handler/routes/notifications.d.ts +39 -0
- package/dist/lib/handler/routes/notifications.d.ts.map +1 -0
- package/dist/lib/handler/routes/notifications.js +150 -0
- package/dist/lib/handler/routes/notifications.js.map +1 -0
- package/dist/lib/handler/routes/notifications.ts +163 -0
- package/dist/lib/handler/routes/projects.d.ts +24 -0
- package/dist/lib/handler/routes/projects.d.ts.map +1 -0
- package/dist/lib/handler/routes/projects.js +47 -0
- package/dist/lib/handler/routes/projects.js.map +1 -0
- package/dist/lib/handler/routes/projects.ts +69 -0
- package/dist/lib/handler/routes/register.d.ts +19 -0
- package/dist/lib/handler/routes/register.d.ts.map +1 -0
- package/dist/lib/handler/routes/register.js +327 -0
- package/dist/lib/handler/routes/register.js.map +1 -0
- package/dist/lib/handler/routes/register.ts +363 -0
- package/dist/lib/handler/routes/restore.d.ts +9 -0
- package/dist/lib/handler/routes/restore.d.ts.map +1 -0
- package/dist/lib/handler/routes/restore.js +52 -0
- package/dist/lib/handler/routes/restore.js.map +1 -0
- package/dist/lib/handler/routes/restore.ts +73 -0
- package/dist/lib/handler/routes/revocation.d.ts +13 -0
- package/dist/lib/handler/routes/revocation.d.ts.map +1 -0
- package/dist/lib/handler/routes/revocation.js +63 -0
- package/dist/lib/handler/routes/revocation.js.map +1 -0
- package/dist/lib/handler/routes/revocation.ts +87 -0
- package/dist/lib/handler/routes/rotation.d.ts +24 -0
- package/dist/lib/handler/routes/rotation.d.ts.map +1 -0
- package/dist/lib/handler/routes/rotation.js +291 -0
- package/dist/lib/handler/routes/rotation.js.map +1 -0
- package/dist/lib/handler/routes/rotation.ts +336 -0
- package/dist/lib/handler/routes/tenants.d.ts +11 -0
- package/dist/lib/handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/handler/routes/tenants.js +181 -0
- package/dist/lib/handler/routes/tenants.js.map +1 -0
- package/dist/lib/handler/routes/tenants.ts +198 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts +21 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.js +76 -0
- package/dist/lib/handler/routes/wrapped-key.js.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.ts +108 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +16 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +18 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { DynamoDBDocumentClient, QueryCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
|
|
5
|
+
export interface AuthResult {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class AuthError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
message: string,
|
|
14
|
+
public readonly statusCode: number,
|
|
15
|
+
) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'AuthError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ParsedAuthHeader {
|
|
22
|
+
signature: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
sequence: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Timestamp tolerance: 30 seconds (replay protection is primarily sequence-based). */
|
|
28
|
+
const TIMESTAMP_TOLERANCE_MS = 30 * 1000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the new SSH-Signature authorization scheme.
|
|
32
|
+
*
|
|
33
|
+
* Headers:
|
|
34
|
+
* Authorization: SSH-Signature <base64-signature>
|
|
35
|
+
* X-ChaosKB-Timestamp: <ISO 8601>
|
|
36
|
+
* X-ChaosKB-Sequence: <monotonic counter>
|
|
37
|
+
*/
|
|
38
|
+
export function parseAuthHeaders(headers: Record<string, string>): ParsedAuthHeader {
|
|
39
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
40
|
+
if (!authHeader) {
|
|
41
|
+
throw new AuthError('Missing Authorization header', 401);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!authHeader.startsWith('SSH-Signature ')) {
|
|
45
|
+
// Support legacy format during migration
|
|
46
|
+
if (authHeader.startsWith('ChaosKB-SSH ')) {
|
|
47
|
+
return parseLegacyAuthHeader(authHeader, headers);
|
|
48
|
+
}
|
|
49
|
+
throw new AuthError('Invalid authorization scheme', 401);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const signature = authHeader.slice('SSH-Signature '.length);
|
|
53
|
+
const timestamp = headers['x-chaoskb-timestamp'] || headers['X-ChaosKB-Timestamp'];
|
|
54
|
+
const sequenceStr = headers['x-chaoskb-sequence'] || headers['X-ChaosKB-Sequence'];
|
|
55
|
+
|
|
56
|
+
if (!signature || !timestamp) {
|
|
57
|
+
throw new AuthError('Missing required headers (X-ChaosKB-Timestamp)', 401);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sequence = sequenceStr ? parseInt(sequenceStr, 10) : 0;
|
|
61
|
+
if (isNaN(sequence)) {
|
|
62
|
+
throw new AuthError('Invalid sequence number', 401);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { signature, timestamp, sequence };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse legacy ChaosKB-SSH format for backwards compatibility. */
|
|
69
|
+
function parseLegacyAuthHeader(
|
|
70
|
+
header: string,
|
|
71
|
+
headers: Record<string, string>,
|
|
72
|
+
): ParsedAuthHeader {
|
|
73
|
+
const params = header.slice('ChaosKB-SSH '.length);
|
|
74
|
+
const fields: Record<string, string> = {};
|
|
75
|
+
|
|
76
|
+
for (const part of params.split(', ')) {
|
|
77
|
+
const eqIndex = part.indexOf('=');
|
|
78
|
+
if (eqIndex === -1) continue;
|
|
79
|
+
fields[part.slice(0, eqIndex)] = part.slice(eqIndex + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!fields['sig'] || !fields['ts']) {
|
|
83
|
+
throw new AuthError('Missing required authorization fields', 401);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sequenceStr = headers['x-chaoskb-sequence'] || headers['X-ChaosKB-Sequence'];
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
signature: fields['sig'],
|
|
90
|
+
timestamp: fields['ts'],
|
|
91
|
+
sequence: sequenceStr ? parseInt(sequenceStr, 10) : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Compute SSH key fingerprint (SHA-256 of raw public key, base64). */
|
|
96
|
+
export function fingerprintFromPublicKey(publicKeyBase64: string): string {
|
|
97
|
+
return crypto.createHash('sha256').update(Buffer.from(publicKeyBase64, 'base64')).digest('base64');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function verifyTimestamp(timestamp: string): void {
|
|
101
|
+
const requestTime = new Date(timestamp).getTime();
|
|
102
|
+
if (isNaN(requestTime)) {
|
|
103
|
+
throw new AuthError('Invalid timestamp format', 401);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const diff = Math.abs(now - requestTime);
|
|
108
|
+
if (diff > TIMESTAMP_TOLERANCE_MS) {
|
|
109
|
+
throw new AuthError('Request timestamp expired', 401);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildCanonicalString(
|
|
114
|
+
method: string,
|
|
115
|
+
path: string,
|
|
116
|
+
timestamp: string,
|
|
117
|
+
sequence: number,
|
|
118
|
+
body?: string | null,
|
|
119
|
+
): string {
|
|
120
|
+
const bodyHash = body
|
|
121
|
+
? crypto.createHash('sha256').update(body).digest('hex')
|
|
122
|
+
: '';
|
|
123
|
+
return `chaoskb-auth\n${method} ${path}\n${timestamp}\n${sequence}\n${bodyHash}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check and update the per-device sequence counter for replay protection.
|
|
128
|
+
*
|
|
129
|
+
* Uses a DynamoDB conditional write: only succeeds if the new sequence
|
|
130
|
+
* is strictly greater than the stored highest-seen sequence.
|
|
131
|
+
*/
|
|
132
|
+
export async function checkSequence(
|
|
133
|
+
ddb: DynamoDBDocumentClient,
|
|
134
|
+
tableName: string,
|
|
135
|
+
tenantId: string,
|
|
136
|
+
fingerprint: string,
|
|
137
|
+
sequence: number,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
if (sequence <= 0) {
|
|
140
|
+
throw new AuthError('Sequence number must be positive', 401);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await ddb.send(
|
|
145
|
+
new UpdateCommand({
|
|
146
|
+
TableName: tableName,
|
|
147
|
+
Key: {
|
|
148
|
+
PK: `TENANT#${tenantId}`,
|
|
149
|
+
SK: `SEQUENCE#${fingerprint}`,
|
|
150
|
+
},
|
|
151
|
+
UpdateExpression: 'SET highestSeq = :new',
|
|
152
|
+
ConditionExpression:
|
|
153
|
+
'attribute_not_exists(highestSeq) OR highestSeq < :new',
|
|
154
|
+
ExpressionAttributeValues: {
|
|
155
|
+
':new': sequence,
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
} catch (error: unknown) {
|
|
160
|
+
if (
|
|
161
|
+
error &&
|
|
162
|
+
typeof error === 'object' &&
|
|
163
|
+
'name' in error &&
|
|
164
|
+
error.name === 'ConditionalCheckFailedException'
|
|
165
|
+
) {
|
|
166
|
+
logger.warn('Replay detected: sequence number already seen', {
|
|
167
|
+
tenantId,
|
|
168
|
+
fingerprint,
|
|
169
|
+
sequence,
|
|
170
|
+
});
|
|
171
|
+
throw new AuthError('Replay detected: sequence number already used', 401);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function verifyEd25519Signature(
|
|
178
|
+
publicKeyBase64: string,
|
|
179
|
+
canonicalString: string,
|
|
180
|
+
signatureBase64: string,
|
|
181
|
+
): boolean {
|
|
182
|
+
try {
|
|
183
|
+
const publicKeyBuffer = Buffer.from(publicKeyBase64, 'base64');
|
|
184
|
+
const signatureBuffer = Buffer.from(signatureBase64, 'base64');
|
|
185
|
+
const data = Buffer.from(canonicalString);
|
|
186
|
+
|
|
187
|
+
// Create Ed25519 public key object from raw bytes
|
|
188
|
+
const keyObject = crypto.createPublicKey({
|
|
189
|
+
key: Buffer.concat([
|
|
190
|
+
// Ed25519 DER prefix for a 32-byte public key
|
|
191
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
192
|
+
publicKeyBuffer,
|
|
193
|
+
]),
|
|
194
|
+
format: 'der',
|
|
195
|
+
type: 'spki',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return crypto.verify(null, data, keyObject, signatureBuffer);
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function tenantIdFromPublicKey(publicKeyBase64: string): string {
|
|
205
|
+
const hash = crypto.createHash('sha256').update(publicKeyBase64).digest('hex');
|
|
206
|
+
return hash.slice(0, 32);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function authenticateRequest(
|
|
210
|
+
event: {
|
|
211
|
+
requestContext: { http: { method: string; path: string } };
|
|
212
|
+
headers: Record<string, string>;
|
|
213
|
+
body?: string | null;
|
|
214
|
+
},
|
|
215
|
+
ddb: DynamoDBDocumentClient,
|
|
216
|
+
tableName: string,
|
|
217
|
+
): Promise<AuthResult> {
|
|
218
|
+
const parsed = parseAuthHeaders(event.headers);
|
|
219
|
+
verifyTimestamp(parsed.timestamp);
|
|
220
|
+
|
|
221
|
+
// Look up the public key by querying tenant META records
|
|
222
|
+
// The public key is identified from the request — we need to find which tenant it belongs to
|
|
223
|
+
// For now, extract from legacy header or from a separate header
|
|
224
|
+
const publicKey = extractPublicKey(event.headers);
|
|
225
|
+
const tenantId = tenantIdFromPublicKey(publicKey);
|
|
226
|
+
const fingerprint = fingerprintFromPublicKey(publicKey);
|
|
227
|
+
|
|
228
|
+
// Look up the registered public key in DynamoDB
|
|
229
|
+
const result = await ddb.send(
|
|
230
|
+
new QueryCommand({
|
|
231
|
+
TableName: tableName,
|
|
232
|
+
KeyConditionExpression: 'PK = :pk AND SK = :sk',
|
|
233
|
+
ExpressionAttributeValues: {
|
|
234
|
+
':pk': `TENANT#${tenantId}`,
|
|
235
|
+
':sk': 'META',
|
|
236
|
+
},
|
|
237
|
+
Limit: 1,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!result.Items || result.Items.length === 0) {
|
|
242
|
+
throw new AuthError('Unknown public key', 401);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const tenant = result.Items[0];
|
|
246
|
+
if (tenant['publicKey'] !== publicKey) {
|
|
247
|
+
throw new AuthError('Public key mismatch', 401);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Verify the SSH signature against the canonical string (includes sequence)
|
|
251
|
+
const canonicalString = buildCanonicalString(
|
|
252
|
+
event.requestContext.http.method,
|
|
253
|
+
event.requestContext.http.path,
|
|
254
|
+
parsed.timestamp,
|
|
255
|
+
parsed.sequence,
|
|
256
|
+
event.body,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const valid = verifyEd25519Signature(
|
|
260
|
+
publicKey,
|
|
261
|
+
canonicalString,
|
|
262
|
+
parsed.signature,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (!valid) {
|
|
266
|
+
logger.warn('Signature verification failed', { tenantId });
|
|
267
|
+
throw new AuthError('Invalid signature', 401);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check sequence number for replay protection (after signature verification)
|
|
271
|
+
if (parsed.sequence > 0) {
|
|
272
|
+
await checkSequence(ddb, tableName, tenantId, fingerprint, parsed.sequence);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { tenantId, publicKey, fingerprint };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract the public key from request headers.
|
|
280
|
+
* New format uses X-ChaosKB-PublicKey header; legacy embeds it in the auth header.
|
|
281
|
+
*/
|
|
282
|
+
function extractPublicKey(headers: Record<string, string>): string {
|
|
283
|
+
// New header format
|
|
284
|
+
const pubKeyHeader = headers['x-chaoskb-publickey'] || headers['X-ChaosKB-PublicKey'];
|
|
285
|
+
if (pubKeyHeader) return pubKeyHeader;
|
|
286
|
+
|
|
287
|
+
// Legacy format: ChaosKB-SSH pubkey=..., ts=..., sig=...
|
|
288
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
289
|
+
if (authHeader?.startsWith('ChaosKB-SSH ')) {
|
|
290
|
+
const params = authHeader.slice('ChaosKB-SSH '.length);
|
|
291
|
+
for (const part of params.split(', ')) {
|
|
292
|
+
const eqIndex = part.indexOf('=');
|
|
293
|
+
if (eqIndex !== -1 && part.slice(0, eqIndex) === 'pubkey') {
|
|
294
|
+
return part.slice(eqIndex + 1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
throw new AuthError('Missing public key in request headers', 401);
|
|
300
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
interface HandlerResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
body: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export type AuditEventType = 'registered' | 'rotation-started' | 'rotation-confirmed' | 'rotation-completed' | 'revoked' | 'device-linked' | 'device-removed';
|
|
8
|
+
export interface AuditEvent {
|
|
9
|
+
eventType: AuditEventType;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Write an audit event to DynamoDB.
|
|
15
|
+
*
|
|
16
|
+
* PK: TENANT#{tenantId}, SK: AUDIT#{ISO timestamp}#{random suffix}
|
|
17
|
+
*/
|
|
18
|
+
export declare function logAuditEvent(ddb: DynamoDBDocumentClient, tableName: string, tenantId: string, event: AuditEvent): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Query all audit log entries for a tenant, sorted by timestamp (ascending).
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleGetAuditLog(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/audit.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAGvB,MAAM,uBAAuB,CAAC;AAG/B,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,kBAAkB,GAClB,oBAAoB,GACpB,oBAAoB,GACpB,SAAS,GACT,eAAe,GACf,gBAAgB,CAAC;AAErB,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAID;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAyB1B"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.logAuditEvent = logAuditEvent;
|
|
37
|
+
exports.handleGetAuditLog = handleGetAuditLog;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
40
|
+
const logger_js_1 = require("../logger.js");
|
|
41
|
+
const TTL_90_DAYS_SECONDS = 90 * 24 * 60 * 60;
|
|
42
|
+
/**
|
|
43
|
+
* Write an audit event to DynamoDB.
|
|
44
|
+
*
|
|
45
|
+
* PK: TENANT#{tenantId}, SK: AUDIT#{ISO timestamp}#{random suffix}
|
|
46
|
+
*/
|
|
47
|
+
async function logAuditEvent(ddb, tableName, tenantId, event) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
const suffix = crypto.randomBytes(6).toString('hex');
|
|
50
|
+
const ttl = Math.floor(Date.now() / 1000) + TTL_90_DAYS_SECONDS;
|
|
51
|
+
await ddb.send(new lib_dynamodb_1.PutCommand({
|
|
52
|
+
TableName: tableName,
|
|
53
|
+
Item: {
|
|
54
|
+
PK: `TENANT#${tenantId}`,
|
|
55
|
+
SK: `AUDIT#${now}#${suffix}`,
|
|
56
|
+
eventType: event.eventType,
|
|
57
|
+
fingerprint: event.fingerprint,
|
|
58
|
+
metadata: event.metadata,
|
|
59
|
+
timestamp: now,
|
|
60
|
+
ttl,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
logger_js_1.logger.info('Audit event logged', {
|
|
64
|
+
tenantId,
|
|
65
|
+
eventType: event.eventType,
|
|
66
|
+
fingerprint: event.fingerprint,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Query all audit log entries for a tenant, sorted by timestamp (ascending).
|
|
71
|
+
*/
|
|
72
|
+
async function handleGetAuditLog(tenantId, ddb, tableName) {
|
|
73
|
+
const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
|
|
74
|
+
TableName: tableName,
|
|
75
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
|
|
76
|
+
ExpressionAttributeValues: {
|
|
77
|
+
':pk': `TENANT#${tenantId}`,
|
|
78
|
+
':prefix': 'AUDIT#',
|
|
79
|
+
},
|
|
80
|
+
ScanIndexForward: true,
|
|
81
|
+
}));
|
|
82
|
+
const events = (result.Items ?? []).map((item) => ({
|
|
83
|
+
eventType: item['eventType'],
|
|
84
|
+
fingerprint: item['fingerprint'],
|
|
85
|
+
metadata: item['metadata'],
|
|
86
|
+
timestamp: item['timestamp'],
|
|
87
|
+
}));
|
|
88
|
+
return {
|
|
89
|
+
statusCode: 200,
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ events }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../../lib/handler/routes/audit.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,sCA8BC;AAKD,8CA6BC;AApGD,+CAAiC;AACjC,wDAI+B;AAC/B,4CAAsC;AAuBtC,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9C;;;;GAIG;AACI,KAAK,UAAU,aAAa,CACjC,GAA2B,EAC3B,SAAiB,EACjB,QAAgB,EAChB,KAAiB;IAEjB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,mBAAmB,CAAC;IAEhE,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,yBAAU,CAAC;QACb,SAAS,EAAE,SAAS;QACpB,IAAI,EAAE;YACJ,EAAE,EAAE,UAAU,QAAQ,EAAE;YACxB,EAAE,EAAE,SAAS,GAAG,IAAI,MAAM,EAAE;YAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,SAAS,EAAE,GAAG;YACd,GAAG;SACJ;KACF,CAAC,CACH,CAAC;IAEF,kBAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE;QAChC,QAAQ;QACR,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,iBAAiB,CACrC,QAAgB,EAChB,GAA2B,EAC3B,SAAiB;IAEjB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAC3B,IAAI,2BAAY,CAAC;QACf,SAAS,EAAE,SAAS;QACpB,sBAAsB,EAAE,uCAAuC;QAC/D,yBAAyB,EAAE;YACzB,KAAK,EAAE,UAAU,QAAQ,EAAE;YAC3B,SAAS,EAAE,QAAQ;SACpB;QACD,gBAAgB,EAAE,IAAI;KACvB,CAAC,CACH,CAAC;IAEF,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjD,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;QAC5B,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC;QAC1B,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;KAC7B,CAAC,CAAC,CAAC;IAEJ,OAAO;QACL,UAAU,EAAE,GAAG;QACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
DynamoDBDocumentClient,
|
|
4
|
+
PutCommand,
|
|
5
|
+
QueryCommand,
|
|
6
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
|
|
9
|
+
interface HandlerResponse {
|
|
10
|
+
statusCode: number;
|
|
11
|
+
body: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type AuditEventType =
|
|
16
|
+
| 'registered'
|
|
17
|
+
| 'rotation-started'
|
|
18
|
+
| 'rotation-confirmed'
|
|
19
|
+
| 'rotation-completed'
|
|
20
|
+
| 'revoked'
|
|
21
|
+
| 'device-linked'
|
|
22
|
+
| 'device-removed';
|
|
23
|
+
|
|
24
|
+
export interface AuditEvent {
|
|
25
|
+
eventType: AuditEventType;
|
|
26
|
+
fingerprint: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TTL_90_DAYS_SECONDS = 90 * 24 * 60 * 60;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write an audit event to DynamoDB.
|
|
34
|
+
*
|
|
35
|
+
* PK: TENANT#{tenantId}, SK: AUDIT#{ISO timestamp}#{random suffix}
|
|
36
|
+
*/
|
|
37
|
+
export async function logAuditEvent(
|
|
38
|
+
ddb: DynamoDBDocumentClient,
|
|
39
|
+
tableName: string,
|
|
40
|
+
tenantId: string,
|
|
41
|
+
event: AuditEvent,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
const suffix = crypto.randomBytes(6).toString('hex');
|
|
45
|
+
const ttl = Math.floor(Date.now() / 1000) + TTL_90_DAYS_SECONDS;
|
|
46
|
+
|
|
47
|
+
await ddb.send(
|
|
48
|
+
new PutCommand({
|
|
49
|
+
TableName: tableName,
|
|
50
|
+
Item: {
|
|
51
|
+
PK: `TENANT#${tenantId}`,
|
|
52
|
+
SK: `AUDIT#${now}#${suffix}`,
|
|
53
|
+
eventType: event.eventType,
|
|
54
|
+
fingerprint: event.fingerprint,
|
|
55
|
+
metadata: event.metadata,
|
|
56
|
+
timestamp: now,
|
|
57
|
+
ttl,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
logger.info('Audit event logged', {
|
|
63
|
+
tenantId,
|
|
64
|
+
eventType: event.eventType,
|
|
65
|
+
fingerprint: event.fingerprint,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Query all audit log entries for a tenant, sorted by timestamp (ascending).
|
|
71
|
+
*/
|
|
72
|
+
export async function handleGetAuditLog(
|
|
73
|
+
tenantId: string,
|
|
74
|
+
ddb: DynamoDBDocumentClient,
|
|
75
|
+
tableName: string,
|
|
76
|
+
): Promise<HandlerResponse> {
|
|
77
|
+
const result = await ddb.send(
|
|
78
|
+
new QueryCommand({
|
|
79
|
+
TableName: tableName,
|
|
80
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
|
|
81
|
+
ExpressionAttributeValues: {
|
|
82
|
+
':pk': `TENANT#${tenantId}`,
|
|
83
|
+
':prefix': 'AUDIT#',
|
|
84
|
+
},
|
|
85
|
+
ScanIndexForward: true,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const events = (result.Items ?? []).map((item) => ({
|
|
90
|
+
eventType: item['eventType'],
|
|
91
|
+
fingerprint: item['fingerprint'],
|
|
92
|
+
metadata: item['metadata'],
|
|
93
|
+
timestamp: item['timestamp'],
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
statusCode: 200,
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ events }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
interface HandlerResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
body: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function handlePutBlob(blobId: string, tenantId: string, rawBody: string | null | undefined, isBase64Encoded: boolean, contentType: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
8
|
+
export declare function handleGetBlob(blobId: string, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
9
|
+
export declare function handleDeleteBlob(blobId: string, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
10
|
+
export declare function handleListBlobs(tenantId: string, since: string | undefined, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
11
|
+
export declare function handleCountBlobs(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=blobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/blobs.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAKvB,MAAM,uBAAuB,CAAC;AAK/B,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAID,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAuG1B;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAkC1B;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAiE1B;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAuE1B;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAmB1B"}
|