概要
AWSのKey Management Service(KMS)を使って署名を行うときに、その秘密鍵に対して証明書を発行してみたかったので試してみました。
そのためには証明書署名要求(Certificate Signing Request; CSR)を作成する必要があります。しかし、OpenSSLではローカルにある秘密鍵でしかCSRを発行できないので(たぶん)、Pythonモジュールであるpyasn1を使って発行してみました。
以下のように行います。
- pyasn1を使って署名するための情報を作成する
- その情報をKMSで署名する
- それらを組み合わせてCSRを出力する
これらの手順をPythonコードにまとめました。
ダミーの秘密鍵でOpenSSLなどでCSRを発行し、そのファイルをpyasn1でパースし、subjectPKInfoとsignatureを置換する方法(参考1)があり、実行可能でした(他の方法は見つけられなかったのでそもそも需要がないのかな…?)。ただ、ダミーで発行するというのも美しくないので、pyasn1ですべて構成する方法にしてみました。
セキュリティに関わるので、実際に使用する際は十分な検証を行って下さい。あくまでこのコードは自己署名CAで証明書を生成することのできるCSRの生成に成功した、程度のものです。
手順
ここではすでにKMS上に秘密鍵を生成してあるものとします。
CentOS 7.9 + python 3.6 + pyasn1 0.5.0 + pyasn1_modules 0.3.0 + boto3を使用しました。
(pyasn1がpython 3.6で動いたので楽でした; CentOS7でスミマセン)
Pythonによる実装
#!/usr/bin/env python3
# pyasn1とAWS KMSを使ったCSR生成
# 2023-10-23
import hashlib, textwrap
from base64 import b64encode
from pyasn1.type import univ, char
from pyasn1_modules.rfc2986 import CertificationRequest, CertificationRequestInfo
from pyasn1_modules.rfc2314 import SubjectPublicKeyInfo, SignatureAlgorithmIdentifier
from pyasn1_modules import rfc2459
from pyasn1.codec.der import decoder, encoder
import boto3
START_MARKER = "-----BEGIN CERTIFICATE REQUEST-----"
END_MARKER = "-----END CERTIFICATE REQUEST-----"
SIGNING_ALGORITHM_OID = {
"ECDSA_SHA_512": "1.2.840.10045.4.3.4",
"ECDSA_SHA_384": "1.2.840.10045.4.3.3",
"ECDSA_SHA_256": "1.2.840.10045.4.3.2",
"ECDSA_SHA_224": "1.2.840.10045.4.3.1",
"RSASSA_PKCS1_V1_5_SHA_512": "1.2.840.113549.1.1.13",
"RSASSA_PKCS1_V1_5_SHA_384": "1.2.840.113549.1.1.12",
"RSASSA_PKCS1_V1_5_SHA_256": "1.2.840.113549.1.1.11",
}
def create_subject(subject_str):
# DNを作る
# subject_str = "/C=JP/O=My Organization/CN=Happy World"
def _create_rdn(attribute_type, value):
# RDNを作る
DNTYPE = {
"C": rfc2459.id_at_countryName, # 国
"ST": rfc2459.id_at_stateOrProvinceName, # 都道府県
"L": rfc2459.id_at_localityName, # 市町村
"O": rfc2459.id_at_organizationName, # 組織名
"CN": rfc2459.id_at_commonName, # コモンネーム
}
attribute = rfc2459.AttributeTypeAndValue()
attribute["type"] = DNTYPE[attribute_type]
attribute["value"] = char.PrintableString(value)
rdn = rfc2459.RelativeDistinguishedName()
rdn.append(attribute)
return rdn
# DNを作る
dn = rfc2459.Name()
dn[0] = rfc2459.RDNSequence()
subject_str = subject_str.lstrip("/")
for rdn_str in subject_str.split("/"):
rdn_id_str, rdn_value = [x.strip() for x in rdn_str.split("=")]
dn[0].append(_create_rdn(rdn_id_str, rdn_value))
return dn
def generate_csr(subject_str, key_id, signing_algorithm):
# 内容を生成
cri = CertificationRequestInfo()
cri["version"] = 0
cri["subject"] = create_subject(subject_str)
# KMSから公開鍵を取得してsubjectPKInfoにセットする
kms = boto3.client("kms")
res = kms.get_public_key(KeyId=key_id)
cri["subjectPKInfo"] = decoder.decode(res["PublicKey"], SubjectPublicKeyInfo())[0]
# certificationRequestInfoをDERフォーマットに変換してKMSで署名する
der_bytes = encoder.encode(cri)
hash_algorithm = "".join(signing_algorithm.split("_")[-2:]).lower()
digest = hashlib.new(hash_algorithm, der_bytes)
res = kms.sign(
KeyId=key_id,
Message=digest.digest(),
MessageType="DIGEST",
SigningAlgorithm=signing_algorithm,
)
signature = res["Signature"]
# CSRに各種情報を格納する
csr = CertificationRequest()
csr["certificationRequestInfo"] = cri
sai = SignatureAlgorithmIdentifier()
sai["algorithm"] = univ.ObjectIdentifier(SIGNING_ALGORITHM_OID[signing_algorithm])
csr["signatureAlgorithm"] = sai
csr["signature"] = univ.BitString.fromOctetString(signature)
return csr
def write_csr(csr, filename):
# ファイルに書き出す
b64csr = b64encode(encoder.encode(csr)).decode()
s = "\n".join([START_MARKER, "\n".join(textwrap.wrap(b64csr)), END_MARKER])
open(filename, "w").write(s)
SUBJECT = "/C=JP/O=My Organization/CN=Happy Paradise"
KEY_ID = "12345678-abcd-ef00-0000-012345678912"
SIGNING_ALGORITHM = "ECDSA_SHA_256"
OUTFILE = "test.csr"
csr = generate_csr(SUBJECT, KEY_ID, SIGNING_ALGORITHM)
write_csr(csr, OUTFILE)
コード内容
create_subject(subject_str)
- subject_str -- DN文字列(/C=JP/ST=State/L=Locality/O=Organization/CN=Common Name)
subject_str
にDNを指定してsubjectに与えるオブジェクトを生成します。例えば、/C=JP/O=My Organization/CN=Happy Paradise
のように指定します。
generate_csr(subject_str, key_id, signing_algorithm)
- subject_str -- DN文字列
- key_id -- AWS KSMのKey ID
- signing_algorithm -- KMSのSigningAlgorithmパラメータ(ECDSA_SHA_256など)
DN文字列とAWS KMSを使用してCSRを生成します。コード中のコメント通り、以下のことを行います。
- Certification Request Info構造に必要なパラメータをセットする(version, subject, subjectPKInfo)
- Certification Request InfoをDER形式にエンコード後、ハッシュ化しKMSで署名する
- Certification Requestオブジェクトにパラメータをセットする
ここではpyasn1_modules.rfc2986.CertificationRequest
オブジェクトを返します。
write_csr(csr, filename)
- csr -- CertificationRequestオブジェクト
- filename -- 出力ファイル
CertificationRequestオブジェクトをPEM形式にエンコードし、整形してファイルに書き出します。
以上で、KMSで署名したCSRができあがります。確認はOpenSSL 3.0.9 30 May 2023
です。
$ ./generate_csr_with_kms.py
$ openssl req -in test.csr -text
Certificate Request:
Data:
Version: 1 (0x0)
Subject: C = JP, O = My Organization, CN = Happy Paradise
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:...
4a:d6:89:69:d5
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
(none)
Requested Extensions:
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:...
-----BEGIN CERTIFICATE REQUEST-----
MI...
-----END CERTIFICATE REQUEST-----
得られたCSRをCAで署名すれば証明書が作成可能です。
余談
pyasn1の使い方のページがほぼなくて、「参考」に挙げたGitHubをもとにいろいろ試行錯誤しました…。ASN.1をちゃんと理解していればいいのかもしれませんが。
Web上に資料が少ないせいか、ChatGPTによるコード生成でも、ビミョーに動かないコードが生成されて、あまり参考になりませんでした…
cryptographyを使ったコードはいくらでもあるのですが、最近のcryptographyはbackendがrustになったらしく(斜め読み)、backendの実装だけをいじるという手段ができなさそうだったので、pyasn1で構成しました。
このコードが参考になれば幸いです。
参考
- 発行済みCSRの署名を置き換える方法
- GitHub g-a-d/aws-kms-sign-csr -- https://github.com/g-a-d/aws-kms-sign-csr