boesing
boesing

Reputation: 345

CMS (PKCS#7) RecipientInfo

I am actually working on a function which should extract RecipientInfo from PKCS7 mime encrypted message. The reason why I want to do this is, that I want to get all mail addresses (or at least the keyids/fingerprints) the message is encrypted for.

Well - I tried something out and created something like this (indata is a *.p7m attachment content, indata_len the strlen of indata):

char *indata;
int indata_len, i;
PKCS7 *p7 = NULL;
BIO *bcont = NULL;
CMS_ContentInfo *cms = NULL;
STACK_OF(CMS_RecipientInfo) *recipients = NULL;
CMS_RecipientInfo *recip = NULL;
BIO *encMessage = BIO_new(BIO_s_mem());
if (encMessage == NULL) {
    goto clean_exit;
}

if(!BIO_write(encMessage, indata, indata_len)) {
    goto clean_exit;
}

cms = SMIME_read_CMS(encMessage,NULL);
if (cms == NULL ) {
    goto clean_exit;
}

recipients = CMS_get0_RecipientInfos(cms);
if (recipients == NULL) {
    goto clean_exit;
}

for (i=0; i< sk_CMS_RecipientInfo_num(recipients); i++) {
    recip = sk_CMS_RecipientInfo_value(recipients, i);
    if( recip == NULL || CMS_RecipientInfo_type(recip) != CMS_RECIPINFO_TRANS ) {
        continue;
    }

    int r;
    ASN1_OCTET_STRING **keyid;
    X509_NAME **issuer;
    ASN1_INTEGER **sno;

    r = CMS_RecipientInfo_ktri_get0_signer_id(recip, keyid, issuer, sno);
    if (!r) {
        continue;
    }

    printf("Key: %s\n", keyid);
}

I get no error (checked with ERR_get_error()) but keyid, issuer and sno stay "null", output of above code is:

Key: (null)

So my question is, is it even possible to get that information of an encrypted message or is there just an error in reasoning on my side?

If it is possible to get that data, can someone give me a hint? If it is not possible, whats the default (best) way to check which private key to use for decryption. Since there can be more than one S/Mime certificate/key for a single user. E.g. creating new key since the old one is lost or just get a new cert/key combination from provider, ... Imho, looping through all keys could take some time if the message is really big.

Best regards, Max

Upvotes: 4

Views: 1761

Answers (1)

Thorsten
Thorsten

Reputation: 129

I don't know how to fix your code, but I have a couple of openssl commands and a python script to solve your task:

You can run the following command to get the list of all serial numbers of the recipient keys in an encrypted file MYMAIL:

openssl smime -pk7out -inform DER -in MYMAIL \
    | openssl pkcs7 -noout -print \
    | grep serial

This will print the serial number as decimal numbers of all recipients, i.e. serial numbers of certificates for which the file MYMAIL has been encrypted for. For a given certificate file CERTFILE.0, the command

openssl x509 -in CERTFILE.0 -serial -noout

prints its serial number as a hexadecimal number. Now, you can to combine the serial numbers of the certificates you have with the serial numbers mentioned in MYMAIL.

I've wrote a python script that does this and that can be used to replace the default smime_decrypt_command in mutt, such that when decrypting an E-Mail, the correct private key is chosen for decryption: https://github.com/t-wissmann/dotfiles/blob/master/utils/smime-recipient-list.py For the case that the url breaks, I'm pasting the entire script below.

#!/usr/bin/env python3
"""
Given an smime encrypted file and some smime certificates,
tell for which of the smime certificates, the encrypted file has been
encrypted for.
"""

import argparse
import os
import re
import subprocess
import sys
import textwrap

class Openssl:
    def __init__(self, openssl_command):
        self.openssl_command = openssl_command

    def get_certificate_serial_number(self, certificate_file):
        """Given a certificate_file filepath, return its serial number as an int"""
        command = [self.openssl_command, 'x509', '-in', certificate_file, '-serial', '-noout']
        proc = subprocess.run(command, stdout=subprocess.PIPE)
        # output should be of the form 'serial=HEXADECIMALNUMBER'
        try:
            return int(proc.stdout.decode().replace('serial=', ''), 16)
        except ValueError:
             print("Can not read file: {}".format(certificate_file), file=sys.stderr)

    def smime_pk7out(self, encrypted_file):
        """run smime -pk7out, return its output"""
        command = [self.openssl_command, 'smime', '-pk7out']
        command += ['-inform', 'DER', '-in', encrypted_file]
        proc = subprocess.run(command, stdout=subprocess.PIPE)
        return proc.stdout.decode()

    def pkcs7_serial_numbers(self, pk7buf):
        """extract all serial numbers via openssl pkcs7 -noout -print"""
        command = [self.openssl_command, 'pkcs7', '-noout', '-print']
        proc = subprocess.run(command, stdout=subprocess.PIPE, text=True, input=pk7buf)
        for match in re.finditer('serial: ([0-9]+)', proc.stdout):
            yield int(match.group(1))

    def list_recipient_serial_numbers(self, encrypted_file):
        """Do essentially:
            openssl smime -pk7out -inform DER -in MYMAIL \
                | openssl pkcs7 -noout -print \
                | grep serial
        """
        pk7out = self.smime_pk7out(encrypted_file)
        return list(self.pkcs7_serial_numbers(pk7out))

    def smime_decrypt(self, private_key, certificate, filepath, passin='stdin'):
        """encrypt the given filepath and print to stdout"""
        command = [self.openssl_command, 'smime', '-decrypt', '-passin', passin]
        command += ['-inform', 'DER', '-in', filepath]
        command += ['-inkey', private_key]
        command += ['-recip', certificate]
        subprocess.run(command)

def main():
    """main"""
    description = "Detect recipients of smime encrypted files"
    epilog = textwrap.dedent(r"""
    E.g. you can decrypt an email with the command that picks the
    private key automatically:

        {} \
            --passin stdin --decrypt \
            --private-key ~/.smime/keys/* \
            -- mymail ~/.smime/certificates/*

    If you use mutt, you can set

    set smime_decrypt_command="\
        ~/path/to/smime-recipient-list.py --passin stdin --decrypt \
        --private-key ~/.smime/keys/* \
        -- %f ~/.smime/certificates/KEYPREFIX.*"

    where KEYPREFIX is the prefix of your key (i.e. without the .0 or .1 suffix).
    """.format(sys.argv[0]))
    parser = argparse.ArgumentParser(
        description=description,
        epilog=epilog,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('encryptedfile', help='the encrypted file')
    parser.add_argument('certificates',
                        nargs='+',
                        help='the smime certificate files')
    parser.add_argument('--openssl', default='openssl', help='openssl command name')
    parser.add_argument('--list-serials', action='store_true',
                        help='list serial numbers of certifacts')
    parser.add_argument('--print-path', action='store_true',
                        help='print path of recipient certificates')
    parser.add_argument('--private-keys', nargs='*', default=[], help='private keys for decrypt')
    parser.add_argument('--decrypt', action='store_true',
                        help='decrypt using one of the private keys passed.\
                              the key must have the same file name as the certificate.')
    parser.add_argument('--passin', default='stdin',
                        help='default openssl -passin parameter for decrypt')
    args = parser.parse_args()
    openssl = Openssl(args.openssl)

    # get the serial number of every smime-certfile:
    serialnum2cert = {}
    for i in args.certificates:
        serialnum2cert[openssl.get_certificate_serial_number(i)] = i
    if args.list_serials:
        for serialnum, keyfile in serialnum2cert.items():
            print("{} --> {}".format(keyfile, serialnum))
    recipients = openssl.list_recipient_serial_numbers(args.encryptedfile)
    if args.print_path or args.decrypt:
        matching_keys = []
        for i in recipients:
            if i in serialnum2cert:
                matching_keys.append(serialnum2cert[i])
    if args.print_path:
        for i in matching_keys:
            print(i)
    if args.decrypt:
        private_keys = {}
        for filepath in args.private_keys:
            private_keys[os.path.basename(filepath)] = filepath
        key_found = None
        for fp in matching_keys:
            if os.path.basename(fp) in private_keys:
                priv_key_path = private_keys[os.path.basename(fp)]
                # print("We can use {} and {}".format(priv_key_path, fp))
                key_found = (priv_key_path, fp)
        if key_found is None:
            print("No matching private key found.", file=sys.stderr)
            sys.exit(1)
        openssl.smime_decrypt(key_found[0], key_found[1],
                              args.encryptedfile, passin=args.passin)

if __name__ == "__main__":
    main()

Upvotes: 3

Related Questions