💡

Python:Authlibを使用して,pem形式の証明書からJWK(JWKS)を生成する.

2024/10/22に公開

はじめに

  • pem形式の公開鍵証明書ファイルから,JWKを生成するプログラムをpython3で記述しました.
  • この記事で紹介するプログラムは,jwk中のx5c(証明書チェーン)の設定も加味しています.ですので,実行する際はサーバー証明書,中間証明書,ルート証明書を用意していることを前提としています.
  • また,kidは,Authlibを使用してRFC 7638で策定されたJSON Web Key (JWK) Thumbprintになるよう設定しています.
  • ※私の方で試した証明書の公開鍵はRSA公開鍵です.

プログラムについて

ライブラリ(インストールする必要があるもの)

  • pyca/cryptography(バージョン43.0.1を使用している)
  • Authlib(バージョン1.3.2を使用している.)

ライブラリのインストール

pipを利用してインストールを行う.

  • pyca/cryptography
$ pip install cryptography
  • Authlib
$ pip install Authlib

プログラム詳細

  • プログラムの使用方法
$ python3 pem_to_jwks.py <サーバー証明書のファイルパス> <中間証明書のファイルパス> <ルート証明書のファイルパス>
# pem_to_jwks.pyと同じディレクトリ配下に,jwks.jsonファイルが作成されます.
  • プログラムの使用例(各証明書ファイルが,pem_to_jwks.pyと同じディレクトリ配下にある場合)
$ python3 pem_to_jwks.py server.cer intermediate.cer root.cer
# pem_to_jwks.pyと同じディレクトリ配下に,```jwks.json```ファイルが保存されます.
pem_to_jwks.py
import argparse, json
from authlib.jose import JsonWebKey

from cryptography import x509
from cryptography.hazmat.primitives import serialization


def generate_x5c_in_jwk(cert_data_list):
    x5c = []
    for cert_data in cert_data_list:
        # Read the certificate, remove the header/footer, and join lines
        cert = cert_data.splitlines()
        cert = [line for line in cert if not line.startswith('-----')]  # Remove header/footer
        cert_data_without_header_and_footer = ''.join(cert)
        x5c.append(cert_data_without_header_and_footer)
    return x5c

def pem_to_jwk(public_key_data, cert_data_list, use="sig", ext=True, alg="RS256"):
    # Create a JsonWebKey object from the public key data
    jwk = JsonWebKey.import_key(public_key_data)
    jwk_as_dict = jwk.as_dict()

    # Add the use, ext, and alg fields
    jwk_as_dict["use"] = use
    jwk_as_dict["ext"] = ext
    jwk_as_dict["alg"] = alg

    # Add the x5c field to the JWK
    x5c = generate_x5c_in_jwk(cert_data_list)
    jwk_as_dict["x5c"] = x5c

    return jwk_as_dict

def extract_public_key_from_cert(pem_data):
    # Extract the public key from the certificate pem data
    pem_byte_data = pem_data.encode()
    cert = x509.load_pem_x509_certificate(pem_byte_data)
    public_key = cert.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    return public_key

def get_pem_data_from_cert(cert_file_name):
    with open(cert_file_name, "r") as cert_file:
        pem_data = cert_file.read()
    return pem_data

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Convert a PEM-encoded public key to JWK format.')
    parser.add_argument('server_cert', help='Path to the server certificate file')
    parser.add_argument('intermediate_cert', help='Path to the intermediate certificate file')
    parser.add_argument('root_cert', help='Path to the root certificate file')
    args = parser.parse_args()

    # get the cert data 
    server_cert = get_pem_data_from_cert(args.server_cert)
    intermediate_cert = get_pem_data_from_cert(args.intermediate_cert)
    root_cert = get_pem_data_from_cert(args.root_cert)

    cert_data_list = [server_cert, intermediate_cert, root_cert]

    # Extract the public key from the server certificate
    public_key_data = extract_public_key_from_cert(server_cert)
    # print(public_key_data,"\n")

    # Convert the public key to JWK format
    jwk = pem_to_jwk(public_key_data, cert_data_list)
    print(jwk)
    
    # Generate the JWKS
    keys = [jwk]
    jwks = {"keys": keys}

    # sava jwks json file
    with open("jwks.json", "w") as jwks_file:
        json.dump(jwks, jwks_file, indent=2)

解説

証明書データを読み込む

  • argparseを用いて,コマンドラインから,各証明書のファイルパスを取得します.
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Convert a PEM-encoded public key to JWK format.')
    parser.add_argument('server_cert', help='Path to the server certificate file')
    parser.add_argument('intermediate_cert', help='Path to the intermediate certificate file')
    parser.add_argument('root_cert', help='Path to the root certificate file')
    args = parser.parse_args()
  • その後,ファイルパスを元に,pem形式の証明書データを読み込みます.
    cert_data_listは,x5cの作成時に利用します.
def get_pem_data_from_cert(cert_file_name):
    with open(cert_file_name, "r") as cert_file:
        pem_data = cert_file.read()
    return pem_data

if __name__ == "__main__":

    # ....

    # get the cert data 
    server_cert = get_pem_data_from_cert(args.server_cert)
    intermediate_cert = get_pem_data_from_cert(args.intermediate_cert)
    root_cert = get_pem_data_from_cert(args.root_cert)

    cert_data_list = [server_cert, intermediate_cert, root_cert]

