記事の概要
mbet TLSを用いてJWT認証を利用します。
サンプルプログラムは以下に置いてあります。
https://github.com/matsuikosuke/mbedTLSTest
JWTとは何か
JWTについては以下の解説が詳しいです。
JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用
JWTはJSON Web Tokenの略です。
JWTはヘッダーとペイロードと署名の3つから構成されています。
以下のサイトで実際にJWTを作成しながら、具体的な中身を見ていきます。
https://jwt.io/
ヘッダ
ヘッダには"typ"と"alg"を記述します。
"typ"には"JWT"を指定します。
"alg"は署名アルゴリズムを意味し、 "HS256"(SHA-256)や"RS256"(SHA-256)を指定します。
今回はRS256を指定します。
{
"alg": "RS256",
"typ": "JWT"
}
これをBase64urlエンコードすると、以下のようなヘッダになります。
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
ペイロード
{
"sub": "Hello,World!",
"name": "Kosuke Matsui",
"iat": 875674839284
}
これをBase64urlエンコードすると、以下のようなペイロードになります。
eyJzdWIiOiJIZWxsbyxXb3JsZCEiLCJuYW1lIjoiS29zdWtlIE1hdHN1aSIsImlhdCI6ODc1Njc0ODM5Mjg0fQ
署名
署名はヘッダーとペイロードを秘密鍵で暗号化し、Base64urlエンコードしたものです。
サイトの署名欄にPublic Key(公開鍵)とPrivate Key(秘密鍵)を代入すると以下を得ます。
N_zzSrKu4emL6OzpAQ7nBsR6XkV3I2TEkkrqM4Ldm7m42a78HEXsHx5X7AQQlApSPzCuIL3elXfrr4Tl2IRlstlsU6Im79V7hpWigAWnT4HyINEX74LKGeGHwve1iJpXKvTYIsjwTaKWwVGmHG2CPqVz_gVlNJJoe9PyMGzLnzZcUIYj20ATaE1NgdSoZEc9xA4T7EQWdTS4WRtSffPTREPG1Wgf0LVQZueW2P1kYsf9-_ItJTJk2GRqnzqaob-5hRANrCqWcxEr-HKw4PIftnxiVN3WqT3NWG7qT6UKfsKVNfFJZJOWcmr0UJGIpHavuvjnd-5P_9AbYchkKCucMg
鍵の生成は以下のサイトでRSA鍵ペアを作成しました。
RSA鍵ペアの生成
-----BEGIN PRIVATE KEY-----MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCn7wJ2SLBWm0S88DPOn2qGLlbHCZ9sfjlpLtjV5JshGCUW/T8a6T/lGhud3W8RXRtmdLGeRJCLN3nl9TJhWXNb0uMtlApvMNfe/H6dOvScOWMZ+sRNse1TIZO7pSt6LS5DCx7trHwZLxYLE7nSLsA3ljOT7uIl5zV07raVpbw2GApnZJdnYcmTjnw+iHScEj+vO0MTCddyo7Mw8T0obMLe4GJtv1YlmS69krsRWABVfls9ZGxTvrk2SpAXHBcFwKfLiQ2q2jghYfYlwjKJMx78eUn+ODPfqB9qgtw5A8to3VpxijG2ifbGU4SQnTHe+vreLMSCfF3GJlYWwQx53NuxAgMBAAECggEAALUDpUAv4QAkQOVnk1Pd8+aSKNspkmdt4Ffj4CwDvoimjDe7iVgxhYNDPYFsdSFs89Tf17Fd4VO8+Jn1MryDZ8b2Cj/PtsTq0uII8xwyYgat1PoBRXsksmSiE+Mhih9Sd5l3VQnbt6+mkX+0fG0ghbtlHaVJdbO6zCtpXRB/hvzGLA5A0TI6GE166/9LHcq6+PhaaUIFX8vpiUuLt/XiOqOglaIfR/VSIPShlqzMEpYSpfyDatanVT0G5m5gu4/3LfrSzla55Q9VRu2YjgEIeVkImvBn7PMhYjyHUO7AaExZEAWWtbcCA1oIl3DSZjhjGK62ao2Iw5vtjJDyOmUXLwKBgQDkODKC7DGqP3d+uP116HGp8TKRDeV3MrjR7uwgFo9XkLzYc5eJTFHZHYJ4WNAQNxbDJnoTiobbLcxVv2Jw8nB34gDO3Am2+mdRYVlcdAbkyeJV+95Fb6OvZ/ZQKom9cnjNNu3TP1s6lrSaH+nHnfDoeRQiznbpfLxv/MKnJYiivwKBgQC8YC0y1XIHpOL78eMF8PKZAm9Ro9h9SA2D7rTaUchrzKYJjhcT2RS+j/Kv6HdeuqSAyjptLcI/i1QyfvJy2TK9ST1MayhRhX+dJ5Zp6QZDOX0s1O8ccTPNhjZf7DD24LcX8Plj56W8a100Aw1YsPoBvAP5vsmhnLsH6ta/AInNjwKBgQDat2DxSBtw5dJHiFKgpVwJWjbz/TVkvx+RUkDJn9VQPk49wsn/szzdrwJTBAqi/6i47i8geyoy8/lYVEqrpC1VNys/FHHbAq/xtjJGAIx6x2A6t1+VfCCLAj91JM9M2GAdi/7w7U8pHzWGX/9UZ3Fx2lgJ44Tiz8BoMvz542bHUQKBgCXSzeCCGRZN1kGuTN45hgyQ/5cN5f8pw4T1Hh0kBLEYc1JB1IJouRCWSK1naOh9Y2B0uoyHfpRRvoMxGC9VSynUldlNPtg3jOsaMsprPpWq6mZhDu3QEQ24YByciSxY7IqDGSAlOBMAH8O8xSZEyr4kaUvxMf0gQjGRxrKtQf4jAoGBAK7Xo8uDmBqHCnNjGUlHqtf4ipwBRI2aH/izHHL2JUkHMvWac0e6VdQxdLPsqulcaoHzJRrX/gGYJJ3YFJ4VjevRMirQ9ZKnT3mNJWU6XfU7TpH3Up8JQ0bQH/B8AV1Ogl4RNOVeZQwsSAf/Ba+VCHNhfFeMKqz2jS6+VRHEpvyd-----END PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+8CdkiwVptEvPAzzp9qhi5WxwmfbH45aS7Y1eSbIRglFv0/Guk/5Robnd1vEV0bZnSxnkSQizd55fUyYVlzW9LjLZQKbzDX3vx+nTr0nDljGfrETbHtUyGTu6Urei0uQwse7ax8GS8WCxO50i7AN5Yzk+7iJec1dO62laW8NhgKZ2SXZ2HJk458Poh0nBI/rztDEwnXcqOzMPE9KGzC3uBibb9WJZkuvZK7EVgAVX5bPWRsU765NkqQFxwXBcCny4kNqto4IWH2JcIyiTMe/HlJ/jgz36gfaoLcOQPLaN1acYoxton2xlOEkJ0x3vr63izEgnxdxiZWFsEMedzbsQIDAQAB-----END PUBLIC KEY-----
Base64url
Base64urlはBase64と似ていますが、少し違います。
Base64の+
は、Base64urlでは-
になります。
Base64の/
は、Base64urlでは_
になります。
Base64の=
は、Base64urlでは削除します。
JWT認証について
以上のヘッダーとペイロードと署名を.
で連結したものがJWTです。
例えばこれを受信したとします。
そして受信側はPublic Key(公開鍵)を知っているとします。
受信者はヘッダーとペイロードをPublic Key(公開鍵)を用いて暗号化し、それをBase64urlデコードします。
それが署名と一致すれば、正しいデータであることが分かります。
Base64
受信したJWTの解読にはBase64urlデコードが必要になります。
以下の関数ではBase64でのエンコードとデコードを試しています。
実際にこれらの関数をJWTに適用する場合は、前もってBase64urlとBase64を変換する処理が必要になります。
static const unsigned char base64_test_dec[64] =
{
0x24, 0x48, 0x6E, 0x56, 0x87, 0x62, 0x5A, 0xBD,
0xBF, 0x17, 0xD9, 0xA2, 0xC4, 0x17, 0x1A, 0x01,
0x94, 0xED, 0x8F, 0x1E, 0x11, 0xB3, 0xD7, 0x09,
0x0C, 0xB6, 0xE9, 0x10, 0x6F, 0x22, 0xEE, 0x13,
0xCA, 0xB3, 0x07, 0x05, 0x76, 0xC9, 0xFA, 0x31,
0x6C, 0x08, 0x34, 0xFF, 0x8D, 0xC2, 0x6C, 0x38,
0x00, 0x43, 0xE9, 0x54, 0x97, 0xAF, 0x50, 0x4B,
0xD1, 0x41, 0xBA, 0x95, 0x31, 0x5A, 0x0B, 0x97
};
static const unsigned char base64_test_enc[] =
"JEhuVodiWr2/F9mixBcaAZTtjx4Rs9cJDLbpEG8i7hPK"
"swcFdsn6MWwINP+Nwmw4AEPpVJevUEvRQbqVMVoLlw==";
int mbedtls_base64_self_test(void)
{
int ret = 1;
size_t len;
const unsigned char *src;
unsigned char buffer[200];
src = base64_test_dec;
if( mbedtls_base64_encode( buffer, sizeof( buffer ), &len, src, 64 ) != 0 ||
memcmp( base64_test_enc, buffer, 88 ) != 0 )
{
printf("BASE64 encode error\r\n");
return( 1 );
}
printf("BASE64 encode success\r\n");
src = base64_test_enc;
if( mbedtls_base64_decode( buffer, sizeof( buffer ), &len, src, 88 ) != 0 ||
memcmp( base64_test_dec, buffer, 64 ) != 0 )
{
printf("BASE64 decode error\r\n");
return( 1 );
}
printf("BASE64 decode success\r\n");
return( 0 );
}
JWT認証
以下の関数mbedtls_jwt_self_test
においては、JWTデータをデータ部分(ヘッダとペイロード)と署名に分割し、Base64urlをBase64に変換した上で、署名の検証をrsa_jwt_verification_test
にて行い、ペイロードをBase64デコードして属性データを取得しています。
// 256 byte
#define RSA1_TEST_PRIVATE_KEY \
"-----BEGIN PRIVATE KEY-----\r\n" \
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCn7wJ2SLBWm0S8\r\n" \
"8DPOn2qGLlbHCZ9sfjlpLtjV5JshGCUW/T8a6T/lGhud3W8RXRtmdLGeRJCLN3nl\r\n" \
"9TJhWXNb0uMtlApvMNfe/H6dOvScOWMZ+sRNse1TIZO7pSt6LS5DCx7trHwZLxYL\r\n" \
"E7nSLsA3ljOT7uIl5zV07raVpbw2GApnZJdnYcmTjnw+iHScEj+vO0MTCddyo7Mw\r\n" \
"8T0obMLe4GJtv1YlmS69krsRWABVfls9ZGxTvrk2SpAXHBcFwKfLiQ2q2jghYfYl\r\n" \
"wjKJMx78eUn+ODPfqB9qgtw5A8to3VpxijG2ifbGU4SQnTHe+vreLMSCfF3GJlYW\r\n" \
"wQx53NuxAgMBAAECggEAALUDpUAv4QAkQOVnk1Pd8+aSKNspkmdt4Ffj4CwDvoim\r\n" \
"jDe7iVgxhYNDPYFsdSFs89Tf17Fd4VO8+Jn1MryDZ8b2Cj/PtsTq0uII8xwyYgat\r\n" \
"1PoBRXsksmSiE+Mhih9Sd5l3VQnbt6+mkX+0fG0ghbtlHaVJdbO6zCtpXRB/hvzG\r\n" \
"LA5A0TI6GE166/9LHcq6+PhaaUIFX8vpiUuLt/XiOqOglaIfR/VSIPShlqzMEpYS\r\n" \
"pfyDatanVT0G5m5gu4/3LfrSzla55Q9VRu2YjgEIeVkImvBn7PMhYjyHUO7AaExZ\r\n" \
"EAWWtbcCA1oIl3DSZjhjGK62ao2Iw5vtjJDyOmUXLwKBgQDkODKC7DGqP3d+uP11\r\n" \
"6HGp8TKRDeV3MrjR7uwgFo9XkLzYc5eJTFHZHYJ4WNAQNxbDJnoTiobbLcxVv2Jw\r\n" \
"8nB34gDO3Am2+mdRYVlcdAbkyeJV+95Fb6OvZ/ZQKom9cnjNNu3TP1s6lrSaH+nH\r\n" \
"nfDoeRQiznbpfLxv/MKnJYiivwKBgQC8YC0y1XIHpOL78eMF8PKZAm9Ro9h9SA2D\r\n" \
"7rTaUchrzKYJjhcT2RS+j/Kv6HdeuqSAyjptLcI/i1QyfvJy2TK9ST1MayhRhX+d\r\n" \
"J5Zp6QZDOX0s1O8ccTPNhjZf7DD24LcX8Plj56W8a100Aw1YsPoBvAP5vsmhnLsH\r\n" \
"6ta/AInNjwKBgQDat2DxSBtw5dJHiFKgpVwJWjbz/TVkvx+RUkDJn9VQPk49wsn/\r\n" \
"szzdrwJTBAqi/6i47i8geyoy8/lYVEqrpC1VNys/FHHbAq/xtjJGAIx6x2A6t1+V\r\n" \
"fCCLAj91JM9M2GAdi/7w7U8pHzWGX/9UZ3Fx2lgJ44Tiz8BoMvz542bHUQKBgCXS\r\n" \
"zeCCGRZN1kGuTN45hgyQ/5cN5f8pw4T1Hh0kBLEYc1JB1IJouRCWSK1naOh9Y2B0\r\n" \
"uoyHfpRRvoMxGC9VSynUldlNPtg3jOsaMsprPpWq6mZhDu3QEQ24YByciSxY7IqD\r\n" \
"GSAlOBMAH8O8xSZEyr4kaUvxMf0gQjGRxrKtQf4jAoGBAK7Xo8uDmBqHCnNjGUlH\r\n" \
"qtf4ipwBRI2aH/izHHL2JUkHMvWac0e6VdQxdLPsqulcaoHzJRrX/gGYJJ3YFJ4V\r\n" \
"jevRMirQ9ZKnT3mNJWU6XfU7TpH3Up8JQ0bQH/B8AV1Ogl4RNOVeZQwsSAf/Ba+V\r\n" \
"CHNhfFeMKqz2jS6+VRHEpvyd\r\n" \
-"----END PRIVATE KEY-----\r\n"
// 256 byte
#define RSA1_TEST_PUBLIC_KEY \
"-----BEGIN PUBLIC KEY-----\r\n" \
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+8CdkiwVptEvPAzzp9q\r\n" \
"hi5WxwmfbH45aS7Y1eSbIRglFv0/Guk/5Robnd1vEV0bZnSxnkSQizd55fUyYVlz\r\n" \
"W9LjLZQKbzDX3vx+nTr0nDljGfrETbHtUyGTu6Urei0uQwse7ax8GS8WCxO50i7A\r\n" \
"N5Yzk+7iJec1dO62laW8NhgKZ2SXZ2HJk458Poh0nBI/rztDEwnXcqOzMPE9KGzC\r\n" \
"3uBibb9WJZkuvZK7EVgAVX5bPWRsU765NkqQFxwXBcCny4kNqto4IWH2JcIyiTMe\r\n" \
"/HlJ/jgz36gfaoLcOQPLaN1acYoxton2xlOEkJ0x3vr63izEgnxdxiZWFsEMedzb\r\n" \
"sQIDAQAB\r\n" \
"-----END PUBLIC KEY-----\r\n"
// 466 chars
static unsigned char jwt_sign_sample[] =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJIZWxsbyxXb3JsZCEiLCJuYW1lIjoiS29zdWtlIE1hdHN1aSIsImlhdCI6ODc1Njc0ODM5Mjg0fQ.N_zzSrKu4emL6OzpAQ7nBsR6XkV3I2TEkkrqM4Ldm7m42a78HEXsHx5X7AQQlApSPzCuIL3elXfrr4Tl2IRlstlsU6Im79V7hpWigAWnT4HyINEX74LKGeGHwve1iJpXKvTYIsjwTaKWwVGmHG2CPqVz_gVlNJJoe9PyMGzLnzZcUIYj20ATaE1NgdSoZEc9xA4T7EQWdTS4WRtSffPTREPG1Wgf0LVQZueW2P1kYsf9-_ItJTJk2GRqnzqaob-5hRANrCqWcxEr-HKw4PIftnxiVN3WqT3NWG7qT6UKfsKVNfFJZJOWcmr0UJGIpHavuvjnd-5P_9AbYchkKCucMg";
static int rsa_jwt_verification_test(uint8_t *data, uint16_t data_len, uint8_t *signature, uint16_t signature_len)
{
int ret = 1;
mbedtls_pk_context p_pk;
mbedtls_rsa_context *p_rsa;
uint8_t hash[32] = {0};
// hash SHA-256
ret = mbedtls_sha256_ret( data, data_len, hash, 0 );
if( 0 != ret)
goto exit;
// public key
mbedtls_rsa_init( p_rsa, MBEDTLS_RSA_PKCS_V15, 0 );
mbedtls_pk_init(&p_pk);
ret = mbedtls_pk_parse_public_key(&p_pk, RSA1_TEST_PUBLIC_KEY, strlen(RSA1_TEST_PUBLIC_KEY) + 1);
if (ret != 0)
goto exit;
p_rsa = mbedtls_pk_rsa( p_pk );
// 64Base decode signature
size_t len;
unsigned char jwt_decoded_buffer[256] = {0};
ret = mbedtls_base64_decode( jwt_decoded_buffer, sizeof( jwt_decoded_buffer ), &len, signature, signature_len );
if (ret != 0)
goto exit;
// verification
ret = mbedtls_rsa_rsassa_pkcs1_v15_verify(p_rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, 0, hash, jwt_decoded_buffer);
if (ret != 0)
goto exit;
// free context
mbedtls_rsa_free(p_rsa);
mbedtls_pk_free(&p_pk);
printf("JWT validation success\r\n");
return ret;
exit:
// free context
mbedtls_rsa_free(p_rsa);
mbedtls_pk_free(&p_pk);
printf("JWT validation error\r\n");
return ret;
}
int mbedtls_jwt_self_test(void)
{
int ret = 1;
uint16_t data_len;
uint16_t payload_len;
uint16_t signature_len;
uint16_t token_len;
size_t len;
uint8_t dot_count = 0;
uint16_t payload_start_index = 0;
uint16_t signatue_start_index = 0;
sys_timer_count[TEST_TIMER] = 0;
uint8_t expand_jwt_sign_test[500] = {0};
for(int i=0; i<466; i++)
{
expand_jwt_sign_test[i] = jwt_sign_sample[i];
}
for(int i=0; i<JWT_SIZE; i++)
{
if(0x00 == expand_jwt_sign_test[i])
{
token_len = i;
break;
}
}
for(int i=0; i<token_len; i++)
{
if(*(expand_jwt_sign_test+i) == '-')
{
*(expand_jwt_sign_test+i) = '+';
}
if(*(expand_jwt_sign_test+i) == '_')
{
*(expand_jwt_sign_test+i) = '/';
}
}
uint8_t jwt_buffer[200];
// check data format
// 01234.6789.BCDEF(token_len = 16chars)
// data_len = A(10) -> sig_len = token_len - data_len - 1 = 16 - 10 - 1 = 5chars
// pay_start_index = 6 -> pay_len = 16 - pay_start_index - sig_len - 1 = 16 - 6 - 5 - 1 = 4chars
for(int i=0; i<token_len; i++)
{
if(*(expand_jwt_sign_test+i) == '.')
{
dot_count += 1;
if(1 == dot_count)
{
payload_start_index = i+1;
dot_count = 0;
break;
}
}
}
for(int i=0; i<token_len; i++)
{
if(*(expand_jwt_sign_test+i) == '.')
{
dot_count += 1;
if(2 == dot_count)
{
signatue_start_index = i+1;
data_len = i;
signature_len = token_len - data_len -1;
break;
}
}
}
payload_len = token_len - payload_start_index - signature_len -1;
// check data format error
if(2 != dot_count)
return 0x0FFF;
// separate JWT into data and signature
uint8_t data[data_len];
uint8_t payload[payload_len];
uint8_t signature[signature_len];
for(int i=0; i<data_len; i++)
{
*(data+i) = *(expand_jwt_sign_test+i);
}
for(int i=0; i<payload_len; i++)
{
*(payload+i) = *(expand_jwt_sign_test+payload_start_index+i);
}
for(int i=0; i<signature_len; i++)
{
*(signature+i) = *(expand_jwt_sign_test+signatue_start_index+i);
}
// signature must be a multiple of 4
uint16_t old_signature_len = signature_len;
uint16_t signature_len_mod = signature_len%4;
if(0 != signature_len_mod)
{
signature_len = signature_len + (4 - signature_len_mod);
for(int i=old_signature_len; i<signature_len; i++)
{
*(signature+i) = '=';
}
}
// verification
ret = rsa_jwt_verification_test(data, data_len, signature, signature_len);
// decode Base64
mbedtls_base64_decode(jwt_buffer, sizeof( jwt_buffer ), &len, payload, payload_len);
printf("JWT=");
for (uint16_t i=0; i<len; i++) {
printf("%c", jwt_buffer[i]);
}
printf("\r\n");
printf("Execution time of rsa_jwt_verification_test %d ms\n",sys_timer_count[TEST_TIMER]*50);
sys_timer_count[TEST_TIMER] = 0;
return ret;
}
署名の検証
rsa_jwt_verification_test
においては、まずデータをmbedtls_sha256_ret
によりSHA256で32バイトのデータにハッシュ化しています。
次にPublic Keyを取り込み、署名(事前にBase64urlをBase64に変換済み)をBase64デコードします。
そしてmbedtls_rsa_rsassa_pkcs1_v15_verify
関数にハッシュ化したデータとBase64デコードした署名を渡すと、署名の検証を行ってくれます。
参照
mbed TLSでAES-GCMを利用する
mbed TLSでRSAを利用する「RSA-OAEPの暗号化と復号」
mbed TLSでRSAを利用する「RSAの公開鍵と秘密鍵の作成」
mbed TLSおよびOberonでECDHを利用する
mbed TLSでHKDFを利用する
mbed TLSでJWTを利用する