Created
March 19, 2018 01:57
-
-
Save divergentdave/7cd98ee16919a1a159ded8bc3160c8da to your computer and use it in GitHub Desktop.
Verify a self signed certificate's signature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
This script verifies whether a given X.509 certificate is self-signed, while | |
ignorng the subject and issuer distinguished names. Python 3 is required, | |
along with recent versions of pyasn1 and pyasn1-modules. To check a | |
certificate, run this script with the file name of that certificate as a | |
command line argument. Certificates can be in PEM or DER format. Only RSA | |
signatures are supported. | |
To verify whether a certificate is self-signed, this script parses the | |
certificate, calculates the appropriate hash over the to-be-signed portion, | |
parses the public modulus, public exponent, and signature into native Python | |
integers. The RSA signature verification is performed using the built-in pow() | |
function. Finally, the contents of the verified signature are parsed and | |
compared to the previously computed hash. "True" is printed for valid | |
self-signed certificates, and "False" is printed for all other certificates. | |
""" | |
import argparse | |
import hashlib | |
import pyasn1.codec.der.decoder | |
import pyasn1.codec.der.encoder | |
import pyasn1.type.univ | |
import pyasn1_modules.pem | |
import pyasn1_modules.rfc2315 | |
import pyasn1_modules.rfc2437 | |
import pyasn1_modules.rfc2459 | |
import pyasn1_modules.rfc5280 | |
HASH_OID_LOOKUP = { | |
pyasn1_modules.rfc2437.sha1WithRSAEncryption: | |
pyasn1_modules.rfc2437.id_sha1, | |
pyasn1.type.univ.ObjectIdentifier("1.2.840.113549.1.1.11"): | |
pyasn1.type.univ.ObjectIdentifier("2.16.840.1.101.3.4.2.1") | |
} | |
HASH_CONSTRUCTOR_LOOKUP = { | |
pyasn1_modules.rfc2437.sha1WithRSAEncryption: | |
hashlib.sha1, | |
pyasn1.type.univ.ObjectIdentifier("1.2.840.113549.1.1.11"): | |
hashlib.sha256, | |
} | |
def integer_to_bytes(n, k): | |
assert n >> (8 * k) == 0 | |
return bytes((n >> (8 * i)) & 0xff | |
for i in range(k - 1, -1, -1)) | |
def load_cert(path): | |
with open(path, "rb") as f: | |
data = f.read() | |
if data.startswith(b"-----BEGIN"): | |
# decode PEM | |
with open(path, "r") as f: | |
return pyasn1_modules.pem.readPemFromFile(f) | |
else: | |
# assume it's binary DER-encoded data | |
return data | |
def public_key_from_certificate(cert_data): | |
cert, _ = pyasn1.codec.der.decoder.decode( | |
cert_data, | |
pyasn1_modules.rfc5280.Certificate()) | |
tbs = cert["tbsCertificate"] | |
spki = tbs["subjectPublicKeyInfo"] | |
assert (spki["algorithm"]["algorithm"] == | |
pyasn1_modules.rfc2437.rsaEncryption) | |
pyasn1.codec.der.decoder.decode( | |
spki["algorithm"]["parameters"], | |
pyasn1.type.univ.Null()) | |
rsa_public_key, _ = pyasn1.codec.der.decoder.decode( | |
spki["subjectPublicKey"].asOctets(), | |
pyasn1_modules.rfc2437.RSAPublicKey()) | |
return (int(rsa_public_key["modulus"]), | |
int(rsa_public_key["publicExponent"])) | |
def parse_certificate(cert_data): | |
cert, _ = pyasn1.codec.der.decoder.decode( | |
cert_data, | |
pyasn1_modules.rfc5280.Certificate()) | |
algorithmIdentifier = cert["signatureAlgorithm"] | |
signature_algorithm = algorithmIdentifier["algorithm"] | |
pyasn1.codec.der.decoder.decode( | |
algorithmIdentifier["parameters"], | |
pyasn1.type.univ.Null()) | |
tbs_der = pyasn1.codec.der.encoder.encode(cert["tbsCertificate"]) | |
return (tbs_der, | |
int(cert["signature"]), | |
len(cert["signature"]), | |
signature_algorithm) | |
def pkcs1_15_unpad(padded): | |
assert padded.startswith(b"\x00\x01") | |
temp = padded[2:].lstrip(b"\xff") | |
assert temp.startswith(b"\x00") | |
return temp[1:] | |
def hash_tbs(tbs, signature_algorithm): | |
digest = HASH_CONSTRUCTOR_LOOKUP[signature_algorithm]() | |
digest.update(tbs) | |
return digest.digest() | |
def unwrap_hash(signature_contents, signature_algorithm): | |
digestInfo, _ = pyasn1.codec.der.decoder.decode( | |
signature_contents, | |
pyasn1_modules.rfc2315.DigestInfo()) | |
algorithmIdentifier = digestInfo["digestAlgorithm"] | |
assert (algorithmIdentifier["algorithm"] == | |
HASH_OID_LOOKUP[signature_algorithm]) | |
pyasn1.codec.der.decoder.decode( | |
algorithmIdentifier["parameters"], | |
pyasn1.type.univ.Null()) | |
return digestInfo["digest"] | |
def verify(path): | |
cert = load_cert(path) | |
( | |
tbscert, | |
signature, | |
bit_length, | |
signature_algorithm | |
) = parse_certificate(cert) | |
tbs_hash = hash_tbs(tbscert, signature_algorithm) | |
n, e = public_key_from_certificate(cert) | |
signed_int = pow(signature, e, n) | |
signed_bytes_padded = integer_to_bytes(signed_int, bit_length // 8) | |
signed_data = pkcs1_15_unpad(signed_bytes_padded) | |
hash_bytes = unwrap_hash(signed_data, signature_algorithm) | |
return tbs_hash == hash_bytes | |
def main(): | |
parser = argparse.ArgumentParser( | |
description="Self-signed certificate signature verifier") | |
parser.add_argument( | |
"certs", | |
nargs="+", | |
metavar="path", | |
help="Certificate file") | |
args = parser.parse_args() | |
for cert in args.certs: | |
print("{}: {}".format(cert, verify(cert))) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What is the unwrap_hash function used for ? as far as I have understood, the certificate is transformed into a sequence of 160bits with (for example) SHA-1, then it should be padded with pkcs1_15, and on this result the signature is calculated using the private key.
Once you calculate from the signature, using the pow function, the result should be like the previous one. So you should compare the unpadded versions. Why do you also apply that unwrap_hash function ? Your script works like a sharm, but I do not understand all the details.
Why should I find a certificate that gives an error because it doesn't seem to be padded ? when you call the pkcs1_15_unpad function I get an error because of the assert, the result doesn't start with b"\x00\x01"
It's also something you wrote 7 years ago so ... you probably forgot some details. Sorry for bothering you.