2020年3月14日土曜日

RS256を使ったJWTを作成する

以前にこちらの記事にて、APEX_JWTパッケージを使わずにJWTを生成する方法について紹介しました。APEX_JWTパッケージでは、JSON Webアルゴリズムとして、HMAC SHA-256 ("HS256")のみがサポートされていて、RSASSA-PKCS1-v1_5 with SHA-256 ("RS256")には対応していません。RS256を指定したJWTを生成するために、APEX_JWTパッケージが持つ機能の一部であっても、活用する方法はありませんでした。

以下に、RS256をデジタル署名としたJWTを生成する方法を記載します。Oracle Databaseはいくつかの暗号処理が実装されていますが、RSA公開鍵暗号方式については未実装です。そのため、デジタル署名の生成については、データベースに組み込みのJavaにて実装します。

RSAの公開鍵/秘密鍵はこちらの記事で紹介したのと、同じ方法で生成します。
mkdir -p ~/.oci && openssl genrsa -out ~/.oci/poa_oci_api_key.pem 2048
実際のデータの部分だけを一行にして取り出せるように、こちらの記事で紹介したスクリプトも以下に紹介しておきます。
#!/bin/sh

while read l
do
   test ${l:0:1} != "-" && /bin/echo -n $l
done < ~/.oci/poa_oci_api_key.pem
echo

デジタル署名のJavaの実装をデータベースに登録する


デジタル署名を行うJavaの実装を、ファイル名SHA256withRSA.javaとして用意しました。
import java.math.BigInteger; 
import java.util.Base64;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.RSAPrivateCrtKeySpec;

import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
  
