Skip to content

Commit

Permalink
Add support for signing custom events
Browse files Browse the repository at this point in the history
  • Loading branch information
cdupuis committed May 10, 2021
1 parent 752fb32 commit 9174e0b
Show file tree
Hide file tree
Showing 18 changed files with 16,127 additions and 958 deletions.
45 changes: 37 additions & 8 deletions lib/api/machine/SigningKeys.ts
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/

export interface GoalVerificationKey<T> {
import { SdmGoalEvent } from "../goal/SdmGoalEvent";
import { SdmGoalMessage } from "../goal/SdmGoalMessage";

export interface VerificationKey<T> {
name: string;
publicKey: T;
algorithm?: string;
Expand All @@ -23,7 +26,7 @@ export interface GoalVerificationKey<T> {
/**
* Private/public key pair to use for SDM goal signing and verification
*/
export interface GoalSigningKey<T> extends GoalVerificationKey<T> {
export interface SigningKey<T> extends VerificationKey<T> {
privateKey: T;
passphrase?: string;
}
Expand Down Expand Up @@ -55,14 +58,14 @@ export interface GoalSigningAlgorithm<T> {
name: string;

/**
* Sign the provided normalized goal with the given key
* Sign the provided goal with the given key
*/
sign(goal: string, key: GoalSigningKey<T>): string;
sign(goal: SdmGoalMessage, key: SigningKey<T>): Promise<string>;

/**
* Verify the provided normalized goal against the signature
* Verify the provided goal against the signature
*/
verify(goal: string, signature: string, key: GoalVerificationKey<T>): boolean;
verify(goal: SdmGoalEvent, signature: string, key: VerificationKey<T>): Promise<SdmGoalEvent>;
}

export interface GoalSigningConfiguration {
Expand All @@ -84,12 +87,12 @@ export interface GoalSigningConfiguration {
* Public/Private key pair to use for goal signing.
* The public key will also be used to verify incoming goals.
*/
signingKey?: GoalSigningKey<any>;
signingKey?: SigningKey<any>;

/**
* Public keys to verify incoming goals
*/
verificationKeys?: GoalVerificationKey<any> | Array<GoalVerificationKey<any>>;
verificationKeys?: VerificationKey<any> | Array<VerificationKey<any>>;

/**
* Algorithms to use for signing and verification
Expand All @@ -98,3 +101,29 @@ export interface GoalSigningConfiguration {
*/
algorithms?: GoalSigningAlgorithm<any> | Array<GoalSigningAlgorithm<any>>;
}

export interface EventSigningConfiguration {

/**
* Enable event signature verification on this SDM.
*/
enabled: boolean;

/**
* Regular expressions matching subscription and mutation names
* to identify events that should be verified.
*/
events: string[];

/**
* Public/Private key pair to use for event signing.
* The public key will also be used to verify incoming events.
*/
signingKey?: SigningKey<any>;

/**
* Public keys to verify incoming events
*/
verificationKeys?: VerificationKey<any> | Array<VerificationKey<any>>;

}
7 changes: 6 additions & 1 deletion lib/api/machine/SoftwareDeliveryMachineOptions.ts
Expand Up @@ -29,7 +29,10 @@ import { EnrichGoal } from "../goal/enrichGoal";
import { GoalScheduler } from "../goal/support/GoalScheduler";
import { TagGoalSet } from "../goal/tagGoalSet";
import { RepoTargets } from "./RepoTargets";
import { GoalSigningConfiguration } from "./SigningKeys";
import {
EventSigningConfiguration,
GoalSigningConfiguration,
} from "./SigningKeys";

/**
* Infrastructure options common to all SoftwareDeliveryMachines.
Expand Down Expand Up @@ -110,6 +113,8 @@ export interface SoftwareDeliveryMachineOptions {
* by this SDM.
*/
goalSigning?: GoalSigningConfiguration;

eventSigning?: EventSigningConfiguration;
}

/**
Expand Down
Expand Up @@ -72,14 +72,14 @@ export class FulfillGoalOnRequested implements HandleEvent<OnAnyRequestedSdmGoal
event: EventFired<OnAnyRequestedSdmGoal.Subscription>,
ctx: HandlerContext,
): Promise<HandlerResult> {
const sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;
let sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;

if (!shouldFulfill(sdmGoal)) {
logger.debug(`Goal ${sdmGoal.uniqueName} skipped because not fulfilled by this SDM`);
return Success;
}

await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, ctx);
sdmGoal = await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, ctx);

if ((await cancelableGoal(sdmGoal, this.configuration)) && (await isGoalCanceled(sdmGoal, ctx))) {
logger.debug(`Goal ${sdmGoal.uniqueName} has been canceled. Not fulfilling`);
Expand Down
Expand Up @@ -78,14 +78,14 @@ export class RequestDownstreamGoalsOnGoalSuccess implements HandleEvent<OnAnySuc

public async handle(event: EventFired<OnAnySuccessfulSdmGoal.Subscription>,
context: HandlerContext): Promise<HandlerResult> {
const sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;
let sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;

if (!shouldHandle(sdmGoal)) {
logger.debug(`Goal ${sdmGoal.uniqueName} skipped because not managed by this SDM`);
return Success;
}

await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);
sdmGoal = await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);

const id = this.repoRefResolver.repoRefFromPush(sdmGoal.push);
const credentials = await resolveCredentialsPromise(this.credentialsResolver.eventHandlerCredentials(context, id));
Expand Down
Expand Up @@ -69,14 +69,14 @@ export class RespondOnGoalCompletion implements HandleEvent<OnAnyCompletedSdmGoa

public async handle(event: EventFired<OnAnyCompletedSdmGoal.Subscription>,
context: HandlerContext): Promise<HandlerResult> {
const sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;
let sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;

if (!shouldHandle(sdmGoal)) {
logger.debug(`Goal ${sdmGoal.uniqueName} skipped because not managed by this SDM`);
return Success;
}

await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);
sdmGoal = await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);

const id = this.repoRefResolver.repoRefFromPush(sdmGoal.push);

Expand Down
Expand Up @@ -58,14 +58,14 @@ export class SkipDownstreamGoalsOnGoalFailure implements HandleEvent<OnAnyFailed

public async handle(event: EventFired<OnAnyFailedSdmGoal.Subscription>,
context: HandlerContext): Promise<HandlerResult> {
const failedGoal = event.data.SdmGoal[0] as SdmGoalEvent;
let failedGoal = event.data.SdmGoal[0] as SdmGoalEvent;

if (!shouldHandle(failedGoal)) {
logger.debug(`Goal ${failedGoal.uniqueName} skipped because not managed by this SDM`);
return Success;
}

await verifyGoal(failedGoal, this.configuration.sdm.goalSigning, context);
failedGoal = await verifyGoal(failedGoal, this.configuration.sdm.goalSigning, context);

const goals = fetchGoalsFromPush(failedGoal);

Expand Down
Expand Up @@ -89,14 +89,14 @@ export class VoteOnGoalApprovalRequest implements HandleEvent<OnAnyApprovedSdmGo

public async handle(event: EventFired<OnAnyApprovedSdmGoal.Subscription>,
context: HandlerContext): Promise<HandlerResult> {
const sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;
let sdmGoal = event.data.SdmGoal[0] as SdmGoalEvent;

if (!shouldHandle(sdmGoal)) {
logger.debug(`Goal ${sdmGoal.name} skipped because not managed by this SDM`);
return Success;
}

await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);
sdmGoal =await verifyGoal(sdmGoal, this.configuration.sdm.goalSigning, context);

const id = this.repoRefResolver.repoRefFromPush(sdmGoal.push);
const credentials = await resolveCredentialsPromise(this.credentialsFactory.eventHandlerCredentials(context, id));
Expand Down
22 changes: 21 additions & 1 deletion lib/core/machine/configureSdm.ts
Expand Up @@ -33,6 +33,10 @@ import {
GoalExecutionRequestProcessor,
} from "../handlers/events/delivery/goals/goalExecution";
import { CacheCleanupAutomationEventListener } from "../handlers/events/delivery/goals/k8s/CacheCleanupAutomationEventListener";
import {
EventSigningAutomationEventListener,
wrapEventHandlersToVerifySignature,
} from "../signing/eventSigning";
import { GoalSigningAutomationEventListener } from "../signing/goalSigning";
import { toArray } from "../util/misc/array";
import { SdmGoalMetricReportingAutomationEventListener } from "../util/SdmGoalMetricReportingAutomationEventListener";
Expand Down Expand Up @@ -93,7 +97,8 @@ export function configureSdm(machineMaker: SoftwareDeliveryMachineMaker,
// Configure the job forking ability
await configureJobLaunching(mergedConfig, sdm);
configureGoalSigning(mergedConfig);

configureEventSigning(mergedConfig);

await registerMetadata(mergedConfig, sdm);

// Register startup message detail
Expand Down Expand Up @@ -189,6 +194,21 @@ function configureGoalSigning(mergedConfig: SoftwareDeliveryMachineConfiguration
}
}

/**
* Configure SDM to sign and verify events
* @param mergedConfig
*/
function configureEventSigning(mergedConfig: SoftwareDeliveryMachineConfiguration): void {
if (mergedConfig.sdm?.eventSigning?.enabled === true) {
_.update(mergedConfig, "graphql.listeners",
old => !!old ? old : []);
mergedConfig.graphql.listeners.push(
new EventSigningAutomationEventListener(mergedConfig.sdm.eventSigning));
mergedConfig.events = wrapEventHandlersToVerifySignature(
mergedConfig.events || [], mergedConfig.sdm.eventSigning);
}
}

async function registerMetadata(config: Configuration,
machine: SoftwareDeliveryMachine): Promise<void> {
// tslint:disable-next-line:no-implicit-dependencies
Expand Down
115 changes: 115 additions & 0 deletions lib/core/signing/eventSigning.ts
@@ -0,0 +1,115 @@
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;
}

0 comments on commit 9174e0b

Please sign in to comment.