@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
148 lines • 6.01 kB
JavaScript
;
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.signGoal = exports.verifyGoal = exports.GoalSigningAutomationEventListener = exports.DefaultGoalSigningAlgorithm = void 0;
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const fs = require("fs-extra");
const path = require("path");
const storeGoals_1 = require("../../api-helper/goal/storeGoals");
const SigningKeys_1 = require("../../api/machine/SigningKeys");
const types_1 = require("../../typings/types");
const array_1 = require("../util/misc/array");
const jwtGoalSigning_1 = require("./jwtGoalSigning");
const rsaGoalSigning_1 = require("./rsaGoalSigning");
exports.DefaultGoalSigningAlgorithm = rsaGoalSigning_1.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.
*/
class GoalSigningAutomationEventListener {
constructor(gsc) {
this.gsc = gsc;
this.initVerificationKeys();
}
async onMutation(options) {
var _a;
if (options.name === "UpdateSdmGoal") {
const goal = (_a = options.variables) === null || _a === void 0 ? void 0 : _a.goal;
if (!!goal) {
options.variables.goal = await signGoal(goal, this.gsc);
}
}
return options;
}
initVerificationKeys() {
this.gsc.verificationKeys = array_1.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" });
}
}
exports.GoalSigningAutomationEventListener = GoalSigningAutomationEventListener;
/**
* 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
*/
async function verifyGoal(goal, gsc, ctx) {
if (!!gsc && gsc.enabled === true && !!goal && isInScope(gsc.scope, ctx) && !isGoalRejected(goal)) {
if (!!goal.signature) {
let verifiedWith;
let verifiedGoal;
for (const key of array_1.toArray(gsc.verificationKeys)) {
const algorithm = findAlgorithm(key, gsc);
verifiedGoal = await algorithm.verify(goal, goal.signature, key);
if (verifiedGoal) {
verifiedWith = key;
break;
}
}
if (!!verifiedWith) {
logger_1.logger.debug(`Verified signature for incoming goal '${goal.uniqueName}' of '${goal.goalSetId}' with key '${verifiedWith.name}' and algorithm '${verifiedWith.algorithm || exports.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;
}
}
exports.verifyGoal = verifyGoal;
/**
* Add a signature to a goal
* @param goal
* @param gsc
*/
async function signGoal(goal, gsc) {
if (!!gsc && gsc.enabled === true && !!gsc.signingKey) {
goal.signature = await findAlgorithm(gsc.signingKey, gsc).sign(goal, gsc.signingKey);
logger_1.logger.debug(`Signed goal '${goal.uniqueName}' of '${goal.goalSetId}'`);
return goal;
}
else {
return goal;
}
}
exports.signGoal = signGoal;
async function rejectGoal(reason, sdmGoal, ctx) {
await storeGoals_1.updateGoal(ctx, sdmGoal, {
state: types_1.SdmGoalState.failure,
description: `Rejected: ${sdmGoal.name}`,
phase: reason,
});
}
function findAlgorithm(key, gsc) {
const algorithm = [...array_1.toArray(gsc.algorithms || []), exports.DefaultGoalSigningAlgorithm, jwtGoalSigning_1.JwtGoalSigningAlgorithm]
.find(a => a.name.toLowerCase() === (key.algorithm || exports.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, ctx) {
if (scope === SigningKeys_1.GoalSigningScope.All) {
return true;
}
else if (scope === SigningKeys_1.GoalSigningScope.Fulfillment &&
ctx.context.operation === "FulfillGoalOnRequested") {
return true;
}
else {
return false;
}
}
function isGoalRejected(sdmGoal) {
return sdmGoal.state === types_1.SdmGoalState.failure && sdmGoal.description === `Rejected: ${sdmGoal.name}`;
}
//# sourceMappingURL=goalSigning.js.map