Configuring code signing with AWS Lambda functions and layers
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 thesam package
orsam 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.