Lambdaからクリアテキスト署名メールを送ろうとして2ヶ月かかった話

SMIMEでメールを暗号化して送信していましたが、SMIMEに対応していないメーラーだと本文が読めないので **クリアテキスト署名** という形式で送ることになりました。
やり方が全然わからずプレッシャーに押しつぶされそうになりながらもなんとか解決できた経緯を書いていきます。

# 環境
+ Lambda(Node.js 12.x)
+ SES

# クリアテキスト署名とは

[PKI関連技術に関するコンテンツ](https://www.ipa.go.jp/security/pki/072.html)

SMIMEでメールを暗号化して送信すると、SMIMEに対応していないメーラーだとメールの本文が読めません。
メール本文と署名データを別にして、メール本文をplaintextで送る手法です。

# node-forgeで対応しようとしたが断念

[node-forge](https://github.com/digitalbazaar/forge#pkcs7)

**PKCS#7** という形式で署名データを生成するらしいが、クリアテキスト署名の生成方法がわからず。

## PKCS #7 分離署名
[3.4.3 multipart/signed フォーマットを使った署名](https://www.ipa.go.jp/security/rfc/RFC2311-03JA.html#name3_4)

さらに調べていくと、**PKCS #7 分離署名**という言葉を見つける。

## detached mode
node-forgeのReadmeを読んでいると、**detached mode**という記載を見つける。
おそらくこれが分離署名にあたるのではないかと仮定し、実装してみる。

```javascript
// PKCS#7 Sign in detached mode.
// Includes the signature and certificate without the signed data.
p7.sign({detached: true});
```

## node-forgeのIssuesに、detached modeについて言及しているものがあった

[PKCS#7 detached is not that much detached #607](https://github.com/digitalbazaar/forge/issues/607)

内容を読んでみると、detached modeに不具合があるもよう。
Closedになってるのでもう直っているのかなと思い、該当のソースを確認してみると…

```javascript
// TODO: optimize away duplication
```

いや直ってないんかいｗｗｗ
node-forgeは諦めよう。

# openssl_pkcs7_sign() というPHPの関数を見つける

```php
$message = realpath('message.txt');
$sign = realpath('sign.txt');
$cert = 'file://' .realpath('./cert.txt');
$key = 'file://' .realpath('./key.txt');
$headers = [
    'signing-time' => (new DateTime())->format('o-m-d H:i:s'),
];

$certfile = file_get_contents($cert);
$pkeyfile = file_get_contents($key);

openssl_pkcs7_sign($message, $sign, $certfile, array($pkeyfile, ''), $headers, PKCS7_TEXT | PKCS7_DETACHED);
```

出力される「sign.txt」の中身をそのままRawMessageとして、 ses.sendRawEmail()を送信したところうまくいった。
この関数はおそらく裏でopensslコマンドを実行しているだけだと思われる。
Lambdaでopensslを使えば解決するのでは。

# Lambdaでopensslコマンドを使用する
[AWS LambdaでOpenSSLを使う方法](https://dev.classmethod.jp/articles/aws-lambda-openssl/)

参考サイトのままだとうまくいかないので補足します。

## opensslに実行権限を付与
opensslに実行権限を付与してからzipを生成すること。

## zipの作り方に注意
opensslをディレクトリに入れてzipを作ると階層が1つ深くなってしまうので、以下の方法でzipを生成すること

```shell
zip -r openssl.zip openssl
```

# 最終的にこんなコードになりました。

```javascript
const execSync = require('child_process').execSync

const toAddresses = '送信先メールアドレス'
const cert = 'PEM形式の証明書'
const privateKey = 'PEM形式の秘密鍵'
let mailBody = 'メール本文'

const certPath = '/tmp/cert.txt'
fs.writeFileSync(certPath, cert)
 
const privateKeyPath = '/tmp/privateKey.txt'
fs.writeFileSync(privateKeyPath, privateKey)

mailBody = 'Content-Type: text/plain; charset=ISO-2022-JP\r\n\r\n' + mailBody 
  
const mailBodyPath = '/tmp/mailBody.txt'
fs.writeFileSync(mailBodyPath, mailBody )
  
// opensslコマンドで署名データ生成
const opensslCommand = `/opt/openssl smime -pk7out -sign -in ${mailBodyPath} -signer ${certPath} -inkey ${privateKeyPath}  -md SHA256 -from "${sender}" -to "${toAddresses}" -subject "${subject}"`
const rawMessage = execSync(opensslCommand).toString()
  
const eParams = {
  Destinations: [toAddresses],
  RawMessage: {
    Data: Buffer.from(rawMessage),
  },
  Source: sender,
}

await ses.sendRawEmail(eParams).promise()

```


おそらく**-pk7out** が分離署名にあたるのではないかと思われます。
証明書などのデータはファイルから読み込む方法しかないようなので、無理やりですが、/tmp/ディレクトリに保存して読み込んでいます。