signed cookie をやる方法はこの記事にまとめた。 rust でやったらまたハマったのでまたまとめておく。
CONTENTS
ENVIRONMENTS
- rust: 1.52.1
[dependencies] base64 = "0.13" rsa = "0.4" sha-1 = "0.9" digest = "0.9" pem = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1"
コード
use pem::parse; use rsa::RSAPrivateKey; use serde::Serialize; use serde_json::to_string; use digest::Digest; use rsa::{Hash, PaddingScheme, RSAPrivateKey}; use sha1::Sha1; use base64::{encode_config, STANDARD}; fn sign() { let pem = parse(PEM) .expect("parse pem error"); let private_key = RSAPrivateKey::from_pkcs1(pem.contents.as_ref()) .expect("parse key error"); let policy = Policy { statement: vec![Statement { resource, condition: Condition { date_less_than: ConditionDateLessThan { epoch_time: expires, }, }, }], }; let policy = to_string(&policy) .expect("encode json error"); let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA1)); let hash = Sha1::new().chain(policy.as_bytes()).finalize(); let signature = private_key.sign(padding, hash.as_ref()) .expect("sign error"); println!("{}", cloudfront_base64(policy)); println!("{}", cloudfront_base64(signature)); } fn cloudfront_base64(source: impl AsRef<[u8]>) -> String { // cloudfront flavored base64 encode_config(source, STANDARD) .replace("+", "-") .replace("=", "_") .replace("/", "~") } #[derive(Serialize)] pub struct Policy { #[serde(rename = "Statement")] pub statement: Vec<Statement>, } #[derive(Serialize)] pub struct Statement { #[serde(rename = "Resource")] pub resource: String, #[serde(rename = "Condition")] pub condition: Condition, } #[derive(Serialize)] pub struct Condition { #[serde(rename = "DateLessThan")] pub date_less_than: ConditionDateLessThan, } #[derive(Serialize)] pub struct ConditionDateLessThan { #[serde(rename = "AWS:EpochTime")] pub epoch_time: i64, } const PEM: &'static str = "-----BEGIN RSA PRIVATE KEY----- MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO 2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ -----END RSA PRIVATE KEY----- ";
pem をパース
use pem::parse; use rsa::RSAPrivateKey; fn parse() { let pem = parse(PEM) .expect("parse pem error"); let private_key = RSAPrivateKey::from_pkcs1(pem.contents.as_ref()) .expect("parse key error"); }
aws のキーペアを生成すると、秘密鍵は BEGIN RSA PRIVATE KEY
で始まる pem ファイルとしてダウンロードできる。
これを rsa の RSAPrivateKey
として読み込む。
rsa のドキュメントには、ファイルの内容を自分で読む方法が書かれているのだが、ちょっと嫌だった。 そこで pem を使用してバイトを読み出すことにした。 やっていることは多分同じようなはず。
これで private key が手に入った。
policy を構築
use serde::Serialize; use serde_json::to_string; fn build(resource: String, expires: i64) { let policy = Policy { statement: vec![Statement { resource, condition: Condition { date_less_than: ConditionDateLessThan { epoch_time: expires, }, }, }], }; let policy = to_string(&policy) .expect("encode json error"); } #[derive(Serialize)] pub struct Policy { #[serde(rename = "Statement")] pub statement: Vec<Statement>, } #[derive(Serialize)] pub struct Statement { #[serde(rename = "Resource")] pub resource: String, #[serde(rename = "Condition")] pub condition: Condition, } #[derive(Serialize)] pub struct Condition { #[serde(rename = "DateLessThan")] pub date_less_than: ConditionDateLessThan, } #[derive(Serialize)] pub struct ConditionDateLessThan { #[serde(rename = "AWS:EpochTime")] pub epoch_time: i64, }
正しく signed cookie 用の policy が構築できるように struct を構成する。
こいつに対して serde_json::to_string
すると json 文字列が手に入る。
rsa private key で sign
use digest::Digest; use rsa::{Hash, PaddingScheme, RSAPrivateKey}; use sha1::Sha1; fn sign(policy: String) { let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA1)); let hash = Sha1::new().chain(policy.as_bytes()).finalize(); let signature = private_key.sign(padding, hash.as_ref()) .expect("sign error"); }
signed cookie は sha1 でハッシュして sign する必要がある。 sha256 や sha512 を試したけどやっぱりだめだった。 (sha1 は使うなっていろんなところで言われてるけど大丈夫なのかしら)
new_pkcs1v15_sign
メソッドで Hash::SHA1
な PaddingScheme
を手に入れたら、RSAPrivateKey
で sign する。
これで signed cookie の Signature
が手に入る。
Sha1::new
を使用するには digest::Digest
を use する必要がある。
sha1::Digest
としても公開されているが、将来的に他の hash に切り替える可能性を考慮して大元の digest クレートから use している。
base64 エンコード
use base64::{encode_config, STANDARD}; fn encode(policy: String, signature: String) { println!("{}", cloudfront_base64(policy)); println!("{}", cloudfront_base64(signature)); } fn cloudfront_base64(source: impl AsRef<[u8]>) -> String { // cloudfront flavored base64 encode_config(source, STANDARD) .replace("+", "-") .replace("=", "_") .replace("/", "~") }
signed cookie は base64 でエンコードしたあと、特定の文字を置換する必要がある。 AWS のドキュメントにしたがってエンコードすれば完成。
デバッグ方法について
cloudfront の signed cookie が正しく動いているかは、本番で試さないとわからない。 しかしいちいち本番にリリースしていると時間がかかってしょうがない。 そこで確認のための時間のかからない方法を用意しておく。
以下のコマンドで Signature を得ることができる。
cat policy.json | openssl sha1 -sign private_key.pem | openssl base64 | tr -- '+=/' '-_~'
まず、この Signature を使用して curl
などで正しくアクセスできることを確認しておく。
そのあと、生成した cookie がこの値と一致することを確認する。
これで sign のプロセスは問題なく動いていることが確認できる。
まとめ
aws cloudfront の signed cookie を rust で生成する方法をまとめた。
sha512 とかにできないかといろいろ試したのがハマった原因。 キャッシュの関係で、正しくない cookie でもアクセスに成功しているように見えたのも敗因。 時間だけかかって結局 sha1 じゃないと上手くいかないっていうドキュメントに書いてある通りの結論になって(涙)。 まあ、そうだよね。 けど sha1 を使いたくないじゃん、っていう気持ちがあってね。