SMIMEでメールを暗号化して送信していましたが、SMIMEに対応していないメーラーだと本文が読めないので クリアテキスト署名 という形式で送ることになりました。
やり方が全然わからずプレッシャーに押しつぶされそうになりながらもなんとか解決できた経緯を書いていきます。
環境
- Lambda(Node.js 12.x)
- SES
クリアテキスト署名とは
SMIMEでメールを暗号化して送信すると、SMIMEに対応していないメーラーだとメールの本文が読めません。
メール本文と署名データを別にして、メール本文をplaintextで送る手法です。
node-forgeで対応しようとしたが断念
PKCS#7 という形式で署名データを生成するらしいが、クリアテキスト署名の生成方法がわからず。
PKCS #7 分離署名
3.4.3 multipart/signed フォーマットを使った署名
さらに調べていくと、PKCS #7 分離署名という言葉を見つける。
detached mode
node-forgeのReadmeを読んでいると、detached modeという記載を見つける。
おそらくこれが分離署名にあたるのではないかと仮定し、実装してみる。
// 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
内容を読んでみると、detached modeに不具合があるもよう。
Closedになってるのでもう直っているのかなと思い、該当のソースを確認してみると…
// TODO: optimize away duplication
いや直ってないんかいwww
node-forgeは諦めよう。
openssl_pkcs7_sign() という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コマンドを使用する
参考サイトのままだとうまくいかないので補足します。
opensslに実行権限を付与
opensslに実行権限を付与してからzipを生成すること。
zipの作り方に注意
opensslをディレクトリに入れてzipを作ると階層が1つ深くなってしまうので、以下の方法でzipを生成すること
zip -r openssl.zip openssl
最終的にこんなコードになりました。
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/ディレクトリに保存して読み込んでいます。