Skip to content

Instantly share code, notes, and snippets.

@divergentdave
Created March 19, 2018 01:57
Show Gist options
  • Save divergentdave/7cd98ee16919a1a159ded8bc3160c8da to your computer and use it in GitHub Desktop.
Save divergentdave/7cd98ee16919a1a159ded8bc3160c8da to your computer and use it in GitHub Desktop.
Verify a self signed certificate's signature
#!/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()
@ricky-andre
Copy link

ricky-andre commented Oct 9, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment