ys memos

Blog

Rustで簡単なJWTライブラリを実装してみた ~4. 署名アルゴリズム~


rust

2024/08/31


  1. 概要と使用例
  2. プロジェクト設定
  3. base64関連
  4. 署名アルゴリズム関連
  5. ヘッダ定義
  6. JWT本体の実装
  7. JWT本体のTest

本Partでは、署名アルゴリズム関連の実装を行う。

ここでは、サンプルとして、HMAC-SHA256のみ記述するが、複数アルゴリズムの定義に対応できるような構成で実装を行う。


src/jwt/header/algorithm.rs
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(())
    }
}


今回は、HMAC-SHA256をサポートするため、hmacsha2を利用する。

src/jwt/header/algorithm.rs
use hmac::{Hmac, Mac};
use sha2::Sha256;

use crate::error::Result;

ここが、「複数署名アルゴリズムへの対応」を容易にするためのトレイト定義となる。

仮にアルゴリズムを追加する場合、このトレイトへの実装を前提とすることで、将来的な変更を容易にすることができるし、実装漏れを早期の段階で検知できるという目的で書いた。

基本的にはこのトレイトを実装する構造体は使い捨てのイメージで、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>;
}

以下のように、ニュータイプでHMAC-SHA256をラップする構造体を定義する。

扱いやすさのため、Cloneの実装を付加しておく。

#[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()
    }
}

サンプルJWTを分割したものを用意し、署名を検証するテストを行う。

  • VALID_DATA: 正しいデータ
  • TAMPERED_DATA: データを改ざんしたもの
  • signature: VALID_DATAに対する署名(jwtの3つ目の部分のbase64デコード結果)

テストの流れは、以下のようにした。

  1. VALID_DATAに対して署名を検証する -> OK
  2. TAMPERED_DATAに対して署名を検証する -> NG
  3. 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(())
    }
}


関連タグを探す