Skip to content

Commit

Permalink
Merge pull request #151 from whilefoo/action-signature
Browse files Browse the repository at this point in the history
  • Loading branch information
0x4007 authored Oct 19, 2024
2 parents be749c8 + a682ce0 commit 7db4ed9
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 39 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion src/github/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function handleEvent(event: EmitterWebhookEvent, eventHandler: InstanceTyp
repository: plugin.repo,
workflowId: plugin.workflowId,
ref: plugin.ref,
inputs: inputs.getWorkflowInputs(),
inputs: await inputs.getWorkflowInputs(),
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/github/handlers/repository-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function repositoryDispatch(context: GitHubContext<"repository_disp
repository: nextPlugin.plugin.repo,
ref: nextPlugin.plugin.ref,
workflowId: nextPlugin.plugin.workflowId,
inputs: inputs.getWorkflowInputs(),
inputs: await inputs.getWorkflowInputs(),
});
} else {
await dispatchWorker(nextPlugin.plugin, await inputs.getWorkerInputs());
Expand Down
9 changes: 7 additions & 2 deletions src/github/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,20 @@ export class PluginInput<T extends EmitterWebhookEventName = EmitterWebhookEvent
this.ref = ref;
}

public getWorkflowInputs() {
return {
public async getWorkflowInputs() {
const inputs = {
stateId: this.stateId,
eventName: this.eventName,
eventPayload: JSON.stringify(this.eventPayload),
settings: JSON.stringify(this.settings),
authToken: this.authToken,
ref: this.ref,
};
const signature = await this.eventHandler.signPayload(JSON.stringify(inputs));
return {
...inputs,
signature,
};
}

public async getWorkerInputs() {
Expand Down
17 changes: 15 additions & 2 deletions src/sdk/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { config } from "dotenv";
import { Context } from "./context";
import { customOctokit } from "./octokit";
import { sanitizeMetadata } from "./util";
import { verifySignature } from "./signature";
import { KERNEL_PUBLIC_KEY } from "./constants";

config();

Expand All @@ -16,6 +18,7 @@ interface Options {
postCommentOnError?: boolean;
settingsSchema?: TAnySchema;
envSchema?: TAnySchema;
kernelPublicKey?: string;
}

const inputSchema = T.Object({
Expand All @@ -25,6 +28,7 @@ const inputSchema = T.Object({
authToken: T.String(),
settings: T.String(),
ref: T.String(),
signature: T.String(),
});

export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName>(
Expand All @@ -36,20 +40,29 @@ export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TSu
postCommentOnError: options?.postCommentOnError || true,
settingsSchema: options?.settingsSchema,
envSchema: options?.envSchema,
kernelPublicKey: options?.kernelPublicKey || KERNEL_PUBLIC_KEY,
};

const githubInputs = { ...github.context.payload.inputs };
const signature = githubInputs.signature;
delete githubInputs.signature;
if (!(await verifySignature(pluginOptions.kernelPublicKey, githubInputs, signature))) {
core.setFailed(`Error: Invalid signature`);
return;
}

const inputs = Value.Decode(inputSchema, github.context.payload.inputs);

let config: TConfig;
if (pluginOptions.settingsSchema) {
config = Value.Decode(pluginOptions.settingsSchema, JSON.parse(inputs.settings));
config = Value.Decode(pluginOptions.settingsSchema, Value.Default(pluginOptions.settingsSchema, JSON.parse(inputs.settings)));
} else {
config = JSON.parse(inputs.settings) as TConfig;
}

let env: TEnv;
if (pluginOptions.envSchema) {
env = Value.Decode(pluginOptions.envSchema, process.env);
env = Value.Decode(pluginOptions.envSchema, Value.Default(pluginOptions.envSchema, process.env));
} else {
env = process.env as TEnv;
}
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ export async function createPlugin<TConfig = unknown, TEnv = unknown, TSupported

let config: TConfig;
if (pluginOptions.settingsSchema) {
config = Value.Decode(pluginOptions.settingsSchema, payload.settings);
config = Value.Decode(pluginOptions.settingsSchema, Value.Default(pluginOptions.settingsSchema, payload.settings));
} else {
config = payload.settings as TConfig;
}

let env: TEnv;
if (pluginOptions.envSchema) {
env = Value.Decode(pluginOptions.envSchema, ctx.env);
env = Value.Decode(pluginOptions.envSchema, Value.Default(pluginOptions.envSchema, ctx.env));
} else {
env = ctx.env as TEnv;
}
Expand Down
35 changes: 20 additions & 15 deletions src/sdk/signature.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
export async function verifySignature(publicKeyPem: string, payload: unknown, signature: string) {
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
try {
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").trim();
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));

const publicKey = await crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["verify"]
);
const publicKey = await crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["verify"]
);

const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
const dataArray = new TextEncoder().encode(JSON.stringify(payload));
const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
const dataArray = new TextEncoder().encode(JSON.stringify(payload));

return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray);
return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray);
} catch (error) {
console.error(error);
return false;
}
}
90 changes: 74 additions & 16 deletions tests/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,24 +181,34 @@ describe("SDK worker tests", () => {
});

