AWS Signerを使って既存のLambda関数をコード署名してみた

## これは何?
最近ですが[AWS Signer](https://docs.aws.amazon.com/ja_jp/signer/latest/developerguide/Welcome.html)によってLambdaなどにコード署名をくわえることができるるのか検証する機会があったので、今回はその検証した内容についていろいろ記載していきたいと思います。

シナリオとしては、未署名のCloudformationで管理しているLambda(Python3.11)関数を、コード署名するまでの流れを検証してみました。


## 1. 事前準備

コード署名の設定していない関数をコード署名を有効化し、コードの改竄防止を確認しようと思います。
この章では、事前準備としてコード署名設定をしていないLambda関数の作成までをしようと思います。

### フォルダ構成
Cloudformation を用いてデプロイしようと思うため、以下のようフォルダ構成になります。
```
./
├── event.json
├── function/
│   └── main.py
└── template.yml
```



### S3バケットの生成
Pythonコードをパッケージング(cloudformation packageコマンド)及びコード署名格納用のS3バケット（ここでは`test-python-packages`）を作成します。

S3バケットの条件としては**バージョニングの有効**と、**パブリックアクセスを無効化**します。
https://docs.aws.amazon.com/ja_jp/signer/latest/developerguide/s3-source-lambda.html

今回のバケットのプレフィックスは以下の通りになります。

```
./test-python-packages
├── lambda/         ... lambda関数を格納
│   └── test_lambda_function/
│       └── (cloudformation packageで格納されるファイル)
│
└── signer/         ... コード署名したファイル
    └── test_lambda_function/
        └── (コード署名用したファイル)
```


### Lambda 関数の作成
今回はランタイムはPython 3.11のLambdaを作成します。以下のリンクからサンプル関数を作成
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html

```python:function/main.py
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    message = 'Hello {} {}!'.format(event['first_name'], event['last_name'])  
    return { 
        'message' : message
    }

```

Lambda関数の実行引数としてをJSON形式で予め作成しておきます。
```json:event.json
{
    "first_name": "John",
    "last_name": "Smith"
}
```

### Cloudformation テンプレート
次にCloudformation テンプレートを作成していきます。
Lambda関数を作成し、AWS SAMにて [AWS::Serverless::Function](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-function.html) によってLambda関数をデプロイしようと思います。
```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Parameters:
  CodeSigningProfileVersionArn:
    Type: String
    Description: Versioned CodeSigningProfile Arn

Resources:
  function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "test_lambda_function"
      Handler: main.lambda_handler
      Runtime: python3.11
      CodeUri: ./function/.
      Description: Test Lambda Function
      Timeout: 10
      Policies:
        - AWSLambdaBasicExecutionRole
        - AWSLambda_ReadOnlyAccess
```

以下のコマンドを実行して、Cloudformationを実行してLambda関数を作成しようと思います。 `package`コマンドでLambda関数で使用するコードをS3バケットに格納して、`deploy`コマンドにてスタックを作成/更新していきます。
```bash  
# Package 
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function  --output-template-file out.yml

# Deploy
aws cloudformation deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM
```

上記を実行することで、Lamdba関数`test_lambda_function`が正常作成されることを確認したら一旦終わりです。

### 動作確認
以下のコマンドを実行して、LambdaをInvokeします。出力結果がout.jsonが正常に出力され、以下の内容が確認できれば下準備が完了です。
```bash
aws lambda invoke --function-name test_lambda_function  --payload file://event.json out.json

cat out.json
# {"message": "Hello John Smith!"} と出力
```


## 2. AWS Signerの設定

### 署名プロファイルの作成 & 署名ジョブの実行
Cloudformationでも生成することはできますが、今回は手動で作成してみようと思います。
AWS Signerを始めるためには先ず、[署名プロファイル](https://docs.aws.amazon.com/ja_jp/signer/latest/developerguide/signing-profiles.html)を作成する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/907c4488-ccc1-b7ac-8366-a52fd53a94aa.png)


プロファイルを作成しましたら、署名ジョブを実行し、コード署名を実施します。
コードアセットのソース場所にはプレフィックス`lambda/test_lambda_function/`内にある圧縮ファイルを選択します。

署名後の送信先バケットはプレフィックス`signer/test_lambda_function/`を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/9ebcda47-dbfb-da02-b740-2b9da9eaccc9.png)


以下のようにステータスが成功と表示されると署名ジョブは成功します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/c32fe481-aae3-7317-7d95-bf51c9476a2a.png)

これでCode署名を実施することができました。


## 3. コード署名設定
CloudFormation でLambdaコード署名を実施するには[コード署名設定](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-codesigning.html?icmpid=docs_lambda_help)を設定した後、Lambda関数上でコード署名設定を関連づける必要があります。
コード署名設定はリソース[AWS::Lambda::CodeSigningConfig](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-codesigningconfig.html)で作成することができ、サンプルコードは以下のようになります。

ここで`CodeSigningProfileVersionArn`は2章で作成したAWS Signerの署名プロファイルの「バージョン管理されたプロファイル ARN」を指定する必要があります。

template.ymlを以下のように編集します。

