2024/08/31
目次
はじめに
本Partでは、署名アルゴリズム関連の実装を行う。
ここでは、サンプルとして、HMAC-SHA256のみ記述するが、複数アルゴリズムの定義に対応できるような構成で実装を行う。
コード全文
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::Result;
pub(super) trait Algorithm {
fn init<T: AsRef<[u8]>>(secret: &[u8], data: T) -> Result<Self>
where
Self: Sized;
fn verify(self, signature: &[u8]) -> bool;
fn sign(self) -> Vec<u8>;
}
#[derive(Clone)]
pub(super) struct HS256(Hmac<Sha256>);
impl Algorithm for HS256 {
fn init<T: AsRef<[u8]>>(secret: &[u8], data: T) -> Result<Self> {
let mut mac = Hmac::<Sha256>::new_from_slice(secret)?;
mac.update(data.as_ref());
Ok(Self(mac))
}
fn verify(self, signature: &[u8]) -> bool {
let result = self.0.verify_slice(signature);
result.is_ok()
}
fn sign(self) -> Vec<u8> {
self.0.finalize().into_bytes().to_vec()
}
}
#[cfg(test)]
mod tests {
use crate::jwt::base64::Base64;
use super::*;
#[test]
fn sign_verify() -> Result<()> {
const VALID_DATA:&str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
const TAMPERED_DATA : &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkxIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
let signature = Base64::decode("mpHl842O7xEZjgQ8CyX8xYLDoEORGVMnAxULkW-u8Ek")?;
const SECRET: &[u8; 11] = b"test-secret";
let hs256 = HS256::init(SECRET, VALID_DATA)?;
assert!(hs256.verify(&signature));
let hs256 = HS256::init(SECRET, TAMPERED_DATA)?;
assert!(!hs256.clone().verify(&signature));
let signature = hs256.clone().sign();
assert!(hs256.verify(&signature));
Ok(())
}
}
解説
useなど
今回は、HMAC-SHA256をサポートするため、hmac
とsha2
を利用する。
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::Result;
Algorithmトレイト
ここが、「複数署名アルゴリズムへの対応」を容易にするためのトレイト定義となる。
仮にアルゴリズムを追加する場合、このトレイトへの実装を前提とすることで、将来的な変更を容易にすることができるし、実装漏れを早期の段階で検知できるという目的で書いた。
基本的にはこのトレイトを実装する構造体は使い捨てのイメージで、init
で初期化し、verify
で検証、あるいはsign
で署名を行い、消費する。
各種引数の型は、基本的に現在唯一のサポートアルゴリズムであるHMAC-SHA256実装を引き継いでいるが、これはアルゴリズムが増える場合に、よりよい共通化となる肩があればそちらに変更することになる。
verify()
,sign()
が構造体を消費するように定義しているのは、今回実装するHmacの verify_slice()
, finalize()
を引き継いでおり、トレイトの実装としては、勝手にCloneすると無駄なコピーが発生するためである。
使いまわしたいかどうかは、Algorithm
トレイトを利用する側に委ねられ、そのシチュエーションではこれらのメソッドを実行する前にclone()
を呼び出すことになる。
sign()
,verify()
の使い回しがしたい場合は、trait Algorithm: Clone
とするのも良い選択と思われるが、ここでは「署名アルゴリズムの実行のためには不要である」の観点から、それを行わなかった。
pub(super) trait Algorithm {
fn init<T: AsRef<[u8]>>(secret: &[u8], data: T) -> Result<Self>
where
Self: Sized;
fn verify(self, signature: &[u8]) -> bool;
fn sign(self) -> Vec<u8>;
}
HS256構造体
以下のように、ニュータイプでHMAC-SHA256をラップする構造体を定義する。
扱いやすさのため、Clone
の実装を付加しておく。
#[derive(Clone)]
pub(super) struct HS256(Hmac<Sha256>);
HS256のAlgorithmトレイト実装
impl Algorithm for HS256 {
fn init<T: AsRef<[u8]>>(secret: &[u8], data: T) -> Result<Self> {
let mut mac = Hmac::<Sha256>::new_from_slice(secret)?;
mac.update(data.as_ref());
Ok(Self(mac))
}
fn verify(self, signature: &[u8]) -> bool {
let result = self.0.verify_slice(signature);
result.is_ok()
}
fn sign(self) -> Vec<u8> {
self.0.finalize().into_bytes().to_vec()
}
}
test
サンプルJWTを分割したものを用意し、署名を検証するテストを行う。
- VALID_DATA: 正しいデータ
- TAMPERED_DATA: データを改ざんしたもの
- signature: VALID_DATAに対する署名(jwtの3つ目の部分のbase64デコード結果)
テストの流れは、以下のようにした。
- VALID_DATAに対して署名を検証する -> OK
- TAMPERED_DATAに対して署名を検証する -> NG
- TAMPERED_DATAに対して署名を作成し、検証する -> OK
#[cfg(test)]
mod tests {
use crate::jwt::base64::Base64;
use super::*;
#[test]
fn sign_verify() -> Result<()> {
const VALID_DATA:&str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
const TAMPERED_DATA : &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkxIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
let signature = Base64::decode("mpHl842O7xEZjgQ8CyX8xYLDoEORGVMnAxULkW-u8Ek")?;
const SECRET: &[u8; 11] = b"test-secret";
let hs256 = HS256::init(SECRET, VALID_DATA)?;
assert!(hs256.verify(&signature));
let hs256 = HS256::init(SECRET, TAMPERED_DATA)?;
assert!(!hs256.clone().verify(&signature));
let signature = hs256.clone().sign();
assert!(hs256.verify(&signature));
Ok(())
}
}