Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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