Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sign stage for NuGet code signing with AWS Signer #1466

Merged
merged 15 commits into from
Oct 6, 2023
17 changes: 17 additions & 0 deletions lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import { AutoBump, AutoMergeBack, AutoBumpProps } from './pull-request';
import { AutoMergeBackPipelineOptions } from './pull-request/merge-back';
import { IRepo, WritableGitHubRepo } from './repo';
import { Shellable, ShellableProps } from './shellable';
import * as signing from './signing';
import { determineRunOrder, flatMap } from './util';

const PUBLISH_STAGE_NAME = 'Publish';
const SIGINING_STAGE_NAME = 'Sign';
const TEST_STAGE_NAME = 'Test';
const METRIC_NAMESPACE = 'CDK/Delivlib';
const FAILURE_METRIC_NAME = 'Failures';
Expand Down Expand Up @@ -357,6 +359,21 @@ export class Pipeline extends Construct {
});
}

public addSigning(signer: signing.ISigner, options: signing.AddSigningOptions = {}) {
const signingStageName = options.stageName ?? SIGINING_STAGE_NAME;
const stage = this.getOrCreateStage(signingStageName);
signer.addToPipeline(stage, `${signer.node.id}Sign`, {
inputArtifact: options.inputArtifact || this.buildOutput,
runOrder: this.determineRunOrderForNewAction(stage),
});
}

public signNuGetWithSigner(options: signing.SignNuGetWithSignerProps & signing.AddSigningOptions) {
this.addSigning(new signing.SignNuGetWithSigner(this, 'NuGetSigning', {
...options,
})), options;
colifran marked this conversation as resolved.
Show resolved Hide resolved
}

public publishToNpm(options: publishing.PublishToNpmProjectProps & AddPublishOptions) {
this.addPublish(new publishing.PublishToNpmProject(this, 'Npm', {
dryRun: this.dryRun,
Expand Down
90 changes: 90 additions & 0 deletions lib/signing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as path from 'path';
import { IBuildImage, LinuxBuildImage, Project } from 'aws-cdk-lib/aws-codebuild';
import { Artifact, IStage } from 'aws-cdk-lib/aws-codepipeline';
import { CodeBuildAction } from 'aws-cdk-lib/aws-codepipeline-actions';
import { IRole } from 'aws-cdk-lib/aws-iam';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { Construct, IConstruct } from 'constructs';
import { AddToPipelineOptions } from './pipeline';
import { LinuxPlatform, Shellable } from './shellable';

export interface ISigner extends IConstruct {
addToPipeline(stage: IStage, id: string, options: AddToPipelineOptions): void;
}

export interface AddSigningOptions {
/**
* The input artifact to use
*
* @default Build output artifact
*/
readonly inputArtifact?: Artifact;

/**
* Stage name to add signing job to
*
* @default "Sign"
*/
readonly stageName?: string;
}

export interface SignNuGetWithSignerProps {
/**
* An S3 bucket used to store signed and unsigned DLL files
*/
readonly signingBucket: IBucket;

/**
* A Lambda function used to perform signing operations with AWS Signer
*/
readonly signingLambda: IFunction;

/**
* A role used provide access to the signing bucket and signing lambda
*/
readonly signingAccessRole: IRole;

/**
* The build image to do the signing in
*
* Needs to have NuGet preinstalled.
*
* @default Latest superchain
*/
readonly buildImage?: IBuildImage;
}

export class SignNuGetWithSigner extends Construct implements ISigner {
public readonly role: IRole;
public readonly project: Project;

public constructor(scope: Construct, id: string, props: SignNuGetWithSignerProps) {
super(scope, id);

const environment = {
SIGNING_BUCKET_NAME: props.signingBucket.bucketName,
SIGNING_LAMBDA_ARN: props.signingLambda.functionArn,
SIGNING_ACCESS_ROLE_ARN: props.signingAccessRole.roleArn,
};

const shellable = new Shellable(this, 'Default', {
platform: new LinuxPlatform(props.buildImage ?? LinuxBuildImage.fromDockerRegistry('public.ecr.aws/jsii/superchain:1-buster-slim-node18')),
scriptDirectory: path.join(__dirname, 'signing', 'nuget'),
entrypoint: 'sign.sh',
environment,
});

this.role = shellable.role;
this.project = shellable.project;
}

public addToPipeline(stage: IStage, id: string, options: AddToPipelineOptions) {
stage.addAction(new CodeBuildAction({
colifran marked this conversation as resolved.
Show resolved Hide resolved
actionName: id,
input: options.inputArtifact || new Artifact(),
runOrder: options.runOrder,
project: this.project,
}));
}
}
64 changes: 64 additions & 0 deletions lib/signing/nuget/sign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
set -euo pipefail

echo "Installing required CLI tools: jq"
if command -v yum &>/dev/null; then
yum install -y jq
elif command -v apt-get &>/dev/null; then
apt-get update
apt-get install -y jq
else
echo "!!! Neither an apt nor yum distribution - could not install jq, things might break!"
fi

if [ -n "${SIGNING_ACCESS_ROLE_ARN:-}" ]; then
ROLE=$(aws sts assume-role --role-arn "${SIGNING_ACCESS_ROLE_ARN:-}" --role-session-name "signer_access")
export AWS_ACCESS_KEY_ID=$(echo $ROLE | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $ROLE | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $ROLE | jq -r .Credentials.SessionToken)
fi

found=false
for nuget_package_path in $(find dotnet -name *.nupkg -not -iname *.symbols.nupkg); do
found=true
echo "🔑 Applying authenticode signatures to assemblies in ${nuget_package_path}"
for file in $(unzip -Z1 ${nuget_package_path} '*.dll'); do
echo "📄 Assembly: ${file}"
tmp=$(mktemp -d)
# extract the dll from the zip file
unzip -q ${nuget_package_path} -d ${tmp} ${file}
# need to set appropriate permissions, otherwise the file has none
chmod u+rw ${tmp}/${file}
# upload dll to signer bucket
version_id=$(aws s3api put-object \
--bucket ${SIGNING_BUCKET_NAME:-} \
--key unsigned/${file} \
--body ${file} | jq -r '.VersionId')
# invoke signer lambda
aws lambda invoke \
--function-name ${SIGNING_LAMBDA_ARN:-} \
--invocation-type RequestResponse \
--cli-binary-format raw-in-base64-out \
--payload '{ "artifactKey": "'"unsigned/${file}"'", "artifactVersion": "'"${version_id}"'" }' \
${tmp}/response.json >/dev/null
signed_artifact_key=$(cat ${tmp}/response.json | jq -r '.signedArtifactKey')
# download signed dll from signer bucket
aws s3api get-object \
--bucket ${SIGNING_BUCKET_NAME:-} \
--key ${signed_artifact_key} \
${tmp}/${file} >/dev/null
# replace the dll in the nuget package
(
cd ${tmp}
zip -qfr ${nuget_package_path} ${file}
)
# clean up temporary directory
rm -rf ${tmp}
done
echo "🔐 All Done!"
done

if ! ${found}; then
echo "❌ No nupkg files found under the dotnet/ directory. Nothing to sign"
exit 1
fi