-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
94 lines (83 loc) · 3.4 KB
/
server.ts
File metadata and controls
94 lines (83 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// Copyright (C) 2025 imput
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import type { CupTicket } from './lib/util.ts';
import { CupParticipant } from './participant.ts';
import * as Bytes from './lib/bytes.ts';
import * as ECDSA from './lib/ecdsa.ts';
/**
* Used for adding the CUP signature to responses.
*/
export class CupServer extends CupParticipant {
/**
* @param keys A series of id -> private key pairs used to
* handle client signature requests.
*/
constructor(keys: Record<number, CryptoKey>) {
for (const key of Object.values(keys)) {
if (key.type !== 'private') {
throw `invalid key type: ${key.type}, must be 'private'`;
}
if (!key.usages.includes('sign')) {
throw `key usages do not include 'sign'`;
}
}
super(keys);
}
/**
* Creates a ticket from the request body, which is used
* for later response signature.
* @param request Request with unconsumed body. Cloned by the
* function, you don't need to clone it yourself.
* @returns An opaque ticket representing the data needed to sign
* the response.
*/
override makeTicket(request: Request): Promise<CupTicket> {
return super.makeTicket(request);
}
/**
* Takes the server response and modifies it so that
* it includes the server-signed CUP response in its
* headers.
* @param response Response to sign.
* @param ticket Ticket made by the makeTicket() call.
* @param [write_etag=false] Whether to write the CUP signature
* also into the ETag header. This is off by default because
* it's a hack, only enable it if you are certain you need it.
* @returns Response containing CUP headers with the
* signature.
*/
async sign(response: Response, ticket: CupTicket, write_etag = false): Promise<Response> {
// We know this key exists, because we validated it
// in makeTicket(), and we know it's valid because
// we validated the contents in the constructor.
const key = this.keys[ticket.keyId]!;
const proofBytes = await crypto.subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
key,
await this.makeProofData(response, ticket),
);
const proof = `${
Bytes.toHex(
ECDSA.fromRs(new Uint8Array(proofBytes)),
)
}:${Bytes.toHex(ticket.hash)}`;
response.headers.set('X-Cup-Server-Proof', proof);
// This part of the protocol is kinda a hack.
// Let's not do this unless we really need to.
if (write_etag) {
response.headers.set('ETag', `W/"${proof}"`);
}
return response;
}
}