UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

183 lines (163 loc) 6.45 kB
/* * Copyright © 2020 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { GraphClientListener } from "@atomist/automation-client/lib/graph/ApolloGraphClient"; import { AutomationContextAware, HandlerContext, } from "@atomist/automation-client/lib/HandlerContext"; import { logger } from "@atomist/automation-client/lib/util/logger"; import { MutationOptions } from "@atomist/automation-client/src/lib/spi/graph/GraphClient"; import * as fs from "fs-extra"; import * as path from "path"; import { DeepPartial } from "ts-essentials"; import { updateGoal } from "../../api-helper/goal/storeGoals"; import { SdmGoalEvent } from "../../api/goal/SdmGoalEvent"; import { SdmGoalMessage } from "../../api/goal/SdmGoalMessage"; import { GoalSigningAlgorithm, GoalSigningConfiguration, SigningKey, GoalSigningScope, VerificationKey, } from "../../api/machine/SigningKeys"; import { SdmGoalState } from "../../typings/types"; import { toArray } from "../util/misc/array"; import { JwtGoalSigningAlgorithm } from "./jwtGoalSigning"; import { RsaGoalSigningAlgorithm } from "./rsaGoalSigning"; export interface SignatureMixin { signature: string; } export const DefaultGoalSigningAlgorithm = RsaGoalSigningAlgorithm; /** * AutomationEventListener that verifies incoming SDM goals against a set of configurable * verification public keys. * * Optionally a private key can be specified to sign outgoing goals. Setting this is strongly * recommended to prevent executing untrusted and/or tampered SDM goals. */ export class GoalSigningAutomationEventListener implements GraphClientListener<{ goal: SdmGoalMessage }> { constructor(private readonly gsc: GoalSigningConfiguration) { this.initVerificationKeys(); } public async onMutation(options: MutationOptions<{ goal: SdmGoalMessage }>): Promise<MutationOptions<{ goal: SdmGoalMessage }>> { if (options.name === "UpdateSdmGoal") { const goal = options.variables?.goal; if (!!goal) { options.variables.goal = await signGoal(goal as SdmGoalMessage & SignatureMixin, this.gsc); } } return options; } private initVerificationKeys(): void { this.gsc.verificationKeys = toArray(this.gsc.verificationKeys) || []; // If signing key is set, also use it to verify if (!!this.gsc.signingKey) { this.gsc.verificationKeys.push(this.gsc.signingKey); } // Load the Atomist public key const publicKey = fs.readFileSync(path.join(__dirname, "atomist-public.pem")).toString(); this.gsc.verificationKeys.push({ publicKey, name: "atomist.com/sdm" }); } } /** * Verify a goal signature against the public keys configured in provided Configuration. * If signature can't be verified, the goal will be marked as failed and an Error will be thrown. * @param goal goal to verify * @param gsc signing configuration * @param ctx */ export async function verifyGoal(goal: SdmGoalEvent & DeepPartial<SignatureMixin>, gsc: GoalSigningConfiguration, ctx: HandlerContext): Promise<SdmGoalEvent> { if (!!gsc && gsc.enabled === true && !!goal && isInScope(gsc.scope, ctx) && !isGoalRejected(goal)) { if (!!goal.signature) { let verifiedWith: VerificationKey<any>; let verifiedGoal: SdmGoalEvent; for (const key of toArray(gsc.verificationKeys)) { const algorithm = findAlgorithm(key, gsc); verifiedGoal = await algorithm.verify(goal, goal.signature, key); if (verifiedGoal) { verifiedWith = key; break; } } if (!!verifiedWith) { logger.debug( `Verified signature for incoming goal '${goal.uniqueName}' of '${goal.goalSetId}' with key '${ verifiedWith.name}' and algorithm '${verifiedWith.algorithm || DefaultGoalSigningAlgorithm.name}'`); return verifiedGoal; } else { await rejectGoal("signature invalid", goal, ctx); throw new Error("SDM goal signature invalid. Rejecting goal!"); } } else { await rejectGoal("signature missing", goal, ctx); throw new Error("SDM goal signature is missing. Rejecting goal!"); } } else { return goal; } } /** * Add a signature to a goal * @param goal * @param gsc */ export async function signGoal(goal: SdmGoalMessage, gsc: GoalSigningConfiguration): Promise<SdmGoalMessage & SignatureMixin> { if (!!gsc && gsc.enabled === true && !!gsc.signingKey) { (goal as any).signature = await findAlgorithm(gsc.signingKey, gsc).sign(goal, gsc.signingKey); logger.debug(`Signed goal '${goal.uniqueName}' of '${goal.goalSetId}'`); return goal as any; } else { return goal as any; } } async function rejectGoal(reason: string, sdmGoal: SdmGoalEvent, ctx: HandlerContext): Promise<void> { await updateGoal( ctx, sdmGoal, { state: SdmGoalState.failure, description: `Rejected: ${sdmGoal.name}`, phase: reason, }); } function findAlgorithm(key: VerificationKey<any> | SigningKey<any>, gsc: GoalSigningConfiguration): GoalSigningAlgorithm<any> { const algorithm = [...toArray(gsc.algorithms || []), DefaultGoalSigningAlgorithm, JwtGoalSigningAlgorithm] .find(a => a.name.toLowerCase() === (key.algorithm || DefaultGoalSigningAlgorithm.name).toLowerCase()); if (!algorithm) { throw new Error( `Goal signing or verification key '${key.name}' requested algorithm '${key.algorithm}' which isn't configured`); } return algorithm; } function isInScope(scope: GoalSigningScope, ctx: HandlerContext): boolean { if (scope === GoalSigningScope.All) { return true; } else if (scope === GoalSigningScope.Fulfillment && (ctx as any as AutomationContextAware).context.operation === "FulfillGoalOnRequested") { return true; } else { return false; } } function isGoalRejected(sdmGoal: SdmGoalEvent): boolean { return sdmGoal.state === SdmGoalState.failure && sdmGoal.description === `Rejected: ${sdmGoal.name}`; }