JOSE 実装チュートリアル

OPTiM Store との連携には、Tenant Contract Setup をはじめとして様々な場面で以下の4つの署名付き JSON データを扱うための仕様群が登場します。

  • JSON Web Token (JWT)
  • JSON Web Signature (JWS)
  • JSON Web Key (JWK)
  • JSON Web Key Thumbprint (JWK Thumbprint)

各仕様の RFC およびその翻訳版、各言語ごとのライブラリ等は Tenant Contract API 冒頭で紹介しているのでそちらをご覧ください。

このチュートリアルでは、主要ライブラリを用いて署名付き JSON データの生成・署名検証のサンプルコードをご紹介します。

目次

Ruby の場合

Ruby では、上記4つの仕様をすべてサポートしているライブラリとして json-jwt を利用します。このライブラリの詳しい使い方は JSON::JWT Wiki にも記載されています。

JWK 生成

Contract API で contract_jwk を生成する際には、RSA 公開鍵を JWK 形式にエンコードします。

public_key = OpenSSL::PKey::RSA.new <<-PEM
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApG82+BzmQMLPTYD4G+ZD
   :
 (中略)
   :
LQIDAQAB
-----END PUBLIC KEY-----
PEM

# NOTE:
#  json-jwt gem では JSON::JWK インスタンスを生成すると
#  自動的に JWK Thumbprint 値が kid にセットされます。
public_jwk = JSON::JWK.new public_key

p public_jwk
# => {"kty": "RSA", "e": "AQAB", "n": "pG82-..(中略)..wgGLQ", "kid": "2wKYU..(中略)..yaFzY"}

JWK Thumbprint 生成

json-jwt gem では JSON::JWK インスタンスを利用する限り自動的に JWK Thumbprint を kid として利用するようになっているため、明示的に JWK Thumbprint 値を計算する必要はありません。

署名付き JWT 生成

OpenID Connect Client Registration API へのリクエスト時には、software_statement 生成のため署名付き JWT を生成します。

private_key = OpenSSL::PKey::RSA.new <<-PEM
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEApG82+BzmQMLPTYD4G+ZD0nMOFfoxzJUmZaaZCId4eXem6mwX
   :
 (中略)
   :
oMLvCmb7301JqWQRpYJ6WcyPchuMJ0x70WMqENXoBFYi1Sfnt864uXo=
-----END RSA PRIVATE KEY-----
PEM
private_jwk = JSON::JWK.new private_key

payload = {
  foo: 'bar',
  hoge: 'fuga'
}

# NOTE:
#  json-jwt gem では JSON::JWK インスタンスで署名を行うと
#  自動的に当該 JWK の JWK Thumbprint 値が JWT Header の kid にセットされます。
jwt = JSON::JWT.new payload

signed_jwt = jwt.sign private_jwk
puts singed_jwt.to_s # => "eyJ0eXAi..(中略).._pBTA"

署名付き JWT の署名検証

Contract API Request や SCIM Client Registration Request の検証時には、署名付き JWT の署名検証を行います。

jwt = JSON::JWT.decode jwt_string, :skip_verification
jwt.kid # => "2wKYU..(中略)..yaFzY"

public_jwk_components = {
  kty: 'RSA',
  e: 'AQAB',
  n: 'pG82-..(中略)..wgGLQ',
  kid: '2wKYU..(中略)..yaFzY'
}

public_jwk = JSON::JWK.new public_jwk_components
jwt = JSON::JWT.decode jwt_string, public_key

PHP の場合

Ruby では、上記4つの仕様をすべてサポートしているライブラリとして jose-php を利用します。このライブラリの詳しい使い方は jose-php README にも記載されています。

JWK 生成

Contract API で contract_jwk を生成する際には、RSA 公開鍵を JWK 形式にエンコードします。

<?php
$public_key = new phpseclib\Crypt\RSA();
$public_key->loadKey('-----BEGIN RSA PUBLIC KEY-----\n...');
$public_jwk = JOSE_JWK::encode($public_key);

JWK Thumbprint 生成