describe("SDK actions tests", () => {
const repo = {
owner: "ubiquity",
repo: "ubiquity-os-kernel",
};

it("Should accept correct request", async () => {
const inputs = {
stateId: "stateId",
eventName: issueCommented.eventName,
settings: "{}",
eventPayload: JSON.stringify(issueCommented.eventPayload),
authToken: "test",
ref: "",
};
const sign = crypto.createSign("SHA256");
sign.update(JSON.stringify(inputs)).end();
const signature = sign.sign(privateKey, "base64");

jest.mock("@actions/github", () => ({
context: {
runId: "1",
payload: {
inputs: {
stateId: "stateId",
eventName: issueCommented.eventName,
settings: "{}",
eventPayload: JSON.stringify(issueCommented.eventPayload),
authToken: "test",
ref: "",
...inputs,
signature,
},
},
repo: {
owner: "ubiquity",
repo: "ubiquity-os-kernel",
},
repo: repo,
},
}));
const setOutput = jest.fn();
Expand All @@ -223,13 +233,18 @@ describe("SDK actions tests", () => {
}));
const { createActionsPlugin } = await import("../src/sdk/actions");

await createActionsPlugin(async (context: Context) => {
return {
event: context.eventName,
};
});
expect(setOutput).toHaveBeenCalledWith("result", { event: issueCommented.eventName });
await createActionsPlugin(
async (context: Context) => {
return {
event: context.eventName,
};
},
{
kernelPublicKey: publicKey,
}
);
expect(setFailed).not.toHaveBeenCalled();
expect(setOutput).toHaveBeenCalledWith("result", { event: issueCommented.eventName });
expect(createDispatchEvent).toHaveBeenCalledWith({
event_type: "return-data-to-ubiquity-os-kernel",
owner: "ubiquity",
Expand All @@ -240,4 +255,47 @@ describe("SDK actions tests", () => {
},
});
});
it("Should deny invalid signature", async () => {
const inputs = {
stateId: "stateId",
eventName: issueCommented.eventName,
settings: "{}",
eventPayload: JSON.stringify(issueCommented.eventPayload),
authToken: "test",
ref: "",
};

jest.mock("@actions/github", () => ({
context: {
runId: "1",
payload: {
inputs: {
...inputs,
signature: "invalid",
},
},
repo: repo,
},
}));
const setOutput = jest.fn();
const setFailed = jest.fn();
jest.mock("@actions/core", () => ({
setOutput,
setFailed,
}));
const { createActionsPlugin } = await import("../src/sdk/actions");

await createActionsPlugin(
async (context: Context) => {
return {
event: context.eventName,
};
},
{
kernelPublicKey: publicKey,
}
);
expect(setFailed).toHaveBeenCalledWith("Error: Invalid signature");
expect(setOutput).not.toHaveBeenCalled();
});
});

0 comments on commit 7db4ed9

Please sign in to comment.