Skip to content

Commit c2cc765

Browse files
authoredAug 28, 2024
feat: verifyRequestByKeyId(), verifyRequest(), fetchVerificationKeys() (#8)
BREAKING CHANGE: `verify()` method has been replaced by `verifyRequestByKeyId()` for clarity Before: ```js import { verify } from "@copilot-extensions/preview-sdk"; const payloadIsVerified = await verify(request.body, signature, keyId, { token }); ``` After ```js import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk"; const payloadIsVerified = await verifyRequestByKeyId(request.body, signature, key, { token }); ```
1 parent f2801e5 commit c2cc765

File tree

5 files changed

+260
-56
lines changed

5 files changed

+260
-56
lines changed
 

‎README.md

+66-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,76 @@
66

77
## Usage
88

9-
### `verify(rawBody, signature, keyId, options)`
9+
### Verify a request
10+
11+
```js
12+
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk";
13+
14+
const payloadIsVerified = await verifyRequestByKeyId(
15+
request.body,
16+
signature,
17+
key,
18+
{
19+
token: process.env.GITHUB_TOKEN,
20+
}
21+
);
22+
// true or false
23+
```
24+
25+
## API
26+
27+
### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
28+
29+
Verify the request payload using the provided signature and key ID. The method will request the public key from GitHub's API for the given keyId and then verify the payload.
30+
31+
The `options` argument is optional. It can contain a `token` to authenticate the request to GitHub's API, or a custom `request` instance to use for the request.
32+
33+
```js
34+
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk";
35+
36+
const payloadIsVerified = await verifyRequestByKeyId(
37+
request.body,
38+
signature,
39+
key
40+
);
41+
42+
// with token
43+
await verifyRequestByKeyId(request.body, signature, key, { token: "ghp_1234" });
44+
45+
// with custom octokit request instance
46+
await verifyRequestByKeyId(request.body, signature, key, { request });
47+
```
48+
49+
### `async fetchVerificationKeys(options)`
50+
51+
Fetches public keys for verifying copilot extension requests [from GitHub's API](https://api.github.com/meta/public_keys/copilot_api)
52+
and returns them as an array. The request can be made without authentication, with a token, or with a custom [octokit request](https://github.com/octokit/request.js) instance.
53+
54+
```js
55+
import { fetchVerificationKeys } from "@copilot-extensions/preview-sdk";
56+
57+
// fetch without authentication
58+
const [current] = await fetchVerificationKeys();
59+
60+
// with token
61+
const [current] = await fetchVerificationKeys({ token: "ghp_1234" });
62+
63+
// with custom octokit request instance
64+
const [current] = await fetchVerificationKeys({ request });)
65+
```
66+
67+
### `async verifyRequestPayload(rawBody, signature, keyId)`
68+
69+
Verify the request payload using the provided signature and key. Note that the raw body as received by GitHub must be passed, before any parsing.
1070

1171
```js
1272
import { verify } from "@copilot-extensions/preview-sdk";
1373

14-
const payloadIsVerified = await verify(request.body, signature, keyId, {
15-
token,
16-
});
74+
const payloadIsVerified = await verifyRequestPayload(
75+
request.body,
76+
signature,
77+
key
78+
);
1779
// true or false
1880
```
1981

‎index.d.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,27 @@ type RequestOptions = {
55
request?: RequestInterface;
66
token?: string;
77
};
8+
export type VerificationPublicKey = {
9+
key_identifier: string;
10+
key: string;
11+
is_current: boolean;
12+
};
13+
14+
interface VerifyRequestInterface {
15+
(
16+
rawBody: string,
17+
signature: string,
18+
key: string
19+
): Promise<boolean>;
20+
}
21+
22+
interface FetchVerificationKeysInterface {
23+
(
24+
requestOptions?: RequestOptions,
25+
): Promise<VerificationPublicKey[]>;
26+
}
827

9-
interface VerifyInterface {
28+
interface VerifyRequestByKeyIdInterface {
1029
(
1130
rawBody: string,
1231
signature: string,
@@ -15,4 +34,6 @@ interface VerifyInterface {
1534
): Promise<boolean>;
1635
}
1736

18-
export declare const verify: VerifyInterface;
37+
export declare const verifyRequest: VerifyRequestInterface;
38+
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
39+
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;

‎index.js

+47-25
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,70 @@ import { createVerify } from "node:crypto";
55
import { request as defaultRequest } from "@octokit/request";
66
import { RequestError } from "@octokit/request-error";
77

8-
/** @type {import('.').VerifyInterface} */
9-
export async function verify(
10-
rawBody,
11-
signature,
12-
keyId,
13-
{ token = "", request = defaultRequest } = { request: defaultRequest },
14-
) {
8+
/** @type {import('.').VerifyRequestByKeyIdInterface} */
9+
export async function verifyRequest(rawBody, signature, key) {
1510
// verify arguments
1611
assertValidString(rawBody, "Invalid payload");
1712
assertValidString(signature, "Invalid signature");
18-
assertValidString(keyId, "Invalid keyId");
13+
assertValidString(key, "Invalid key");
1914

20-
// receive valid public keys from GitHub
21-
const requestOptions = request.endpoint("GET /meta/public_keys/copilot_api", {
15+
const verify = createVerify("SHA256").update(rawBody);
16+
17+
// verify signature
18+
try {
19+
return verify.verify(key, signature, "base64");
20+
} catch {
21+
return false;
22+
}
23+
}
24+
25+
/** @type {import('.').FetchVerificationKeysInterface} */
26+
export async function fetchVerificationKeys(
27+
{ token = "", request = defaultRequest } = { request: defaultRequest }
28+
) {
29+
const { data } = await request("GET /meta/public_keys/copilot_api", {
2230
headers: token
2331
? {
2432
Authorization: `token ${token}`,
2533
}
2634
: {},
2735
});
28-
const response = await request(requestOptions);
29-
const { data: keys } = response;
36+
37+
return data.public_keys;
38+
}
39+
40+
/** @type {import('.').VerifyRequestByKeyIdInterface} */
41+
export async function verifyRequestByKeyId(
42+
rawBody,
43+
signature,
44+
keyId,
45+
requestOptions
46+
) {
47+
// verify arguments
48+
assertValidString(rawBody, "Invalid payload");
49+
assertValidString(signature, "Invalid signature");
50+
assertValidString(keyId, "Invalid keyId");
51+
52+
// receive valid public keys from GitHub
53+
const keys = await fetchVerificationKeys(requestOptions);
3054

3155
// verify provided key Id
32-
const publicKey = keys.public_keys.find(
33-
(key) => key.key_identifier === keyId,
34-
);
56+
const publicKey = keys.find((key) => key.key_identifier === keyId);
57+
3558
if (!publicKey) {
36-
throw new RequestError(
37-
"[@copilot-extensions/preview-sdk] No public key found matching key identifier",
38-
404,
59+
const keyNotFoundError = Object.assign(
60+
new Error(
61+
"[@copilot-extensions/preview-sdk] No public key found matching key identifier"
62+
),
3963
{
40-
request: requestOptions,
41-
response,
42-
},
64+
keyId,
65+
keys,
66+
}
4367
);
68+
throw keyNotFoundError;
4469
}
4570

46-
const verify = createVerify("SHA256").update(rawBody);
47-
48-
// verify signature
49-
return verify.verify(publicKey.key, signature, "base64");
71+
return verifyRequest(rawBody, signature, publicKey.key);
5072
}
5173

5274
function assertValidString(value, message) {

‎index.test-d.ts

+43-9
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,66 @@
11
import { expectType } from "tsd";
22
import { request } from "@octokit/request";
33

4-
import { verify } from "./index.js";
4+
import {
5+
fetchVerificationKeys,
6+
verifyRequest,
7+
verifyRequestByKeyId,
8+
type VerificationPublicKey,
9+
} from "./index.js";
510

611
const rawBody = "";
712
const signature = "";
813
const keyId = "";
14+
const key = ""
915
const token = "";
1016

11-
export async function verifyTest() {
12-
const result = await verify(rawBody, signature, keyId);
17+
export async function verifyRequestByKeyIdTest() {
18+
const result = await verifyRequestByKeyId(rawBody, signature, keyId);
1319
expectType<boolean>(result);
1420

1521
// @ts-expect-error - first 3 arguments are required
16-
verify(rawBody, signature);
22+
verifyRequestByKeyId(rawBody, signature);
1723

1824
// @ts-expect-error - rawBody must be a string
19-
await verify(1, signature, keyId);
25+
await verifyRequestByKeyId(1, signature, keyId);
2026

2127
// @ts-expect-error - signature must be a string
22-
await verify(rawBody, 1, keyId);
28+
await verifyRequestByKeyId(rawBody, 1, keyId);
2329

2430
// @ts-expect-error - keyId must be a string
25-
await verify(rawBody, signature, 1);
31+
await verifyRequestByKeyId(rawBody, signature, 1);
2632

2733
// accepts a token argument
28-
await verify(rawBody, signature, keyId, { token });
34+
await verifyRequestByKeyId(rawBody, signature, keyId, { token });
2935

3036
// accepts a request argument
31-
await verify(rawBody, signature, keyId, { request });
37+
await verifyRequestByKeyId(rawBody, signature, keyId, { request });
3238
}
39+
40+
export async function verifyRequestTest() {
41+
const result = await verifyRequest(rawBody, signature, key);
42+
expectType<boolean>(result);
43+
44+
// @ts-expect-error - first 3 arguments are required
45+
verifyRequest(rawBody, signature);
46+
47+
// @ts-expect-error - rawBody must be a string
48+
await verifyRequest(1, signature, key);
49+
50+
// @ts-expect-error - signature must be a string
51+
await verifyRequest(rawBody, 1, key);
52+
53+
// @ts-expect-error - key must be a string
54+
await verifyRequest(rawBody, signature, 1);
55+
}
56+
57+
export async function fetchVerificationKeysTest() {
58+
const result = await fetchVerificationKeys();
59+
expectType<VerificationPublicKey[]>(result);
60+
61+
// accepts a token argument
62+
await fetchVerificationKeys({ token });
63+
64+
// accepts a request argument
65+
await fetchVerificationKeys({ request });
66+
}

0 commit comments

Comments
 (0)
Failed to load comments.