概要
デジタル署名者の証明書をPythonで検証する手順を学習するために、asn1cryptoとPyCryptodomeを使用して実装してみました。簡単な実装ですので、あくまで実装検証です。
手順
証明書の検証
証明書はRFC5280で定義されており、以下のような構造になっています。
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present, version MUST be v3 }
TBSCertificateはTo Be Signed
Certificateであり、証明される内容です。
このTBSCertificateのDERフォーマットに対するCAの署名がsignatureValueです。つまり、以下の情報を検証すれば、この証明書が上位のCAから署名を受けているかがわかります。
- Subject (検証したい証明書)
- tbsCertificate -- これを検証したい
- signatureAlgorithm -- 署名スキーム
- signatureValue -- CAによる署名
この内容を検証するにはCAの公開鍵を入手する必要があります。証明書チェーンやOSバンドルの証明書から得ることができます。tbsCertificate.extensionsのauthorityKeyIdentifierからCA証明書の情報を取得することができます。
証明書の公開鍵はtbsCertificateに含まれています。
- Issuer (CA証明書)
- tbsCertificate
- subjectPublicKeyInfo -- 公開鍵
- tbsCertificate
これにより必要な情報が揃いました。
- Subject.tbsCertificate -- 検証したい内容
- Subject.signatureValue -- 署名
- Issuer.tbsCertificate.subjectPublicKeyInfo -- CAの公開鍵
これらを検証すれば内容が正しいかどうかがわかります。この検証を実装したのが以下です。
実装
環境
- CentOS 7.9 + Python 3.6
- asn1crypto 1.5.1
- PyCryptodome 3.19.0
コード
このコードでは上位証明書の有効期限、basicConstraints、keyUsageを確認しています。それ以外にもっと厳しく検証が必要な場合は追加などするとよさそうです。
from importlib import import_module
from datetime import datetime, timezone
class CertificateVerifier:
# 証明書の検証
SIGNATURE_SCHEME = {
"rsassa_pkcs1v15": (".RSA", ".pkcs1_15", {}),
"ecdsa": (".ECC", ".DSS", {"mode": "fips-186-3", "encoding": "der"}),
"ed25519": (".ECC", ".eddsa", {"mode": "rfc8032"}),
"ed448": (".ECC", ".eddsa", {"mode": "rfc8032"}),
}
def validate_certificate(self, cert):
# 証明書自身を検証
# 有効期限
if not cert.not_valid_before <= datetime.now(timezone.utc) <= cert.not_valid_after:
raise ValueError("Invalid validity.")
# basicConstraintsがCA:TRUE
if not cert.ca: raise ValueError("This cert is not for CA.")
# keyUsageにkeyCertSignが含まれる
if cert.key_usage_value and "key_cert_sign" not in cert.key_usage_value.native:
raise ValueError("This cert is not for 'key_cert_sign'.")
def verify(self, subject, issuer):
# 証明書を検証する
print("-", subject.subject.native["common_name"], "<=", issuer.subject.native["common_name"])
# issuerのプロパティを検証
self.validate_certificate(issuer)
# 署名を確認
sig_scheme = self.SIGNATURE_SCHEME[subject.signature_algo] # ex. rsassa_pkcs1v15
pk_algo = import_module(sig_scheme[0], "Crypto.PublicKey") # 公開鍵暗号方式
scheme = import_module(sig_scheme[1], "Crypto.Signature") # 署名スキーム
hash_algo = import_module(f".{subject.hash_algo.upper()}", "Crypto.Hash") # ex. sha384
# EdDSAではverify()に渡すメッセージはハッシュ化しない
msg = subject["tbs_certificate"].dump() # TBSCertificateをDERにする
if subject.signature_algo not in ("ed25519", "ed448"): msg = hash_algo.new(msg)
pubkey = pk_algo.import_key(issuer.public_key.dump())
verifier = scheme.new(pubkey, **sig_scheme[2])
verifier.verify(msg, subject.signature)
print(" - Verify OK")
if __name__ == "__main__":
# test.crtが検証したい証明書、ca.crtがCA証明書(PEM)
from asn1crypto.pem import unarmor
from asn1crypto.x509 import Certificate
subject = Certificate.load(unarmor(open("test.crt", "rb").read())[2])
issuer = Certificate.load(unarmor(open("ca.crt", "rb").read())[2])
v = CertificateVerifier()
v.verify(subject, issuer)
動作は、OpenSSL 3.0.9で生成した証明書と手元にあったタイムスタンプトークンで行いました。
以下の方式では多分動きます。
- RSASSA-PKCS1v1.5
- ECDSA(prime256v1)
- Ed25519
- Ed448
まとめ
はじめはpyasn1を使おうと思ったのですが、読み込みなどに癖が強く、イマイチ使いこなせなかったのでasn1cryptoを使いました。asn1cryptoは、pyasn1が不満で(?)作ったというようなことが書かれているだけあって、慣れてしまえば非常に使いやすかったです。
asn1cryptoのモジュールの使い方などはasn1cryptoのソースを読む必要があるので、ちょっと面倒かも。Certificateクラスの便利プロパティなどはソースを見て探しました。
参考
- PyPI - asn1crypto -- https://pypi.org/project/asn1crypto/
- PyCryptodome -- https://www.pycryptodome.org/
- RFC5280 -- https://www.rfc-editor.org/rfc/rfc5280.html