7

i have a usb cryptotoken and able to sign data and pack it in pkcs file. then i can extract certificate and data from that file using openssl as follows:

openssl cms -verify -in signature.p7s -inform DER -noverify -outform DER -signer cert.pem -out textdata

so my question is how to do the same using python (pyopenssl)?

i've tried to do as described here, but there is different case - i have attached signature and do not have separate signature and certificate file - i have ASN.1 encoded file, which contains as certificates as data and signature

stovfl
  • 14,998
  • 7
  • 24
  • 51
borune
  • 548
  • 5
  • 21

2 Answers2

6

There are several hurdles to overcome to achieve what you are looking for.

First, the pyopenssl binding itself is limited when it comes to its crypto module, where you desired functionality resides. In fact, the pyopenssl crypto documentation states: enter image description here The pyca/cryptography module mentioned is exposed via two internal attributes of the pyopenssl crypto module, with the names _lib and _ffi, which need to be used to get to the required functionality.

Then the CMS_verify() function that would be your logical choice for this is not included in the pyca/cryptography bindings either. However, for your purpose it is probably good enough to use PKCS7_verify() -- you can read all about that in a StackExchange question OpenSSL PKCS#7 vs. S/MIME. The function crypto.load_pkcs7_data() comes in handy.

All that said, the following code snippet might do it for you -- although from your description it is not clear to me whether the certificate of the signer is included in the .p7s file (in that case you do not have to give -signer as an argument to openssl cms -verify like you did). It worked for me, so give it a try:

from OpenSSL import crypto
from OpenSSL._util import (
    ffi as _ffi,
    lib as _lib,
)

# Or, alternatively:
# from cryptography.hazmat.bindings.openssl.binding import Binding
# _lib = Binding.lib
# _ffi = Binding.ffi

with open('message_der.p7s', 'rb') as f:
    p7data = f.read()
p7 = crypto.load_pkcs7_data(crypto.FILETYPE_ASN1, p7data)

bio_out =crypto._new_mem_buf()
res = _lib.PKCS7_verify(p7._pkcs7, _ffi.NULL, _ffi.NULL, _ffi.NULL, bio_out, _lib.PKCS7_NOVERIFY)
if res == 1:
    databytes = crypto._bio_to_string(bio_out)
    print(databytes)
else:
    errno = _lib.ERR_get_error()
    errstrlib = _ffi.string(_lib.ERR_lib_error_string(errno))
    errstrfunc = _ffi.string(_lib.ERR_func_error_string(errno))
    errstrreason = _ffi.string(_lib.ERR_reason_error_string(errno)) 

In case you decide to use this approach, here is a caveat about using this OpenSSL bindings module directly: enter image description here

Reinier Torenbeek
  • 16,669
  • 7
  • 46
  • 69
  • Correct answer to loading PKCS#7 file is in https://stackoverflow.com/a/45111623/1548275 – Jari Turkia Feb 22 '20 at 15:32
  • @JariTurkia In that answer I do not see where the actual signed data is extracted from the PKCS7 file and the signature verified, which was the question here. Interesting link to that `pyopenssl` pull request though, it does seem to fill the gap to get to the data. – Reinier Torenbeek Feb 22 '20 at 15:47
  • Actually, in that answer there is a suggested `OpenSSL.crypto.load_pkcs7_data(type, buffer)` which does the trick of loading data. Given `OpenSSL.crypto.PKCS7` does not implement almost anything useful, the obvious next move is to extract the X.509 certificate. I spent a while attempting to get `PKCS7_verify()` working, but failed. As suggested in the other answer, getting the CData-pointer and reading X.509 certificate bytes there does work. – Jari Turkia Feb 22 '20 at 16:12
  • My answer here does show how to leverage `PKCS7_verify()`, did that not work for you? – Reinier Torenbeek Feb 22 '20 at 16:14
  • Doing a `_lib.PKCS7_verify()` always returned 0. The error reason was "no data". I'm speculating, that PKCS7_verify() doesn't account for different types of PKCS#7 data: signed, enveloped or signed&enveloped. – Jari Turkia Feb 22 '20 at 16:48
  • That is weird. Did the `openssl cms -verify` command succeed with that same input though? – Reinier Torenbeek Feb 22 '20 at 17:19
  • That would be a negative. This is not a S/MIME-case. It's a simple case of trying to extract X.509 out of PKCS#7-file. – Jari Turkia Feb 24 '20 at 07:05
  • So that explains why `PKCS7_verify()` returned "no data" in your case. But this question was about a case that the bundle _did_ contain data -- hence the `openssl cms -verify` command. By the way, PKCS#7 (or nowadays CMS) does not necessarily imply S/MIME, which is built on top of it. – Reinier Torenbeek Feb 24 '20 at 14:44
2

The Python code below (posted in How to extract the signature from a p7s file?) will extract the cryptographic data you need, plus some (no protected/private arguments needed).

# Python 3.10 code to extract relevant data from a PKCS#7 signature file

from datetime import datetime
from asn1crypto import cms
from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs7

# these are the components we are going to extract
payload: bytes                      # the original payload
signature: bytes                    # the digital signature
signature_algorithm: str            # the algorithm used to generate the signature
signature_timestamp: datetime       # the signature's timestamp
payload_hash: bytes                 # the payload hash
hash_algorithm: str                 # the algorithm used to calculate the payload hash
cert_chain: list[x509.Certificate]  # the X509 certificate chain

# define the PKCS#7 signature file path here
p7s_filepath: str = 'my_signature_file_path.p7s'

# load the p7s file
with open(p7s_filepath, 'rb') as f:
    p7s_bytes: bytes = f.read()
    f.close()

# extract the certificater chain
cert_chain = pkcs7.load_der_pkcs7_certificates(p7s_bytes)

# extract the needed structures
content_info: cms.ContentInfo = cms.ContentInfo.load(p7s_bytes)
signed_data: cms.SignedData = content_info['content']
signer_info: cms.SignerInfo = signed_data['signer_infos'][0]

# extract the payload (None if payload is detached)
payload = signed_data['encap_content_info']['content'].native

# extract the remaining components
signature = signer_info['signature'].native
signature_algorithm = signer_info['signature_algorithm']['algorithm'].native
hash_algorithm = signer_info['digest_algorithm']['algorithm'].native

signed_attrs = signer_info['signed_attrs']
for signed_attr in signed_attrs:
    match signed_attr['type'].native:
        case 'message_digest':
            payload_hash = signed_attr['values'][0].native
        case 'signing_time':
            signature_timestamp = signed_attr['values'][0].native
lcofresi
  • 33
  • 6