```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Parameters:
  CodeSigningProfileVersionArn:
    Type: String
    Description: Versioned CodeSigningProfile Arn

Resources:
  function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "test_lambda_function"
      Handler: main.lambda_handler
      Runtime: python3.11
      CodeUri: ./function/.
      Description: Test Lambda Function
      Timeout: 10
      Policies:
        - AWSLambdaBasicExecutionRole
        - AWSLambda_ReadOnlyAccess
      CodeSigningConfigArn: !Ref csc

  csc:
    Type: AWS::Lambda::CodeSigningConfig
    Properties:
      Description: "Code Signing Config for test_lambda_function"
      AllowedPublishers:
        SigningProfileVersionArns:
          - !Ref CodeSigningProfileVersionArn
      CodeSigningPolicies:
        UntrustedArtifactOnDeployment: "Enforce"
```

上記の内容を追記したら、以下のコマンドを実行。変更点としては`cloudformation deploy`コマンドでパラメータ値署名プロファイルArnを指定する必要があります。

```bash
# Package
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function  --output-template-file out.yml

# Deploy
aws cloudformation deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM   --parameter-overrides CodeSigningProfileVersionArn=「CodeSigningProfileVersionのArn値」
```

これを実行すると、Lambdaコンソール上のコード署名設定画面で、署名プロファイル「test_signer」を元にしたコード署名設定を作成することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/4a51f51f-18d5-eeac-41d5-2e137c30beed.png)


### 変更前/変更後確認
以下はコード署名前のLambdaになります。コードソース自体中身が見えて編集することもできてしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/3bbbe8c7-dce3-c52f-f5c3-ea10f1c96907.png)

コード署名すると以下のように関数自体確認できなくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/97c93d88-54e8-d767-92db-c8c0afda0bc5.png)
設定 > コード署名画面でもコード署名に用いられている証明書Arnや署名プロファイルが作成されていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/a2199ee5-74c7-c144-1e73-56be1e405fb6.png)


## 4. 動作確認
### 動作確認前準備
以下のようにLambda関数を更新した後、cloudformation packageコマンドで、S3上にpythonファイルを圧縮したオブジェクトをアップロードします。
```
(before)
message = 'Hello {} {}!'.format(event['first_name'], event['last_name'])  

(after)
message = 'Hello {} {}!!!'.format(event['first_name'], event['last_name'])  
```

```bash
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function  --output-template-file out.yml
```

これでS3バケット上に変更後のスクリプトをアップロードできました。しかし、これは未だSignerの署名ジョブによってコード署名されていないオブジェクトになります。

### [失敗パターン] 未署名のファイルでデプロイでしてみる
Lambdaコンソールからアップロードされた未署名のオブジェクトを指定して、Lambda関数を更新してみます。
すると以下のように`Lambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept. `のエラーが発生しました。
未署名のオブジェクトだったためエラーになったものかと。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/bb46d01f-3d69-061a-2c97-1d0b77100adc.png)


また、CLI上から`cloudformation deploy`を実行して、Lambdaが更新されるのかを確認してみます。
すると以下のようなエラーになります。未署名のコードでデプロイしようとするとロールバック失敗になり大惨事になってしまいます。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/73a02c55-c14f-d696-95e8-84ae3c681771.png)


### [成功パターン]署名済みのファイルでデプロイでしてみる
成功させるには、事前に署名ジョブを実行してコード署名済みのオブジェクトにする必要があります。
[こちら](https://repost.aws/ja/knowledge-center/cloudformation-update-rollback-failed)の手順でCloudformation上のロールバックを完了させてから、実行するようにしましょう。


ロールバックが完了したら、先ほどのようなcloudformationとは異なり、AWS SAMを用いてデプロイしていきます。SAM を用いたコード署名は[こちら](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/authoring-codesigning.html)を参考に。

何故、CloudformationコマンドではなくSAMなのかの理由としては以下の通りです。
> cloudformation pacakgeコマンドで出力されるout.ymlが、どうやってもコード署名されたファイルを指定できなかった。。
> sedとかで書き換えることも可能ではあるが、SAMを使った方が--signing-profilesオプションを提供しているため、簡単にデプロイできる。

以下のコマンドを実行することで既存のCfnスタックをSAMでデプロイすることができます。

```bash
sam package --signing-profiles "function=test_signer" --template-file template.yml --s3-bucket 「バケット名」 --s3-prefix lambda/test_lambda_function  --output-template-file out.yml

sam deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM 
```

無事デプロイすることができました。
デプロイ後LambdaをInvokeして変更した内容が反映できていればOKです！

## 終わりに
既存のCfnテンプレートから作成したLambdaでも簡単にコード署名することができました。
また、AWS SAMを利用すれば`--signing-profiles`などのオプションを利用することで、コード署名からデプロイまでをワンストップで実施することができるため、デプロイの際はAWS SAMを利用するようにした方が良いかと思います。


## [補足] CLIで署名ジョブの実行。
署名ジョブを実行するには`start-signing-job`を実行する必要があります。start-signing-jobではS3オブジェクトの現行VersionIdも必要なため、以下のようにlist-object-versionsでバージョンを取得してから実行します。
```bash
# バケットのバージョンIDを確認。
BUCKET_NAME="「バケット名」"
KEY="Prefix名"
version =`aws s3api  list-object-versions --bucket $BUCKET_NAME --prefix $KEY --query "Versions[0].VersionId"`

# 署名ジョブの実行
aws signer start-signing-job     \
    --source "s3={bucketName=$BUCKET_NAME, key=$KEY, version=$version}"     \
    --destination "s3={bucketName=$BUCKET_NAME, prefix=signer/test_lambda_functi
on/}"     \
    --profile-name test_signer
```