Browse Source

CMS sign digest

CLI changes: New parameter -digest to CLI command openssl cms, to
provide pre-computed digest for use with -sign.

API changes: New function CMS_final_digest(), like CMS_final() but
uses a pre-computed digest instead of computing it from the data.

Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
Reviewed-by: Todd Short <todd.short@me.com>
(Merged from https://github.com/openssl/openssl/pull/15348)
Viktor Söderqvist 3 years ago
parent
commit
07342bad1b

+ 43 - 9
apps/cms.c

@@ -66,7 +66,7 @@ typedef enum OPTION_choice {
     OPT_DECRYPT, OPT_SIGN, OPT_CADES, OPT_SIGN_RECEIPT, OPT_RESIGN,
     OPT_VERIFY, OPT_VERIFY_RETCODE, OPT_VERIFY_RECEIPT,
     OPT_CMSOUT, OPT_DATA_OUT, OPT_DATA_CREATE, OPT_DIGEST_VERIFY,
-    OPT_DIGEST_CREATE, OPT_COMPRESS, OPT_UNCOMPRESS,
+    OPT_DIGEST, OPT_DIGEST_CREATE, OPT_COMPRESS, OPT_UNCOMPRESS,
     OPT_ED_DECRYPT, OPT_ED_ENCRYPT, OPT_DEBUG_DECRYPT, OPT_TEXT,
     OPT_ASCIICRLF, OPT_NOINTERN, OPT_NOVERIFY, OPT_NOCERTS,
     OPT_NOATTR, OPT_NODETACH, OPT_NOSMIMECAP, OPT_BINARY, OPT_KEYID,
@@ -106,6 +106,7 @@ const OPTIONS cms_options[] = {
      "Generate a signed receipt for a message"},
     {"verify_receipt", OPT_VERIFY_RECEIPT, '<',
      "Verify receipts; exit if receipt signatures do not verify"},
+    {"digest", OPT_DIGEST, 's', "Sign a pre-computed digest in hex notation"},
     {"digest_create", OPT_DIGEST_CREATE, '-',
      "Create a CMS \"DigestedData\" object"},
     {"digest_verify", OPT_DIGEST_VERIFY, '-',
@@ -293,6 +294,9 @@ int cms_main(int argc, char **argv)
     const char *CAfile = NULL, *CApath = NULL, *CAstore = NULL;
     char *certsoutfile = NULL, *digestname = NULL, *wrapname = NULL;
     int noCAfile = 0, noCApath = 0, noCAstore = 0;
+    char *digesthex = NULL;
+    unsigned char *digestbin = NULL;
+    long digestlen = 0;
     char *infile = NULL, *outfile = NULL, *rctfile = NULL;
     char *passinarg = NULL, *passin = NULL, *signerfile = NULL;
     char *originatorfile = NULL, *recipfile = NULL, *ciphername = NULL;
@@ -367,6 +371,9 @@ int cms_main(int argc, char **argv)
         case OPT_DIGEST_CREATE:
             operation = SMIME_DIGEST_CREATE;
             break;
+        case OPT_DIGEST:
+            digesthex = opt_arg();
+            break;
         case OPT_DIGEST_VERIFY:
             operation = SMIME_DIGEST_VERIFY;
             break;
@@ -885,10 +892,31 @@ int cms_main(int argc, char **argv)
             goto end;
     }
 
-    in = bio_open_default(infile, 'r',
-                          binary_files ? FORMAT_BINARY : informat);
-    if (in == NULL)
-        goto end;
+    if (digesthex != NULL) {
+        if (operation != SMIME_SIGN) {
+            BIO_printf(bio_err,
+                       "Cannot use -digest for non-signing operation\n");
+            goto end;
+        }
+        if (infile != NULL
+            || (flags & CMS_DETACHED) == 0
+            || (flags & CMS_STREAM) != 0) {
+            BIO_printf(bio_err,
+                       "Cannot use -digest when -in, -nodetach or streaming is used\n");
+            goto end;
+        }
+        digestbin = OPENSSL_hexstr2buf(digesthex, &digestlen);
+        if (digestbin == NULL) {
+            BIO_printf(bio_err,
+                       "Invalid hex value after -digest\n");
+            goto end;
+        }
+    } else {
+        in = bio_open_default(infile, 'r',
+                              binary_files ? FORMAT_BINARY : informat);
+        if (in == NULL)
+            goto end;
+    }
 
     if (operation & SMIME_IP) {
         cms = load_content_info(informat, in, flags, &indata, "SMIME");
@@ -1037,12 +1065,12 @@ int cms_main(int argc, char **argv)
     } else if (operation & SMIME_SIGNERS) {
         int i;
         /*
-         * If detached data content we enable streaming if S/MIME output
-         * format.
+         * If detached data content and not signing pre-computed digest, we
+         * enable streaming if S/MIME output format.
          */
         if (operation == SMIME_SIGN) {
 
-            if (flags & CMS_DETACHED) {
+            if ((flags & CMS_DETACHED) != 0 && digestbin == NULL) {
                 if (outformat == FORMAT_SMIME)
                     flags |= CMS_STREAM;
             }
@@ -1103,7 +1131,12 @@ int cms_main(int argc, char **argv)
             key = NULL;
         }
         /* If not streaming or resigning finalize structure */
-        if ((operation == SMIME_SIGN) && !(flags & CMS_STREAM)) {
+        if (operation == SMIME_SIGN && digestbin != NULL
+            && (flags & CMS_STREAM) == 0) {
+            /* Use pre-computed digest instead of content */
+            if (!CMS_final_digest(cms, digestbin, digestlen, NULL, flags))
+                goto end;
+        } else if (operation == SMIME_SIGN && (flags & CMS_STREAM) == 0) {
             if (!CMS_final(cms, in, NULL, flags))
                 goto end;
         }
@@ -1272,6 +1305,7 @@ int cms_main(int argc, char **argv)
     BIO_free(in);
     BIO_free(indata);
     BIO_free_all(out);
+    OPENSSL_free(digestbin);
     OPENSSL_free(passin);
     NCONF_free(conf);
     return ret;

+ 3 - 1
crypto/cms/cms_err.c

@@ -1,6 +1,6 @@
 /*
  * Generated by util/mkerr.pl DO NOT EDIT
- * Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 1995-2022 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -109,6 +109,8 @@ static const ERR_STRING_DATA CMS_str_reasons[] = {
     {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_NO_PUBLIC_KEY), "no public key"},
     {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_NO_RECEIPT_REQUEST), "no receipt request"},
     {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_NO_SIGNERS), "no signers"},
+    {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_OPERATION_UNSUPPORTED),
+    "operation unsupported"},
     {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_PEER_KEY_ERROR), "peer key error"},
     {ERR_PACK(ERR_LIB_CMS, 0, CMS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE),
     "private key does not match certificate"},

+ 8 - 1
crypto/cms/cms_lib.c

@@ -207,6 +207,13 @@ err:
 
 /* unfortunately cannot constify SMIME_write_ASN1() due to this function */
 int CMS_dataFinal(CMS_ContentInfo *cms, BIO *cmsbio)
+{
+    return ossl_cms_DataFinal(cms, cmsbio, NULL, 0);
+}
+
+int ossl_cms_DataFinal(CMS_ContentInfo *cms, BIO *cmsbio,
+                       const unsigned char *precomp_md,
+                       unsigned int precomp_mdlen)
 {
     ASN1_OCTET_STRING **pos = CMS_get0_content(cms);
 
@@ -245,7 +252,7 @@ int CMS_dataFinal(CMS_ContentInfo *cms, BIO *cmsbio)
         return ossl_cms_AuthEnvelopedData_final(cms, cmsbio);
 
     case NID_pkcs7_signed:
-        return ossl_cms_SignedData_final(cms, cmsbio);
+        return ossl_cms_SignedData_final(cms, cmsbio, precomp_md, precomp_mdlen);
 
     case NID_pkcs7_digest:
         return ossl_cms_DigestedData_do_final(cms, cmsbio, 0);

+ 6 - 1
crypto/cms/cms_local.h

@@ -395,6 +395,9 @@ const char *ossl_cms_ctx_get0_propq(const CMS_CTX *ctx);
 void ossl_cms_resolve_libctx(CMS_ContentInfo *ci);
 
 CMS_ContentInfo *ossl_cms_Data_create(OSSL_LIB_CTX *ctx, const char *propq);
+int ossl_cms_DataFinal(CMS_ContentInfo *cms, BIO *cmsbio,
+                       const unsigned char *precomp_md,
+                       unsigned int precomp_mdlen);
 
 CMS_ContentInfo *ossl_cms_DigestedData_create(const EVP_MD *md,
                                               OSSL_LIB_CTX *libctx,
@@ -404,7 +407,9 @@ int ossl_cms_DigestedData_do_final(const CMS_ContentInfo *cms,
                                    BIO *chain, int verify);
 
 BIO *ossl_cms_SignedData_init_bio(CMS_ContentInfo *cms);
-int ossl_cms_SignedData_final(CMS_ContentInfo *cms, BIO *chain);
+int ossl_cms_SignedData_final(CMS_ContentInfo *cms, BIO *chain,
+                              const unsigned char *precomp_md,
+                              unsigned int precomp_mdlen);
 int ossl_cms_set1_SignerIdentifier(CMS_SignerIdentifier *sid, X509 *cert,
                                    int type, const CMS_CTX *ctx);
 int ossl_cms_SignerIdentifier_get0_signer_id(CMS_SignerIdentifier *sid,

+ 23 - 11
crypto/cms/cms_sd.c

@@ -694,7 +694,9 @@ ASN1_OCTET_STRING *CMS_SignerInfo_get0_signature(CMS_SignerInfo *si)
 }
 
 static int cms_SignerInfo_content_sign(CMS_ContentInfo *cms,
-                                       CMS_SignerInfo *si, BIO *chain)
+                                       CMS_SignerInfo *si, BIO *chain,
+                                       const unsigned char *md,
+                                       unsigned int mdlen)
 {
     EVP_MD_CTX *mctx = EVP_MD_CTX_new();
     int r = 0;
@@ -722,11 +724,13 @@ static int cms_SignerInfo_content_sign(CMS_ContentInfo *cms,
      */
 
     if (CMS_signed_get_attr_count(si) >= 0) {
-        unsigned char md[EVP_MAX_MD_SIZE];
-        unsigned int mdlen;
+        unsigned char computed_md[EVP_MAX_MD_SIZE];
 
-        if (!EVP_DigestFinal_ex(mctx, md, &mdlen))
-            goto err;
+        if (md == NULL) {
+            if (!EVP_DigestFinal_ex(mctx, computed_md, &mdlen))
+                goto err;
+            md = computed_md;
+        }
         if (!CMS_signed_add1_attr_by_NID(si, NID_pkcs9_messageDigest,
                                          V_ASN1_OCTET_STRING, md, mdlen))
             goto err;
@@ -739,12 +743,14 @@ static int cms_SignerInfo_content_sign(CMS_ContentInfo *cms,
     } else if (si->pctx) {
         unsigned char *sig;
         size_t siglen;
-        unsigned char md[EVP_MAX_MD_SIZE];
-        unsigned int mdlen;
+        unsigned char computed_md[EVP_MAX_MD_SIZE];
 
         pctx = si->pctx;
-        if (!EVP_DigestFinal_ex(mctx, md, &mdlen))
-            goto err;
+        if (md == NULL) {
+            if (!EVP_DigestFinal_ex(mctx, computed_md, &mdlen))
+                goto err;
+            md = computed_md;
+        }
         siglen = EVP_PKEY_get_size(si->pkey);
         sig = OPENSSL_malloc(siglen);
         if (sig == NULL) {
@@ -760,6 +766,10 @@ static int cms_SignerInfo_content_sign(CMS_ContentInfo *cms,
         unsigned char *sig;
         unsigned int siglen;
 
+        if (md != NULL) {
+            ERR_raise(ERR_LIB_CMS, CMS_R_OPERATION_UNSUPPORTED);
+            goto err;
+        }
         sig = OPENSSL_malloc(EVP_PKEY_get_size(si->pkey));
         if (sig == NULL) {
             ERR_raise(ERR_LIB_CMS, ERR_R_MALLOC_FAILURE);
@@ -784,7 +794,9 @@ static int cms_SignerInfo_content_sign(CMS_ContentInfo *cms,
 
 }
 
-int ossl_cms_SignedData_final(CMS_ContentInfo *cms, BIO *chain)
+int ossl_cms_SignedData_final(CMS_ContentInfo *cms, BIO *chain,
+                              const unsigned char *precomp_md,
+                              unsigned int precomp_mdlen)
 {
     STACK_OF(CMS_SignerInfo) *sinfos;
     CMS_SignerInfo *si;
@@ -793,7 +805,7 @@ int ossl_cms_SignedData_final(CMS_ContentInfo *cms, BIO *chain)
     sinfos = CMS_get0_SignerInfos(cms);
     for (i = 0; i < sk_CMS_SignerInfo_num(sinfos); i++) {
         si = sk_CMS_SignerInfo_value(sinfos, i);
-        if (!cms_SignerInfo_content_sign(cms, si, chain))
+        if (!cms_SignerInfo_content_sign(cms, si, chain, precomp_md, precomp_mdlen))
             return 0;
     }
     cms->d.signedData->encapContentInfo->partial = 0;

+ 25 - 0
crypto/cms/cms_smime.c

@@ -897,6 +897,31 @@ err:
 
 }
 
+int CMS_final_digest(CMS_ContentInfo *cms,
+                     const unsigned char *md, unsigned int mdlen,
+                     BIO *dcont, unsigned int flags)
+{
+    BIO *cmsbio;
+    int ret = 0;
+
+    if ((cmsbio = CMS_dataInit(cms, dcont)) == NULL) {
+        ERR_raise(ERR_LIB_CMS, CMS_R_CMS_LIB);
+        return 0;
+    }
+
+    (void)BIO_flush(cmsbio);
+
+    if (!ossl_cms_DataFinal(cms, cmsbio, md, mdlen)) {
+        ERR_raise(ERR_LIB_CMS, CMS_R_CMS_DATAFINAL_ERROR);
+        goto err;
+    }
+    ret = 1;
+
+err:
+    do_free_upto(cmsbio, dcont);
+    return ret;
+}
+
 #ifdef ZLIB
 
 int CMS_uncompress(CMS_ContentInfo *cms, BIO *dcont, BIO *out,

+ 1 - 0
crypto/err/openssl.txt

@@ -343,6 +343,7 @@ CMS_R_NO_PRIVATE_KEY:133:no private key
 CMS_R_NO_PUBLIC_KEY:134:no public key
 CMS_R_NO_RECEIPT_REQUEST:168:no receipt request
 CMS_R_NO_SIGNERS:135:no signers
+CMS_R_OPERATION_UNSUPPORTED:182:operation unsupported
 CMS_R_PEER_KEY_ERROR:188:peer key error
 CMS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE:136:\
 	private key does not match certificate

+ 13 - 0
doc/man1/openssl-cms.pod.in

@@ -25,6 +25,7 @@ Operation options:
 [B<-resign>]
 [B<-sign_receipt>]
 [B<-verify_receipt> I<receipt>]
+[B<-digest> I<digest>]
 [B<-digest_create>]
 [B<-digest_verify>]
 [B<-compress>]
@@ -207,6 +208,16 @@ Verify a signed receipt in filename B<receipt>. The input message B<must>
 contain the original receipt request. Functionality is otherwise similar
 to the B<-verify> operation.
 
+=item B<-digest> I<digest>
+
+When used with B<-sign>, provides the digest in hexadecimal form instead of
+computing it from the original message content. Cannot be combined with B<-in>
+or B<-nodetach>.
+
+This operation is the CMS equivalent of L<openssl-pkeyutl(1)> signing.
+When signing a pre-computed digest, the security relies on the digest and its
+computation from the original message being trusted.
+
 =item B<-digest_create>
 
 Create a CMS B<DigestedData> type.
@@ -900,6 +911,8 @@ The B<-nameopt> option was added in OpenSSL 3.0.0.
 
 The B<-engine> option was deprecated in OpenSSL 3.0.
 
+The B<-digest> option was added in OpenSSL 3.1.
+
 =head1 COPYRIGHT
 
 Copyright 2008-2021 The OpenSSL Project Authors. All Rights Reserved.

+ 19 - 4
doc/man3/CMS_final.pod

@@ -2,13 +2,15 @@
 
 =head1 NAME
 
-CMS_final - finalise a CMS_ContentInfo structure
+CMS_final, CMS_final_digest - finalise a CMS_ContentInfo structure
 
 =head1 SYNOPSIS
 
  #include <openssl/cms.h>
 
  int CMS_final(CMS_ContentInfo *cms, BIO *data, BIO *dcont, unsigned int flags);
+ int CMS_final_digest(CMS_ContentInfo *cms, const unsigned char *md,
+                      unsigned int mdlen, BIO *dcont, unsigned int flags);
 
 =head1 DESCRIPTION
 
@@ -19,24 +21,37 @@ processed. The B<dcont> parameter contains a BIO to write content to after
 processing: this is only used with detached data and will usually be set to
 NULL.
 
+CMS_final_digest() finalises the structure B<cms> using a pre-computed digest,
+rather than computing the digest from the original data.
+
 =head1 NOTES
 
-This function will normally be called when the B<CMS_PARTIAL> flag is used. It
+These functions will normally be called when the B<CMS_PARTIAL> flag is used. It
 should only be used when streaming is not performed because the streaming
 I/O functions perform finalisation operations internally.
 
+To sign a pre-computed digest, L<CMS_sign(3)> or CMS_sign_ex() is called
+with the B<data> parameter set to NULL before the CMS structure is finalised
+with the digest provided to CMS_final_digest() in binary form.
+When signing a pre-computed digest, the security relies on the digest and its
+computation from the original message being trusted.
+
 =head1 RETURN VALUES
 
-CMS_final() returns 1 for success or 0 for failure.
+CMS_final() and CMS_final_digest() return 1 for success or 0 for failure.
 
 =head1 SEE ALSO
 
 L<ERR_get_error(3)>, L<CMS_sign(3)>,
 L<CMS_encrypt(3)>
 
+=head1 HISTORY
+
+CMS_final_digest() was added in OpenSSL 3.1.
+
 =head1 COPYRIGHT
 
-Copyright 2008-2016 The OpenSSL Project Authors. All Rights Reserved.
+Copyright 2008-2021 The OpenSSL Project Authors. All Rights Reserved.
 
 Licensed under the Apache License 2.0 (the "License").  You may not use
 this file except in compliance with the License.  You can obtain a copy

+ 1 - 1
include/crypto/cmserr.h

@@ -1,6 +1,6 @@
 /*
  * Generated by util/mkerr.pl DO NOT EDIT
- * Copyright 2020-2021 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2020-2022 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy

+ 3 - 0
include/openssl/cms.h.in

@@ -119,6 +119,9 @@ int SMIME_write_CMS(BIO *bio, CMS_ContentInfo *cms, BIO *data, int flags);
 
 int CMS_final(CMS_ContentInfo *cms, BIO *data, BIO *dcont,
               unsigned int flags);
+int CMS_final_digest(CMS_ContentInfo *cms,
+                     const unsigned char *md, unsigned int mdlen, BIO *dcont,
+                     unsigned int flags);
 
 CMS_ContentInfo *CMS_sign(X509 *signcert, EVP_PKEY *pkey,
                           STACK_OF(X509) *certs, BIO *data,

+ 2 - 1
include/openssl/cmserr.h

@@ -1,6 +1,6 @@
 /*
  * Generated by util/mkerr.pl DO NOT EDIT
- * Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 1995-2022 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -86,6 +86,7 @@
 #  define CMS_R_NO_PUBLIC_KEY                              134
 #  define CMS_R_NO_RECEIPT_REQUEST                         168
 #  define CMS_R_NO_SIGNERS                                 135
+#  define CMS_R_OPERATION_UNSUPPORTED                      182
 #  define CMS_R_PEER_KEY_ERROR                             188
 #  define CMS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE     136
 #  define CMS_R_RECEIPT_DECODE_ERROR                       169

+ 43 - 1
test/recipes/80-test_cms.t

@@ -50,7 +50,7 @@ my ($no_des, $no_dh, $no_dsa, $no_ec, $no_ec2m, $no_rc2, $no_zlib)
 
 $no_rc2 = 1 if disabled("legacy");
 
-plan tests => 12;
+plan tests => 14;
 
 ok(run(test(["pkcs7_test"])), "test pkcs7");
 
@@ -847,6 +847,48 @@ subtest "CMS binary input tests\n" => sub {
        "verify binary input with -binary missing -crlfeol");
 };
 
+subtest "CMS signed digest, DER format" => sub {
+    plan tests => 2;
+
+    # Pre-computed SHA256 digest of $smcont in hexadecimal form
+    my $digest = "ff236ef61b396355f75a4cc6e1c306d4c309084ae271a9e2ad6888f10a101b32";
+
+    my $sig_file = "signature.der";
+    ok(run(app(["openssl", "cms", @prov, "-sign", "-digest", $digest,
+                    "-outform", "DER",
+                    "-certfile", catfile($smdir, "smroot.pem"),
+                    "-signer", catfile($smdir, "smrsa1.pem"),
+                    "-out", $sig_file])),
+        "CMS sign pre-computed digest, DER format");
+
+    ok(run(app(["openssl", "cms", @prov, "-verify", "-in", $sig_file,
+                    "-inform", "DER",
+                    "-CAfile", catfile($smdir, "smroot.pem"),
+                    "-content", $smcont])),
+       "Verify CMS signed digest, DER format");
+};
+
+subtest "CMS signed digest, S/MIME format" => sub {
+    plan tests => 2;
+
+    # Pre-computed SHA256 digest of $smcont in hexadecimal form
+    my $digest = "ff236ef61b396355f75a4cc6e1c306d4c309084ae271a9e2ad6888f10a101b32";
+
+    my $sig_file = "signature.smime";
+    ok(run(app(["openssl", "cms", @prov, "-sign", "-digest", $digest,
+                    "-outform", "SMIME",
+                    "-certfile", catfile($smdir, "smroot.pem"),
+                    "-signer", catfile($smdir, "smrsa1.pem"),
+                    "-out", $sig_file])),
+        "CMS sign pre-computed digest, S/MIME format");
+
+    ok(run(app(["openssl", "cms", @prov, "-verify", "-in", $sig_file,
+                    "-inform", "SMIME",
+                    "-CAfile", catfile($smdir, "smroot.pem"),
+                    "-content", $smcont])),
+       "Verify CMS signed digest, S/MIME format");
+};
+
 sub check_availability {
     my $tnam = shift;
 

+ 1 - 0
util/libcrypto.num

@@ -5437,3 +5437,4 @@ BN_signed_bn2native                     ?	3_1_0	EXIST::FUNCTION:
 ASYNC_set_mem_functions                 ?	3_1_0	EXIST::FUNCTION:
 ASYNC_get_mem_functions                 ?	3_1_0	EXIST::FUNCTION:
 BIO_ADDR_dup                            ?	3_1_0	EXIST::FUNCTION:SOCK
+CMS_final_digest                        ?	3_1_0	EXIST::FUNCTION:CMS