Merge pull request #711 from dod-ccpo/pdf-signature-verification

Verify PDF signatures
This commit is contained in:
George Drummond 2019-03-20 15:35:55 -04:00 committed by GitHub
commit 27314b8120
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 419 additions and 39 deletions

View File

@ -23,6 +23,7 @@ lockfile = "*"
defusedxml = "*"
"flask-rq2" = "*"
simplejson = "*"
asn1crypto = "*"
[dev-packages]
bandit = "*"
@ -39,6 +40,7 @@ pytest-cov = "*"
selenium = "*"
honcho = "*"
blinker = "*"
pytest-mock = "*"
[requires]
python_version = "3.6.6"

85
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "7aff94ddfb4f3f3ebf7f7910f3ade4eebd546b297cf72863a618824f87ec76fc"
"sha256": "975303153664e6936b5118686cb7056e8135e7c8184b7c0c029fa120c9e0b67e"
},
"pipfile-spec": 6,
"requires": {
@ -36,6 +36,7 @@
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
"sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
],
"index": "pypi",
"version": "==0.24.0"
},
"certifi": {
@ -94,10 +95,10 @@
},
"croniter": {
"hashes": [
"sha256:5389776e54a5e285d0c8e7b9a7e139a4d590f96f32958b0822d6d1b2faa12c0d",
"sha256:fbd72189a0ff38c27e953d15175c5fedafb953479559240a1afcf8e8e7523757"
"sha256:79a5eeaa10a7d5fb9bdae54211b8c1d306e0ed481fa970934bf3197940650d6f",
"sha256:c31adf6a9b0b1981d362538bfa57769acaade1d62f80c264f402ce1f8d1210b4"
],
"version": "==0.3.27"
"version": "==0.3.28"
},
"cryptography": {
"hashes": [
@ -209,9 +210,9 @@
},
"mako": {
"hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
"sha256:04092940c0df49b01f43daea4f5adcecd0e50ef6a4b222be5ac003d5d84b2843"
],
"version": "==1.0.7"
"version": "==1.0.8"
},
"markupsafe": {
"hashes": [
@ -333,11 +334,11 @@
},
"redis": {
"hashes": [
"sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54",
"sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775"
"sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175",
"sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273"
],
"index": "pypi",
"version": "==3.2.0"
"version": "==3.2.1"
},
"requests": {
"hashes": [
@ -417,10 +418,10 @@
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
"sha256:590abe38f8be026d78457fe3b5200895b3543e58ac3fc1dd792c6333ea11af64",
"sha256:ee11b0f0640c56fb491b43b38356c4b588b3202b415a1e03eacf1c5561c961cf"
],
"version": "==0.14.1"
"version": "==0.15.0"
},
"wtforms": {
"hashes": [
@ -491,11 +492,11 @@
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==18.9b0"
"version": "==19.3b0"
},
"blinker": {
"hashes": [
@ -554,10 +555,10 @@
},
"decorator": {
"hashes": [
"sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e",
"sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b"
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
],
"version": "==4.3.2"
"version": "==4.4.0"
},
"docopt": {
"hashes": [
@ -575,10 +576,10 @@
},
"faker": {
"hashes": [
"sha256:16342dca4d92bfc83bab6a7daf6650e0ab087605a66bc38f17523fdb01757910",
"sha256:d871ea315b2dcba9138b8344f2c131a76ac62d6227ca39f69b0c889fec97376c"
"sha256:00b7011757c4907546f17d0e47df098b542ea2b04c966ee0e80a493aae2c13c8",
"sha256:745ac8b9c9526e338696e07b7f2e206e5e317e5744e22fdd7c2894bf19af41f1"
],
"version": "==1.0.2"
"version": "==1.0.4"
},
"flask": {
"hashes": [
@ -612,10 +613,10 @@
},
"ipdb": {
"hashes": [
"sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a"
"sha256:dce2112557edfe759742ca2d0fee35c59c97b0cc7a05398b791079d78f1519ce"
],
"index": "pypi",
"version": "==0.11"
"version": "==0.12"
},
"ipython": {
"hashes": [
@ -851,6 +852,14 @@
"index": "pypi",
"version": "==0.14.0"
},
"pytest-mock": {
"hashes": [
"sha256:4d0d06d173eecf172703219a71dbd4ade0e13904e6bbce1ce660e2e0dc78b5c4",
"sha256:bfdf02789e3d197bd682a758cae0a4a18706566395fbe2803badcd1335e0173e"
],
"index": "pypi",
"version": "==1.10.1"
},
"pytest-watch": {
"hashes": [
"sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"
@ -867,19 +876,19 @@
},
"pyyaml": {
"hashes": [
"sha256:544a0050e76e9b60751c58617fa28c253ad5d23af2e5f0b1c250390bf90bb0df",
"sha256:594bf80477a58b6fd53e8b3f24ccf965c25eeeb6e05e4b1fb18c82c2d2090603",
"sha256:75e20ca689d0a2bf0c84f0e2028cc68ebef34b213fa66d73c410c53f870c49f4",
"sha256:994da68a1dc1050f290f8017f044172360b608c0f2562b47645ecc69d7a61c0a",
"sha256:ad902e00088c50bdced94a57b819c24fdadaeaed5494df7a9a67d63774f210fd",
"sha256:b11aff75875ffc73541c4e4b1ac2f5e21717c1fc4396238943b9a44d962e74e1",
"sha256:bc733b5a9047c3e4848c0e80eeacfa6a799139242606410260c5450d665ea58c",
"sha256:d960c68931b96bb215f385baa8ef867b8ebac66af60fa06cc1008f963848c7ad",
"sha256:dd461c04e6a91e4eef7d5b75c1fc1c7013d3f8d354033b16526baadddd524079",
"sha256:e4d6b5d6218a06f3141189d75c93876dd525a6d15f1b00ef4f274726c93719f1",
"sha256:f3c386fa12415bde8a0162745c4badf98fe171c6dfd67e54831f05ec88feeebb"
"sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c",
"sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95",
"sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2",
"sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4",
"sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad",
"sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba",
"sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1",
"sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e",
"sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673",
"sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13",
"sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19"
],
"version": "==5.1b5"
"version": "==5.1"
},
"selenium": {
"hashes": [
@ -978,10 +987,10 @@
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
"sha256:590abe38f8be026d78457fe3b5200895b3543e58ac3fc1dd792c6333ea11af64",
"sha256:ee11b0f0640c56fb491b43b38356c4b588b3202b415a1e03eacf1c5561c961cf"
],
"version": "==0.14.1"
"version": "==0.15.0"
},
"wrapt": {
"hashes": [

View File

@ -52,7 +52,7 @@ def get_current_user():
def logout():
if session.get("user_id"): # pragma: no branch
del (session["user_id"])
del session["user_id"]
def _unprotected_route(request):

View File

@ -0,0 +1,224 @@
import hashlib
from OpenSSL import crypto
from asn1crypto import cms, pem, core
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
class PDFSignature:
def __init__(self, byte_range_start=None, crl_check=None, pdf=None):
self._signers_cert = None
self._openssl_loaded_certificate = None
self.byte_range_start = byte_range_start
self.crl_check = crl_check
self.pdf = pdf
@property
def byte_range(self):
"""
This returns an array of 4 numbers that represent the byte range of
the PDF binary file that is signed by the certificate.
E.G: [0, 2045, 3012, 5012]
Bytes 0 to 2045 - represent part A of the signed file
Bytes 2046 to 3012 - would contain the signature and certificate information
Bytes 3013 to 5012 - represent part B of the signed file
"""
start = self.pdf.find(b"[", self.byte_range_start)
stop = self.pdf.find(b"]", start)
contents_range = [int(i, 10) for i in self.pdf[start + 1 : stop].split()]
return contents_range
@property
def signed_binary_data(self):
"""
This is the binary data stored in the signature
"""
br = self.byte_range
contents = self.pdf[br[0] + br[1] + 1 : br[2] - 1]
data = []
for i in range(0, len(contents), 2):
data.append(int(contents[i : i + 2], 16))
return cms.ContentInfo.load(bytes(data))["content"]
@property
def signers_cert(self):
"""
This returns the certificate used to sign the PDF
"""
if self._signers_cert is None:
for cert in self.signed_binary_data["certificates"]:
if (
self.signers_serial
== cert.native["tbs_certificate"]["serial_number"]
):
cert = cert.dump()
self._signers_cert = pem.armor("CERTIFICATE", cert)
break
return self._signers_cert
@property
def signers_serial(self):
"""
Return the signers serial from their certificate
"""
return self.signed_binary_data["signer_infos"][0]["sid"].native["serial_number"]
@property
def hashing_algorithm(self):
"""
This is the hashing algorithm used to generate the hash of binary file content
which is then signed by the certificate.
E.G. sha256, sha1
"""
return self.signed_binary_data["digest_algorithms"][0]["algorithm"].native
@property
def openssl_loaded_certificate(self):
if self._openssl_loaded_certificate is None:
self._openssl_loaded_certificate = crypto.load_certificate(
crypto.FILETYPE_PEM, self.signers_cert
)
return self._openssl_loaded_certificate
@property
def cert_common_name(self):
"""
This returns the common name on the certificate. This might be a name or
a DOD ID for example.
"""
return self.openssl_loaded_certificate.get_subject().commonName
@property
def encrypted_hash_of_signed_document(self):
"""
This is the calculated hash of the PDF binary data stored in the
signature. We calculate it outselves and then compare to this
so we can see if data has changed.
"""
stored_hash = None
for attr in self.signed_binary_data["signer_infos"][0]["signed_attrs"]:
if attr["type"].native == "message_digest":
stored_hash = attr["values"].native[0]
break
return stored_hash
@property
def binary_data(self):
"""
Take the byte range and return the binary data for that rage.
"""
br = self.byte_range
data1 = self.pdf[br[0] : br[0] + br[1]]
data2 = self.pdf[br[2] : br[2] + br[3]]
return data1 + data2
@property
def hashed_binary_data(self):
"""
Takes the data in the byte range and hashes it using
the hashing algorithm specified in the signed PDF. We
can later compare this to the encrypted_hash_of_signed_document.
"""
return getattr(hashlib, self.hashing_algorithm)(self.binary_data)
@property
def is_cert_valid(self):
"""
Takes the signing certificate and runs it through the CRLCache
checker. Returns a boolean.
"""
return self.crl_check(self.signers_cert)
@property
def is_signature_valid(self):
"""
Get signed PDF signature and determine if it was actually signed
by the certificate that it claims it was. Returns a boolean.
"""
public_key = self.openssl_loaded_certificate.get_pubkey().to_cryptography_key()
attrs = self.signed_binary_data["signer_infos"][0]["signed_attrs"]
signed_data = None
if attrs is not None and not isinstance(attrs, core.Void):
signed_data = attrs.dump()
signed_data = b"\x31" + signed_data[1:]
else:
signed_data = self.binary_data
try:
public_key.verify(
bytes(self.signed_binary_data["signer_infos"][0]["signature"]),
signed_data,
padding.PKCS1v15(),
getattr(hashes, self.hashing_algorithm.upper())(),
)
return True
except Exception:
return False
@property
def to_dict(self):
is_cert_valid = self.is_cert_valid
is_signature_valid = self.is_signature_valid
is_hash_valid = (
self.hashed_binary_data.digest() == self.encrypted_hash_of_signed_document
)
return {
"cert_common_name": self.cert_common_name,
"hashed_binary_data": self.hashed_binary_data.hexdigest(),
"hashing_algorithm": self.hashing_algorithm,
"is_valid": is_cert_valid and is_hash_valid and is_signature_valid,
"is_valid_cert": is_cert_valid,
"is_valid_hash": is_hash_valid,
"is_valid_signature": is_signature_valid,
"signers_serial": self.signers_serial,
}
def pdf_signature_validations(pdf=None, crl_check=None):
"""
As arguments we accept a pdf binary blob and a callable crl_check.
An example implementation of the crl_check can be found in the
tests (test/utils/test_pdf_verification.py)
"""
signatures = []
start_byte = 0
while True:
start = start_byte + 1
n = pdf.find(b"/ByteRange", start)
if n == -1:
break
signatures.append(
PDFSignature(byte_range_start=n, crl_check=crl_check, pdf=pdf)
)
start_byte = n
response = {"result": None, "signature_count": len(signatures), "signatures": []}
for signature in signatures:
sig = signature.to_dict
response["signatures"].append(sig)
if not sig["is_valid"]:
response["result"] = False
elif response["result"] is not False:
response["result"] = True
if len(signatures) == 0:
response["result"] = False
return response

BIN
tests/fixtures/sally-darth-signed.pdf vendored Normal file

Binary file not shown.

BIN
tests/fixtures/signed-expired-cert.pdf vendored Normal file

Binary file not shown.

BIN
tests/fixtures/signed-pdf-not-dod.pdf vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,145 @@
import pytest
import cryptography
from atst.domain.authnid.crl import CRLCache, CRLRevocationException
from atst.utils.pdf_verification import pdf_signature_validations
@pytest.fixture
def crl_check():
def _crl_check(signers_cert):
try:
cache = CRLCache(
"ssl/server-certs/ca-chain.pem",
crl_locations=["ssl/client-certs/client-ca.der.crl"],
)
return cache.crl_check(signers_cert)
except CRLRevocationException:
return False
return _crl_check
def test_unsigned_pdf(crl_check):
unsigned_pdf = open("tests/fixtures/sample.pdf", "rb").read()
result = pdf_signature_validations(pdf=unsigned_pdf, crl_check=crl_check)
assert result == {"result": False, "signature_count": 0, "signatures": []}
def test_valid_signed_pdf(crl_check):
valid_signed_pdf = open("tests/fixtures/sally-darth-signed.pdf", "rb").read()
result = pdf_signature_validations(pdf=valid_signed_pdf, crl_check=crl_check)
assert result == {
"result": True,
"signature_count": 2,
"signatures": [
{
"cert_common_name": "WILLIAMS.SALLY.3453453453",
"hashed_binary_data": "b879a15e19eece534dc63019d3fe539ff4a3efbf8e8f5403a8bdae26a9b713ea",
"hashing_algorithm": "sha256",
"is_valid": True,
"is_valid_cert": True,
"is_valid_hash": True,
"is_valid_signature": True,
"signers_serial": 9_662_248_800_192_484_626,
},
{
"cert_common_name": "VADER.DARTH.9012345678",
"hashed_binary_data": "d98339766c20a369219f236220d7b450111554acc902e242d015dd6d306c7809",
"hashing_algorithm": "sha256",
"is_valid": True,
"is_valid_cert": True,
"is_valid_hash": True,
"is_valid_signature": True,
"signers_serial": 9_662_248_800_192_484_627,
},
],
}
def test_signed_pdf_thats_been_modified(crl_check):
valid_signed_pdf = open("tests/fixtures/sally-darth-signed.pdf", "rb").read()
modified_pdf = valid_signed_pdf.replace(b"PDF-1.6", b"PDF-1.7")
result = pdf_signature_validations(pdf=modified_pdf, crl_check=crl_check)
assert result == {
"result": False,
"signature_count": 2,
"signatures": [
{
"cert_common_name": "WILLIAMS.SALLY.3453453453",
"hashed_binary_data": "d1fb3c955b57f139331586276ba4abca90ecc5d36b53fe6bbbbbd8707d7124bb",
"hashing_algorithm": "sha256",
"is_valid": False,
"is_valid_cert": True,
"is_valid_hash": False,
"is_valid_signature": True,
"signers_serial": 9_662_248_800_192_484_626,
},
{
"cert_common_name": "VADER.DARTH.9012345678",
"hashed_binary_data": "75ef47824de4b5477c75665c5a90e39a2b8a8985422cf2f7f641661a7b5217a8",
"hashing_algorithm": "sha256",
"is_valid": False,
"is_valid_cert": True,
"is_valid_hash": False,
"is_valid_signature": True,
"signers_serial": 9_662_248_800_192_484_627,
},
],
}
def test_signed_pdf_that_has_invalid_signature(mocker):
def mock_crl_check(_):
return True
mocker.patch.object(
cryptography.hazmat.backends.openssl.rsa._RSAPublicKey, "verify", Exception()
)
valid_signed_pdf = open("tests/fixtures/signed-pdf-not-dod.pdf", "rb").read()
result = pdf_signature_validations(pdf=valid_signed_pdf, crl_check=mock_crl_check)
assert result == {
"result": False,
"signature_count": 1,
"signatures": [
{
"cert_common_name": "John B Harris",
"hashed_binary_data": "3f0047e6cb5b9bb089254b20d174445c3ba4f513",
"hashing_algorithm": "sha1",
"is_valid": False,
"is_valid_cert": True,
"is_valid_hash": True,
"is_valid_signature": False,
"signers_serial": 514,
}
],
}
@pytest.mark.skip(reason="Need fixture file")
def test_signed_pdf_dod_revoked(crl_check):
signed_pdf_dod_revoked = open(
"tests/fixtures/signed-pdf-dod_revoked.pdf", "rb"
).read()
result = pdf_signature_validations(pdf=signed_pdf_dod_revoked, crl_check=crl_check)
assert result == {
"result": False,
"signature_count": 1,
"signatures": [
{
"cert_common_name": None,
"hashed_binary_data": None,
"hashing_algorithm": None,
"is_valid": None,
"is_valid_cert": None,
"is_valid_hash": None,
"signers_serial": None,
}
],
}