Skip to content

Commit

Permalink
feat: verifyRequestByKeyId(), verifyRequest(), `fetchVerification…
Browse files Browse the repository at this point in the history
…Keys()` (#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 });
```
  • Loading branch information
gr2m authored Aug 28, 2024
1 parent f2801e5 commit c2cc765
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 56 deletions.
70 changes: 66 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,76 @@

## Usage

### `verify(rawBody, signature, keyId, options)`
### Verify a request

```js
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk";

const payloadIsVerified = await verifyRequestByKeyId(
request.body,
signature,
key,
{
token: process.env.GITHUB_TOKEN,
}
);
// true or false
```

## API

### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`

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.

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.

```js
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk";

const payloadIsVerified = await verifyRequestByKeyId(
request.body,
signature,
key
);

// with token
await verifyRequestByKeyId(request.body, signature, key, { token: "ghp_1234" });

// with custom octokit request instance
await verifyRequestByKeyId(request.body, signature, key, { request });
```

### `async fetchVerificationKeys(options)`

Fetches public keys for verifying copilot extension requests [from GitHub's API](https://api.github.com/meta/public_keys/copilot_api)
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.

```js
import { fetchVerificationKeys } from "@copilot-extensions/preview-sdk";

// fetch without authentication
const [current] = await fetchVerificationKeys();

// with token
const [current] = await fetchVerificationKeys({ token: "ghp_1234" });

// with custom octokit request instance
const [current] = await fetchVerificationKeys({ request });)
```

### `async verifyRequestPayload(rawBody, signature, keyId)`

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.

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

const payloadIsVerified = await verify(request.body, signature, keyId, {
token,
});
const payloadIsVerified = await verifyRequestPayload(
request.body,
signature,
key
);
// true or false
```

Expand Down
25 changes: 23 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,27 @@ type RequestOptions = {
request?: RequestInterface;
token?: string;
};
export type VerificationPublicKey = {
key_identifier: string;
key: string;
is_current: boolean;
};

interface VerifyRequestInterface {
(
rawBody: string,
signature: string,
key: string
): Promise<boolean>;
}

interface FetchVerificationKeysInterface {
(
requestOptions?: RequestOptions,
): Promise<VerificationPublicKey[]>;
}

interface VerifyInterface {
interface VerifyRequestByKeyIdInterface {
(
rawBody: string,
signature: string,
Expand All @@ -15,4 +34,6 @@ interface VerifyInterface {
): Promise<boolean>;
}

export declare const verify: VerifyInterface;
export declare const verifyRequest: VerifyRequestInterface;
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;
72 changes: 47 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,70 @@ import { createVerify } from "node:crypto";
import { request as defaultRequest } from "@octokit/request";
import { RequestError } from "@octokit/request-error";

/** @type {import('.').VerifyInterface} */
export async function verify(
rawBody,
signature,
keyId,
{ token = "", request = defaultRequest } = { request: defaultRequest },
) {
/** @type {import('.').VerifyRequestByKeyIdInterface} */
export async function verifyRequest(rawBody, signature, key) {
// verify arguments
assertValidString(rawBody, "Invalid payload");
assertValidString(signature, "Invalid signature");
assertValidString(keyId, "Invalid keyId");
assertValidString(key, "Invalid key");

// receive valid public keys from GitHub
const requestOptions = request.endpoint("GET /meta/public_keys/copilot_api", {
const verify = createVerify("SHA256").update(rawBody);

// verify signature
try {
return verify.verify(key, signature, "base64");
} catch {
return false;
}
}

/** @type {import('.').FetchVerificationKeysInterface} */
export async function fetchVerificationKeys(
{ token = "", request = defaultRequest } = { request: defaultRequest }
) {
const { data } = await request("GET /meta/public_keys/copilot_api", {
headers: token
? {
Authorization: `token ${token}`,
}
: {},
});
const response = await request(requestOptions);
const { data: keys } = response;

return data.public_keys;
}

/** @type {import('.').VerifyRequestByKeyIdInterface} */
export async function verifyRequestByKeyId(
rawBody,
signature,
keyId,
requestOptions
) {
// verify arguments
assertValidString(rawBody, "Invalid payload");
assertValidString(signature, "Invalid signature");
assertValidString(keyId, "Invalid keyId");

// receive valid public keys from GitHub
const keys = await fetchVerificationKeys(requestOptions);

// verify provided key Id
const publicKey = keys.public_keys.find(
(key) => key.key_identifier === keyId,
);
const publicKey = keys.find((key) => key.key_identifier === keyId);

if (!publicKey) {
throw new RequestError(
"[@copilot-extensions/preview-sdk] No public key found matching key identifier",
404,
const keyNotFoundError = Object.assign(
new Error(
"[@copilot-extensions/preview-sdk] No public key found matching key identifier"
),
{
request: requestOptions,
response,
},
keyId,
keys,
}
);
throw keyNotFoundError;
}

const verify = createVerify("SHA256").update(rawBody);

// verify signature
return verify.verify(publicKey.key, signature, "base64");
return verifyRequest(rawBody, signature, publicKey.key);
}

function assertValidString(value, message) {
Expand Down
52 changes: 43 additions & 9 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
import { expectType } from "tsd";
import { request } from "@octokit/request";

import { verify } from "./index.js";
import {
fetchVerificationKeys,
verifyRequest,
verifyRequestByKeyId,
type VerificationPublicKey,
} from "./index.js";

const rawBody = "";
const signature = "";
const keyId = "";
const key = ""
const token = "";

export async function verifyTest() {
const result = await verify(rawBody, signature, keyId);
export async function verifyRequestByKeyIdTest() {
const result = await verifyRequestByKeyId(rawBody, signature, keyId);
expectType<boolean>(result);

// @ts-expect-error - first 3 arguments are required
verify(rawBody, signature);
verifyRequestByKeyId(rawBody, signature);

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

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

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

// accepts a token argument
await verify(rawBody, signature, keyId, { token });
await verifyRequestByKeyId(rawBody, signature, keyId, { token });

// accepts a request argument
await verify(rawBody, signature, keyId, { request });
await verifyRequestByKeyId(rawBody, signature, keyId, { request });
}

export async function verifyRequestTest() {
const result = await verifyRequest(rawBody, signature, key);
expectType<boolean>(result);

// @ts-expect-error - first 3 arguments are required
verifyRequest(rawBody, signature);

// @ts-expect-error - rawBody must be a string
await verifyRequest(1, signature, key);

// @ts-expect-error - signature must be a string
await verifyRequest(rawBody, 1, key);

// @ts-expect-error - key must be a string
await verifyRequest(rawBody, signature, 1);
}

export async function fetchVerificationKeysTest() {
const result = await fetchVerificationKeys();
expectType<VerificationPublicKey[]>(result);

// accepts a token argument
await fetchVerificationKeys({ token });

// accepts a request argument
await fetchVerificationKeys({ request });
}
Loading

0 comments on commit c2cc765

Please sign in to comment.