jose-php では、JOSE_JWK インスタンスを生成した時点で kid に JWK Thumbprint 値が自動的にセットされます。

<?php
$public_jwk = JOSE_JWK::encode($public_key);
echo $public_jwk->components['kid'];

署名付き JWT 生成

OpenID Connect Client Registration API へのリクエスト時には、software_statement 生成のため署名付き JWT を生成します。

署名生成時には、署名生成を行う前に 鍵の kid 値を JWT Header に指定する必要があります。(署名生成後に JWT Header に変更を加えると、署名検証時にエラーになります)

なお jose-php は JWK 形式の RSA Private Key による署名生成をサポートしていないため、署名用の秘密鍵として JOSE_JWK インスタンス自体を指定することができません。

<?php
$private_key = new phpseclib\Crypt\RSA();
$private_key->loadKey('-----BEGIN RSA PRIVATE KEY-----\n...');
$private_jwk = JOSE_JWK::encode($private_key);

$payload = array(
  foo: 'bar',
  hoge: 'fuga'
);

$jwt = new JOSE_JWT($payload);
$jwt->header['kid'] = $private_jwk->components['kid']; # NOTE: JOSE_JWT::sign() を呼ぶ前に!
$jwt->sign($private_key, 'RS256'); # NOTE: $private_jwk ではなく $private_key を指定
echo $jwt->toString(); # => "eyJ0eXAi..(中略).._pBTA"

署名付き JWT の署名検証

Contract API Request や SCIM Client Registration Request の検証時には、署名付き JWT の署名検証を行います。

<?php
$jwt_string = 'eyJ0eXAi..(中略).._pBTA';
$jwt = JOSE_JWT::decode($jwt_string);
echo $jwt->header['kid']; # => "2wKYU..(中略)..yaFzY";

$public_jwk_components = array(
  kty: 'RSA',
  e: 'AQAB',
  n: 'pG82-..(中略)..wgGLQ',
  kid: '2wKYU..(中略)..yaFzY'
);

$public_jwk = new JOSE_JWK($public_jwk_components);
$jwt->verify($public_jwk, 'RS256');

Java

Java では、上記4つの仕様をすべてサポートしているライブラリとして nimbus-jose-jwt を利用します。このライブラリの詳しい使い方は Nimbus JOSE + JWT examples にも記載されています。

JWK 生成

Contract API で contract_jwk を生成する際には、RSA 公開鍵を JWK 形式にエンコードします。

import java.security.*;
import java.security.interfaces.*;

import com.nimbusds.jose.jwk.RSAKey;

public class JWK {
  public static void main(String[] args) throws Exception {
    // NOTE: 実際には DB などから適宜鍵を取得するようにしてください。
    KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
    keyGenerator.initialize(2048);
    KeyPair kp = keyGenerator.genKeyPair();
    RSAPublicKey publicKey = (RSAPublicKey)kp.getPublic();

    RSAKey jwk = new RSAKey.Builder(publicKey)
      .keyIDFromThumbprint()
      .build();

    System.out.println(jwk.toJSONObject().toJSONString());
  }
}

JWK Thumbprint 生成

nimbus-jose-jwt では RSAKey.Builder.keyIDFromThumbprint() を呼び出すことでに JWK Thumbprint を当該 JWK の kid として利用するようになっています。

署名付き JWT 生成

OpenID Connect Client Registration API へのリクエスト時には、software_statement 生成のため署名付き JWT を生成します。

import java.security.*;
import java.security.interfaces.*;
import java.util.*;
import javax.crypto.*;

import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;

public class JWTSign {
  public static void main(String[] args) throws Exception {
    // NOTE: 実際には DB などから適宜鍵を取得するようにしてください。
    KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
    keyGenerator.initialize(2048);
    KeyPair kp = keyGenerator.genKeyPair();
    RSAPublicKey publicKey = (RSAPublicKey)kp.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey)kp.getPrivate();

    JWTClaimsSet payload = new JWTClaimsSet.Builder()
      .claim("foo", "bar")
      .claim("hoge", "fuga")
      .build();

