UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

116 lines (104 loc) 4.04 kB
import { GraphClientListener } from "@atomist/automation-client/lib/graph/ApolloGraphClient"; import { HandleEvent } from "@atomist/automation-client/lib/HandleEvent"; import { metadataFromInstance } from "@atomist/automation-client/lib/internal/metadata/metadataReading"; import { EventHandlerMetadata } from "@atomist/automation-client/lib/metadata/automationMetadata"; import { Maker, toFactory, } from "@atomist/automation-client/lib/util/constructionUtils"; import { logger } from "@atomist/automation-client/lib/util/logger"; import { MutationOptions } from "@atomist/automation-client/src/lib/spi/graph/GraphClient"; import * as crypto from "crypto"; import * as _ from "lodash"; import { EventSigningConfiguration } from "../../api/machine/SigningKeys"; import { toArray } from "../util/misc/array"; /** * AutomationEventListener that signs outgoing custom events with a configurable * JWS signature key. */ export class EventSigningAutomationEventListener implements GraphClientListener<any> { constructor(private readonly esc: EventSigningConfiguration) { this.initVerificationKeys(); } public async onMutation(options: MutationOptions<any>): Promise<MutationOptions<any>> { if (eventMatch(options.name, this.esc.events)) { const privateKey = crypto.createPrivateKey({ key: this.esc.signingKey.privateKey, passphrase: this.esc.signingKey.passphrase, }); const { default: CompactSign } = require("jose/jws/compact/sign"); for (const key of Object.getOwnPropertyNames(options.variables || {})) { const value = options.variables[key]; const jws = await new CompactSign(Buffer.from(JSON.stringify(value))) .setProtectedHeader({ alg: "ES512" }) .sign(privateKey); value.signature = jws; logger.debug(`Signed custom event '${options.name}'`); } } return options; } private initVerificationKeys(): void { this.esc.verificationKeys = toArray(this.esc.verificationKeys) || []; // If signing key is set, also use it to verify if (!!this.esc.signingKey) { this.esc.verificationKeys.push(this.esc.signingKey); } } } /** * Wrap every event handler that is registered and its subscription name matches a configurable set of * regular expression patterns for event signature verification. */ export function wrapEventHandlersToVerifySignature(handlers: Array<Maker<HandleEvent<any>>>, options: EventSigningConfiguration): Array<Maker<HandleEvent<any>>> { const wh: Array<Maker<HandleEvent<any>>> = []; for (const handler of handlers) { const instance = toFactory(handler)(); const md = metadataFromInstance(instance) as EventHandlerMetadata; if (eventMatch(md.subscriptionName, options.events)) { wh.push(() => ({ ...md, handle: async (e, ctx, params) => { const { default: compactVerify } = require("jose/jws/compact/verify"); for (const key of Object.getOwnPropertyNames(e.data)) { const evv = e.data[key][0]; if (!evv.signature) { throw new Error("Signature missing on incoming event"); } let verified = false; for (const pkey of toArray(options.verificationKeys)) { const publicKey = crypto.createPublicKey({ key: pkey.publicKey, }); try { const { payload } = await compactVerify(evv.signature, publicKey); e.data[key][0] = _.merge({}, evv, JSON.parse(Buffer.from(payload).toString())); verified = true; logger.debug(`Verified signature on custom event '${md.subscriptionName}'`); break; } catch (e) { // return undefined; } } if (!verified) { throw new Error("Signature verification failed for incoming event"); } } return instance.handle(e, ctx, params); }, })); } else { wh.push(handler); } } return wh; } function eventMatch(event: string, patterns: string[]): boolean { for (const pattern of patterns) { if (new RegExp(pattern).test(event)) { return true; } } return false; }