public class SHA256withRSA { 
  /**
   * RSASSA-PKCS1-v1_5 with SHA-256によるデジタル署名の生成
   *
   * @param header    BASE64エンコード済みのJWTヘッダー
   * @param payload   BASE64エンコード済みのJWTペイロード
   * @param key       PKCS#1形式でのRSA秘密鍵(BEGIN/END行なし、改行なし)
   * @return デジタル署名 - BASE64エンコード済み
   */
  public static String sign(String header, String payload, String key)
  throws Exception
  {
    /* header and payload are both encoded in base64 */
    byte[] data = new String(header + "." + payload).getBytes("UTF-8");

    /* get PrivateKey instance from PKCS#1 */
    byte[] pkdata = Base64.getDecoder().decode(key);
    DerInputStream derReader = new DerInputStream(pkdata);
    DerValue[] seq = derReader.getSequence(0);
    // skip version seq[0];
    BigInteger modulus = seq[1].getBigInteger();
    BigInteger publicExp = seq[2].getBigInteger();
    BigInteger privateExp = seq[3].getBigInteger();
    BigInteger prime1 = seq[4].getBigInteger();
    BigInteger prime2 = seq[5].getBigInteger();
    BigInteger exp1 = seq[6].getBigInteger();
    BigInteger exp2 = seq[7].getBigInteger();
    BigInteger crtCoef = seq[8].getBigInteger();

    RSAPrivateCrtKeySpec keySpec =
      new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

    // creating the object of Signature 
    Signature sr = Signature.getInstance("SHA256withRSA"); 
    sr.initSign(privateKey);
    sr.update(data);
    byte[] bytes = sr.sign();
   
    /* return signature in Base64 */ 
    return Base64.getEncoder().encodeToString(bytes);
  }
}
これをデータベースにロードします。ロードするスキーマはMYWORKSPACEとします。
loadjava -force -verbose -resolve -u myworkspace/**********@localhost/service.world SHA256withRSA.java
-verboseをオプションがついているので、以下のような出力が行われます。最後のErrorsが0でloadjavaの実行が終了したら、このJavaコードのデータベースへのロードが完了です。
arguments: '-u' 'myworkspace/***@localhost/service.world' '-force' '-verbose' '-resolve' 'SHA256withRSA.java' creating : source SHA256withRSA
loading  : source SHA256withRSA
resolving: source SHA256withRSA
Classes Loaded: 0
Resources Loaded: 0
Sources Loaded: 1
Published Interfaces: 0
Classes generated: 0
Classes skipped: 0
Synonyms Created: 0
Errors: 0

Javaの実装をラップするPL/SQLファンクションを定義する


PL/SQLからファンクションとして呼び出せるよう、Wrapperとなるファンクションを設定します。
create or replace function sha256withrsa_sign
(
  header      varchar2,
  payload     varchar2,
  private_key varchar2
) return varchar2
as
language java name 'SHA256withRSA.sign (java.lang.String, java.lang.String, java.lang.String) return java.lang.String';
/
これで、SHA256WITHRSA_SIGNとしてPL/SQLのコード中からRSAによるデジタル署名を実行できるようになりました。

sun.security.utilというパッケージに含まれるクラスは、Oracle Databaseのデフォルトでは、一般のスキーマからのアクセスに制限が設けられている模様です。ですので、sysやsystemといったDBA権限のあるユーザーにて、これらの実行をMYWORKSPACEにたいして許可する必要がありました。そのために使用したコマンドが以下になります。
dbms_java.grant_permission( 'MYWORKSPACE', 'SYS:java.lang.RuntimePermission', 'accessClassInPackage.sun.security.util', '' );

JWTをPL/SQLコードで生成する


実装したデジタル署名アルゴリズムを指定したうえで、PL/SQLのコードを使ってJWTを生成します。このコード自体は、こちらの記事とほぼ同じものになります。
set lines 1000
set serveroutput on
declare
  l_now       timestamp;
  l_secret    varchar2(32767) := 'MIIEpAIBAAKCAQEA+ARMJiUjN+3kWFckXnxQkbHcbnKxoB46cHyuI+p7f7itiE4x8gJ6A9ML1alo6uCnHn8D0vaDJ/DVzL9whTS8zXJTB/WzGs35DsFrb23RIqrQCQHwCigi/rePzuaco225Rdc6yeMzTPnYhx36Vhhw/wR43oMtd2rBLntK8qOQgC5e8XFA5AxBCY6h0vPlgcF8fV3v+m1MonoC+9htXY8j6hg2pflcu2nTsJes8pysWYqxDjK32Vf2Mo/7SoK21zpjwUQonzllYPmuPguLhJcpGnCHKH2rU2jUBxfQtOirNYmuQ+qyZ4bgbzWj3iwoe584OHUw5BJ00kPiUA5dgaetswIDAQABAoIBAQCVCFgNz+Ei2ZOONgcHs+HiOqN/xvHPinqJX5JwyJUfgYTdVEeU6kIRjD8fS+ZcxVQG8kIFAdZ8KK8AksT1dmRBfAJC1TSj1cNkR3vcgHvcPhI2Z3CrWcT1lK4YoODdH7DmM9gCusD372ZagvwLwjIcO765zs+CY6bj3HiD5x+MtaHIGrrArKFb2rTBSt9HSrSov5lvlHT0GQJGYyfGEKQzcviv8pwflOUv8LlfTHRpOKl+V+mMsT4ljdqzc4cauSrU+nObBZKWJH+vdGZE78E7NnTP4bVQ/Sz0R6JMawL0lfQ2ty4mrceQ/92LpKRyIEHkUYOr5i+ln+DqGzvIbUyhAoGBAP15KZ5DkF4CfD0+I5v00xDmMLwWx0+8f3eBVEznq7SHLQUMQgXLjUtDWfIPzspNOmyn8J9nOhsIP9f92kYxGNcsIC7IoYhGdM1chGeT3yeOxc4pa0t13JpVZPAe5lsD6OCkEn6aH6fCc2XD/I6DvlsbOLYRakz3C+Oety3qRxERAoGBAPp9NeBmQW7pKy6lVy8ZOIcDezi+eOB0YzO3y2D4kt8ZO7w56I/IjwjINggCRlWeKROyaqJHzjs51iIOS9eIQlCnEubNKouyztflvwjI/lQSI+gXS85TVdrQoZn4OyVCkbxzs/MLWQxp6QqF850EwGFQJxzFAav+xF8suI8eqtKDAoGAJvhl6atBmvKO32uaVfQCL2r0uzZVVnEIt/ruVxSXVZ4i0c/cpr9w7c1hGtJLXNca0HHRkPByAo32IuMol/occ5iZKhg2nZkXhCthP/uAJ7jPuK6tjGCGlkPizPgzOnMTbtZK8aQIkeZXj6HeRpsCvUcmJPfwe/zPmyNfznNikcECgYAZqlip3HBZ5+Q3zFMpjUEAfHfUsZd7EBHWC7/3+KA74b8AI0LT8K3PLYxzt5/zR9hXn1FTvV3BLcTmCmb0944r62KBaYYxeCm/b1fqk4WToaQyNKjcxCco23WjbA7LW43JLGXEwmXmYrexD1aNwR3hGrBXP0xTjjAavF9Qssi39wKBgQCahtl16QWS/2Gh65X2jJGjKychgUd1RbZTE7v9gNh4oR+uciFdyoJ5Zbj3mpbKFtA0SFBGTZr86CKYqlO4712XPqSjtnw8Fpc8CnyO9mSxmn1BfSB+8FNdvgUKcDBQTno5PK5KfDNosylhUK7mROHawX/GXAtT2OsNXA451TjNVg==';
  l_username  varchar2(32) := 'TESTUSER';
  l_jwt       varchar2(32767);
  l_jwt_token apex_jwt.t_token;
  l_jwt_t     apex_t_varchar2;

  l_header_json json_object_t;
  l_header_str  varchar2(200);
  l_header_base64 varchar2(400);
  l_payload_json json_object_t;
  l_payload_str  varchar2(200);
  l_payload_base64 varchar2(800);
  l_token varchar2(1000);
  l_hmac varchar2(1000);

  -- Unix時間の取得
  function unixtime(p_timestamp in timestamp)
  return pls_integer
  is
    l_date date;
    l_epoc number;
  begin
    l_date := sys_extract_utc(p_timestamp);
    l_epoc := l_date - date'1970-01-01';
    return l_epoc * 24 * 60 * 60;
  end unixtime;

  -- Base64のデコード
  function from_base64(t in varchar2) return varchar2 is
  begin
    return utl_raw.cast_to_varchar2(utl_encode.base64_decode(utl_raw.cast_to_raw(t)));
  end from_base64;

  -- Base64のエンコード
  function to_base64(t in varchar2) return varchar2 is
    l_base64 varchar2(32767);
  begin
    l_base64 := utl_raw.cast_to_varchar2(utl_encode.base64_encode(utl_raw.cast_to_raw(t)));
    l_base64 := replace(l_base64, chr(13)||chr(10), '');
    return l_base64;
  end to_base64;
begin
  -- 共通で使用する現在時刻
  l_now := current_timestamp;
  dbms_output.put_line('Current Timestamp = ' || l_now || ', unixtime = ' || unixtime(l_now));

  -- ヘッダーを手作業で生成する。 
  dbms_output.put_line('Hand made ==================================');
  l_header_json := json_object_t();
  l_header_json.put('alg','RS256');
  l_header_json.put('typ','JWT');
  l_header_str := l_header_json.to_string();
  l_header_base64 := to_base64(l_header_str);
  dbms_output.put_line('Header    = ' || l_header_str);

  -- ペイロードを手作業で作成する。
  l_payload_json := json_object_t();
  l_payload_json.put('iss','sqlplus');
  l_payload_json.put('sub',l_username);
  l_payload_json.put('aud','APEX');
  l_payload_json.put('iat',unixtime(l_now));
  l_payload_json.put('exp',unixtime(l_now)+10);
  l_payload_str := l_payload_json.to_string();
  l_payload_base64 := to_base64(l_payload_str);
  dbms_output.put_line('Payload   = ' || l_payload_str);

  -- シグネチャを手作業で作成する。
  l_hmac := SHA256withRSA_sign(l_header_base64, l_payload_base64, l_secret);
  l_hmac := trim(translate(l_hmac, '+/=', '-_ '));
  dbms_output.put_line('JWT = ' || l_header_base64 || '.' || l_payload_base64 || '.' || l_hmac);
end;
/

exit;
上記のコードを実行すると以下のような出力がされます。
Current Timestamp = 17-MAR-20 03.48.50.702398 PM, unixtime = 1584427730
Hand made ==================================
Header   = {"alg":"RS256","typ":"JWT"}
Payload   = {"iss":"sqlplus","sub":"TESTUSER","aud":"APEX","iat":1584427730,"exp":1584427740}
JWT = eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzcWxwbHVzIiwic3ViIjoiVEVTVFVTRVIiLCJhdWQiOiJBUEVYIiwiaWF0IjoxNTg0NDI3NzMwLCJleHAiOjE1ODQ0Mjc3NDB9.DqFIbu-a3lD7QVgLjiIraRFUT-0jV7bNrg-wRDrQ9CUSoGxATWx2d43yJ2CrJxgDY-h62ogQgnFmh8vAn54Se7wXii9U9LO7wcclm96KP2PRyve0c1LDYmVt2-2_cGCQqas5J1yDF2E5Twxr4o9oeZcVCBkfjS22cZzz4VoiowvpbJw_HCbh0iU_bXv_l1dqSctRZWNKugaLEcPnERzIoXeceORb5YR8icB8A9z10l750MdsfxEIQAdtuFGYhJkQi4_a2cPGp0adJIt_Y0X2aDGfoz1sKdTbIbbIbcOBStspOVI5v-PCc01yNMyf1jJk-4zoliSmV6GIWyfl5ewawQ
https://jwt.ioにはJWTのデバッガーがありますので、生成されたデータと鍵を入力し、JWTのデジタル署名の検証が可能です。

RSASSA-PKCS1-v1_5 with SHA-256によるデジタル署名の生成についてはこちらの記事、PKCS#1の鍵フォーマットから、秘密鍵を取り出すためにこちらの記事を参照させていただきました。