    RSAKey jwk = new RSAKey.Builder(publicKey)
      .keyIDFromThumbprint()
      .build();

    JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
      .keyID(jwk.getKeyID())
      .type(JOSEObjectType.JWT)
      .build();

    JWSSigner signer = new RSASSASigner(privateKey);
    SignedJWT signedJWT = new SignedJWT(header, payload);
    signedJWT.sign(signer);

    System.out.println(signedJWT.serialize());
  }
}

署名付き JWT の署名検証

Contract API Request や SCIM Client Registration Request の検証時には、署名付き JWT の署名検証を行います。

import java.security.*;
import java.security.interfaces.*;
import java.util.*;
import javax.crypto.*;

import net.minidev.json.JSONObject;

import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;

public class JWTVerify {
  public static void main(String[] args) throws Exception {
    String jwkString = "{\"kty\":\"RSA\",\"e\":\"AQAB\",...}";
    JSONObject pubicJwkComponents = (JSONObject) JSONSerializer.toJSON(jwkString);
    RSAKey rsaKey = RSAKey.parse(pubicJwkComponents);
    RSAPublicKey publicKey = (RSAPublicKey) rsaKey.toPublicKey();

    String signedJWTString = "eyJ0eXAi..(中略).._pBTA";
    SignedJWT signedJWT = SignedJWT.parse(signedJWTString);
    JWSVerifier verifier = new RSASSAVerifier(publicKey);

    if (signedJWT.verify(verifier)) {
      System.out.println("Verified!");
      System.out.println(signedJWT.getJWTClaimsSet().toJSONObject().toJSONString());
    } else {
      System.out.println("Verification Failed!");
    }
  }
}

JavaScript

Java では、上記4つの仕様をすべてサポートしているライブラリとして jsrsasign を利用します。このライブラリの詳しい使い方は jsrsasign wiki にも記載されています。

JWK 生成

Contract API で contract_jwk を生成する際には、RSA 公開鍵を JWK 形式にエンコードします。

var pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9...(後略)";

var pubKey = KEYUTIL.getKey(pem);
var jwk = {
  kty: pubKey.type,
  e: hextob64u(pubKey.e.toString(16)),
  n: hextob64u(pubKey.n.toString(16))
}
jwk.kid = KJUR.jws.JWS.getJWKthumbprint(jwk);
console.info(jwk);

JWK Thumbprint 生成

上記 JWK 生成のサンプルにある通り、KJUR.jws.JWS.getJWKthumbprint(jwk) を利用することで JWK Thumbprint が得られます。

署名付き JWT 生成

penID Connect Client Registration API へのリクエスト時には、software_statement 生成のため署名付き JWT を生成します。

var pem = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQE...(後略)"
var prvKey = KEYUTIL.getKey(pem);
var jwk = {
  kty: prvKey.type,
  e: hextob64u(prvKey.e.toString(16)),
  n: hextob64u(prvKey.n.toString(16))
}

var header = {
  alg: 'RS256',
  typ: 'JWT',
  kid: KJUR.jws.JWS.getJWKthumbprint(jwk)
};

var payload = {
  foo: 'bar',
  hoge: 'fuga'
};

var jwtString = KJUR.jws.JWS.sign("RS256", header, payload, prvKey);
console.info(jwtString);

署名付き JWT の署名検証

Contract API Request や SCIM Client Registration Request の検証時には、署名付き JWT の署名検証を行います。

var jwtString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXV...(後略)";
var public_jwk_component = {
  kty: 'RSA',
  e: 'AQAB',
  n: 'pG82-..(中略)..wgGLQ',
  kid: '2wKYU..(中略)..yaFzY'
};

var pubKey = KEYUTIL.getKey(pem);
var isValid = KJUR.jws.JWS.verify(jwtString, pubKey, 'RS256');
if (isValid) {
  var verifiedJWT = KJUR.jws.JWS.parse(jwtString)
  console.info(verifiedJWT.headerObj);
  console.info(verifiedJWT.payloadObj);
} else {
  console.info('JWT signature invalid.');
}