Configuring code signing with AWS Lambda functions and layers

Heeki Park
4 min readNov 24, 2021

I’ve been spending a bunch of time lately thinking about governance for developing serverless applications. One way to secure serverless deployments is by signing the function or layer zip artifacts, somewhat akin to basic file verification with checksums or more aptly digital trust signatures with container images. While AWS documentation covers the process, I found I needed to fill in the blanks in a few spots and ran into a few issues. Here I document my journey for setting up code signing for both functions and layers. The full code repository for this blog post can be found here.

Setting up the signing profile

I always deploy resources into my account using AWS SAM, even if I end up deploying regular CloudFormation resources, as I like getting the stack events directly in my terminal session rather than having to check status in the AWS console. As such, I started by writing the template for the signing profile.

Type: AWS::Signer::SigningProfile
Properties:
PlatformId: String
SignatureValidityPeriod:
SignatureValidityPeriod
Tags:
- Tag

The documentation states that PlatformId is a required field but doesn’t specify valid values for that property. I ran the following command to get the list of available PlatformId values: aws signer list-signing-platforms | jq -r '.platforms[].platformId', which at the time of this writing is:

AWSIoTDeviceManagement-SHA256-ECDSA
AWSLambda-SHA384-ECDSA
AmazonFreeRTOS-TI-CC3220SF
AmazonFreeRTOS-Default

With that, I wrote the following resource definition for my signing profile. I pass in AWSLambda-SHA384-ECDSA to the pPlatformId stack parameter.

Signer:
Type: AWS::Signer::SigningProfile
Properties:
PlatformId: !Ref pPlatformId
SignatureValidityPeriod:
Type: DAYS
Value: 31

Setting up the code signing configuration

After typing up the signing profile definition, I moved on to writing the template for the Lambda code signing configuration. I initially wrote my signing configuration as follows. [Spoiler alert!] This is wrong, and I revisit this below.

SigningConfig:
Type: AWS::Lambda::CodeSigningConfig
Properties:
AllowedPublishers:
SigningProfileVersionArns:
- !Ref Signer
CodeSigningPolicies:
UntrustedArtifactOnDeployment: Enforce
Description: testing function and layer signing

I deployed those two resources into my account as a stack and got the ARN of the signing configuration, which I save for passing into a separate stack with my Lambda function later.

Creating an unsigned and signed Lambda layer

Next I created an unsigned and signed layer to test the behavior of attaching each to a signed function. I wrote my layer definition as follows.

LayerBoto3:
Type: AWS::Serverless::LayerVersion
Properties:
CompatibleRuntimes:
- python3.8
- python3.9
ContentUri: ../layer
LayerName: boto3
Description: !Ref pDescription

Nothing special from a template standpoint, but I did trip up on packaging and deploying the layer. The documentation for configuring code signing with AWS SAM states the following:

You can sign your code when packaging or deploying your application. Specify the --signing-profiles option with either the sam package or sam deploy command

The key words in those statements are “either/or”. I tried to use the --signing-profiles option for both the sam package and sam deploy commands. The sam package command succeeded but the sam deploy command failed with the following message:

botocore.exceptions.WaiterError: Waiter SuccessfulSigningJob failed: Waiter encountered a terminal failure state: For expression "status" we matched expected path: "Failed"

Looking at the signing job details in AWS Signer, I saw the status reason as follows:

Signing failed: 5b2137839636e55dbe9fd4f3eab47ce6.zip/signed_f4c90190-dc12–47d7-b40e-eac346413c11.zip File has already been signed.

I updated my process to use --signing-profiles with sam package only.

sam package -t ${LAMBDA_TEMPLATE} --output-template-file ${LAMBDA_OUTPUT} --s3-bucket ${S3BUCKET} --signing-profiles ${SIGNING_PROFILES}sam deploy -t ${LAMBDA_OUTPUT} --stack-name ${LAMBDA_STACK} --parameter-overrides ${LAMBDA_PARAMS} --capabilities CAPABILITY_NAMED_IAM

Note that when deploying layers, the Lambda code signing configuration is not used. The signing profile is used directly on the layer zip artifact with sam package.

Deploying a Lambda function with a signing configuration

With the layers all ready to go, I then wrote my function definition as follows. I define language runtime, memory size, function timeout, and layers using the Globals section. I pass in the output of !Ref SigningConfig above into the pSigningConfigArn stack parameter.

Fn:
Type: AWS::Serverless::Function
Properties:
CodeUri: ../src
Handler: fn.handler
Role: !GetAtt FnRole.Arn
CodeSigningConfigArn: !Ref pSigningConfigArn

I then went to deploy the function without any attached layers and got the following error.

Resource handler returned message: "Lambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept. Check the provided signature for arn:aws:lambda:us-east-1:112233445566:function:lambda-testing-Fn-LrCYPqyemBYX. (Service: Lambda, Status Code: 400, Request ID: de80c4db-13d0-419a-b2b0-8a19e8da63dc, Extended Request ID: null)" (RequestToken: 86f15ac0-859d-ab40-2d58-fecbac9c29aa, HandlerErrorCode: InvalidRequest)

Hmm. I configured the code signing configuration to use the signing profile that I created. My google-fu led me to this issue, which points out that using !Ref Signer returns the ARN of the signing profile without the version. The signing configuration property is named SigningProfileVersionArns, indicating the need for a version ARN. However, the documentation for signing profile doesn’t list any return values to get the version ARN. The aforementioned issue provides the attribute needed to get the value using !GetAtt Signer.ProfileVersionArn. I then updated my signing configuration as follows.

SigningConfig:
Type: AWS::Lambda::CodeSigningConfig
Properties:
AllowedPublishers:
SigningProfileVersionArns:
- !GetAtt Signer.ProfileVersionArn
CodeSigningPolicies:
UntrustedArtifactOnDeployment: Enforce
Description: testing function and layer signing

Success! I was able to deploy a signed function.

Attempting to attach unsigned/signed layers to a signed function

I first tried attaching the unsigned layer, expecting this deployment to fail. Cha-ching! The error message points out the signature for the layer as the problem with this deployment.

Resource handler returned message: "Lambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept. Check the provided signature for arn:aws:lambda:us-east-1:112233445566:layer:boto3:1. (Service: Lambda, Status Code: 400, Request ID: 3fc2323f-c669-4b01-b55f-743efb7550d8, Extended Request ID: null)" (RequestToken: 085537f1-e14d-444a-e4d3-9b5859b39822, HandlerErrorCode: InvalidRequest)

I then tried attaching the signed layer next, hoping it would succeed, which it did. Boom!

Conclusion

I was able to confirm that an unsigned layer would not attach to a signed function and confirm that a signed layer would attach to a signed function. This helps ensure that only trusted zip artifacts are used for performing Lambda function and layer deployments.

--

--

Heeki Park

Principal Solutions Architect @ AWS. Opinions are my own.