サーバー証明書から公開鍵データのみを抽出

  • pyca/cryptographyライブラリを利用して,pem形式の公開鍵データを取得します.

使用するメソッドやクラス

  • cryptography.x509.load_pem_x509_certificate

https://cryptography.io/en/43.0.1/x509/reference/#cryptography.x509.load_pem_x509_certificate

  • cryptography.x509.Certificate

https://cryptography.io/en/43.0.1/x509/reference/#cryptography.x509.Certificate

  • cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey

https://cryptography.io/en/43.0.1/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey

プログラム部分

from cryptography import x509
from cryptography.hazmat.primitives import serialization

# ....

def extract_public_key_from_cert(pem_data):
    # Extract the public key from the certificate pem data

    """読み込んだサーバ証明書のデータをバイナリ化する."""
    pem_byte_data = pem_data.encode()

    """
    バイナリ化した証明書データを引数として, 
    cryptography.x509.load_pem_x509_certificateメソッドから,
    cryptography.x509.Certificateクラスの証明書データを取得します.
    """
    cert = x509.load_pem_x509_certificate(pem_byte_data)

    """
    cryptography.x509.Certificateクラスには,public_key()メソッド
    があり,公開鍵データを取得できる.
    (私の場合だと
    cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyのインスタンス
   を取得している.)
    その後,public_bytes()メソッドを用いてPEM形式の公開鍵データを取得する.
    """
    public_key = cert.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    return public_key

# ....

if __name__ == "__main__":

    # ....

    # Extract the public key from the server certificate
    public_key_data = extract_public_key_from_cert(server_cert)
    # print(public_key_data,"\n")

jwk(jwks)生成

  • Authlibを使用して,pem形式の公開鍵データpublic_key_dataと各証明書データを格納しているリストcert_data_listからjwk(jwks)を生成します.

使用するメソッドやクラス

  • JSON Web Key (JWK)

https://docs.authlib.org/en/latest/jose/jwk.html

https://docs.authlib.org/en/latest/specs/rfc7517.html

プログラム部分

import argparse, json
from authlib.jose import JsonWebKey


def generate_x5c_in_jwk(cert_data_list):
    """
    cert_data_listから,x5cデータを生成する.
    """
    x5c = []
    """
    各証明書データに対して,
    最初の行の「-----BEGIN CERTIFICATE-----」と
    最後の行の「-----END CERTIFICATE-----」を
    取り除く操作を行う.取り除いたら,x5cリストに追加する.
    具体的な処理手順:
    - 行毎にデータを分割してリスト変換.
    - '-----'で始まる要素だけを取り除き,''.join(cert)で再度結合する.
    """
    for cert_data in cert_data_list:
        # Read the certificate, remove the header/footer, and join lines
        cert = cert_data.splitlines()
        cert = [line for line in cert if not line.startswith('-----')]  # Remove header/footer
        cert_data_without_header_and_footer = ''.join(cert)
        x5c.append(cert_data_without_header_and_footer)
    return x5c

def pem_to_jwk(public_key_data, cert_data_list, use="sig", ext=True, alg="RS256"):
    # Create a JsonWebKey object from the public key data
    """
    PEM形式のデータをインポートし,jwkを生成する.
    この時点でkidは,jwk thumbprintに設定される.
    as_dict()メソッドを使用して,dict型のデータに直す.
    """
    jwk = JsonWebKey.import_key(public_key_data)
    jwk_as_dict = jwk.as_dict()

    """
    以下,公開鍵以外のjwkフィールド値を設定している.
    """

    # Add the use, ext, and alg fields
    jwk_as_dict["use"] = use
    jwk_as_dict["ext"] = ext
    jwk_as_dict["alg"] = alg

    # Add the x5c field to the JWK
    x5c = generate_x5c_in_jwk(cert_data_list)
    jwk_as_dict["x5c"] = x5c

    return jwk_as_dict

if __name__ == "__main__":
    # ....
    cert_data_list = [server_cert, intermediate_cert, root_cert]
    # ....
    public_key_data = extract_public_key_from_cert(server_cert)

    # Convert the public key to JWK format
    jwk = pem_to_jwk(public_key_data, cert_data_list)
    print(jwk)
    
    # Generate the JWKS
    keys = [jwk]
    jwks = {"keys": keys}

    # save jwks json file
    """
    jsonライブラリを使用して,jwksファイルを保存する.
    """
    with open("jwks.json", "w") as jwks_file:
        json.dump(jwks, jwks_file, indent=2)

参考資料

pyca/cryptography

  • pyca/cryptography lateset

https://cryptography.io/en/latest/

  • pyca/cryptography 43.0.1

https://cryptography.io/en/43.0.1/

X.509 Reference — Cryptography 43.0.1 documentation

https://cryptography.io/en/43.0.1/x509/reference/#x-509-reference

Authlib

  • Authlib latest(2024年10月22日時点では,バージョン1.3.2に相当)

https://docs.authlib.org/en/latest/

  • JSON Web Key (JWK) - Authlib 1.3.2 documentation

https://docs.authlib.org/en/latest/jose/jwk.html

Discussion