Files
David Benjamin 34a1d37575 Implement functions to generate CMS external signatures
This implements just the small subset of OpenSSL's CMS API to support
the Linux kernel's sign-file.c tool. It is nowhere close to a full CMS
implementation and is not intended to become one. In particular, it does
not implement enough of CMS to support S/MIME. That requires much, much
more infrastructure than was implemented here.

CMS is, like PKCS#7, an over-engineered and cryptographically unsound
set of nestable combinators to support just about any configuration of
cryptographic operations. Profiling CMS down to a usable subset is, as a
result, more complicated, more risky, and less efficient than just
designing a bespoke structure for your use case. It is derived from
PKCS#7, and largely overlaps. However, both PKCS#7 and CMS use the v1
version number, but CMS made incompatible changes in some corner cases
that, so far, do not matter to us. (It is incompatible if you try to
layer SignedData atop another combinator, where the lack of proper
domain separation in this badly designed format is of extra risk.)

In the case of the kernel, sign-file.c wants an "external signature",
which is when the data to be signed lives elsewhere. This is, as a
result, a very, very inefficient way to concatenate an enum with a byte
string. But this is what the kernel chose, so here we are.

Because PKCS#7 and CMS are broadly the same structure, I've generalized
the internal PKCS#7 function rather than duplicating all this code. If
we ever hit the cases where PKCS#7 and CMS v1 are incompatible, plumbing
an extra boolean will be the least of our worries.

Test data was generated by compiling the actual sign-file.c against
OpenSSL and saving the output.

Change-Id: Idb0874d2b5294bfad564f3a00458c3fd044d9da5
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/78452
Auto-Submit: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
2025-04-14 13:44:28 -07:00

173 lines
5.3 KiB
C++

// Copyright 2025 The BoringSSL Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <openssl/cms.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/mem.h>
#include <openssl/x509.h>
#include "../pkcs7/internal.h"
// TODO(davidben): Should we move the core PKCS#7 / CMS implementation into
// crypto/cms instead of crypto/pkcs7? CMS is getting new features while PKCS#7
// is not.
OPENSSL_DECLARE_ERROR_REASON(CMS, CERTIFICATE_HAS_NO_KEYID)
struct CMS_SignerInfo_st {
X509 *signcert = nullptr;
EVP_PKEY *pkey = nullptr;
const EVP_MD *md = nullptr;
bool use_key_id = false;
};
struct CMS_ContentInfo_st {
bool has_signer_info = false;
CMS_SignerInfo signer_info;
uint8_t *der = nullptr;
size_t der_len = 0;
};
CMS_ContentInfo *CMS_sign(X509 *signcert, EVP_PKEY *pkey, STACK_OF(X509) *certs,
BIO *data, uint32_t flags) {
// We only support external signatures and do not support embedding
// certificates in SignedData.
if ((flags & CMS_DETACHED) == 0 || sk_X509_num(certs) != 0) {
OPENSSL_PUT_ERROR(CMS, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return nullptr;
}
bssl::UniquePtr<CMS_ContentInfo> cms(
static_cast<CMS_ContentInfo *>(OPENSSL_zalloc(sizeof(CMS_ContentInfo))));
if (cms == nullptr) {
return nullptr;
}
if (pkey != nullptr &&
!CMS_add1_signer(cms.get(), signcert, pkey, /*md=*/nullptr, flags)) {
return nullptr;
}
// We don't actually use streaming mode, but Linux passes |CMS_STREAM| to
// |CMS_sign| and OpenSSL interprets it as an alias for |CMS_PARTIAL| in this
// context.
if ((flags & (CMS_PARTIAL | CMS_STREAM)) == 0 &&
!CMS_final(cms.get(), data, NULL, flags)) {
return nullptr;
}
return cms.release();
}
void CMS_ContentInfo_free(CMS_ContentInfo *cms) {
if (cms == nullptr) {
return;
}
X509_free(cms->signer_info.signcert);
EVP_PKEY_free(cms->signer_info.pkey);
OPENSSL_free(cms->der);
OPENSSL_free(cms);
}
CMS_SignerInfo *CMS_add1_signer(CMS_ContentInfo *cms, X509 *signcert,
EVP_PKEY *pkey, const EVP_MD *md,
uint32_t flags) {
if ( // Already finalized.
cms->der_len != 0 ||
// We only support one signer.
cms->has_signer_info ||
// We do not support configuring a signer in multiple steps. (In OpenSSL,
// this is used to configure attributes.
(flags & CMS_PARTIAL) != 0 ||
// We do not support embedding certificates in SignedData.
(flags & CMS_NOCERTS) == 0 ||
// We do not support attributes in SignedData.
(flags & CMS_NOATTR) == 0) {
OPENSSL_PUT_ERROR(CMS, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return nullptr;
}
if (signcert == nullptr || pkey == nullptr) {
OPENSSL_PUT_ERROR(CMS, ERR_R_PASSED_NULL_PARAMETER);
return nullptr;
}
if (!X509_check_private_key(signcert, pkey)) {
OPENSSL_PUT_ERROR(CMS, CMS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE);
return nullptr;
}
// Default to SHA-256.
if (md == nullptr) {
md = EVP_sha256();
}
// Save information for later.
cms->has_signer_info = true;
cms->signer_info.signcert = bssl::UpRef(signcert).release();
cms->signer_info.pkey = bssl::UpRef(pkey).release();
cms->signer_info.md = md;
cms->signer_info.use_key_id = (flags & CMS_USE_KEYID) != 0;
return &cms->signer_info;
}
int CMS_final(CMS_ContentInfo *cms, BIO *data, BIO *dcont, uint32_t flags) {
if ( // Already finalized.
cms->der_len != 0 ||
// Require a SignerInfo. We do not support signature-less SignedDatas.
!cms->has_signer_info ||
// We only support the straightforward passthrough mode, without S/MIME
// translations.
(flags & CMS_BINARY) == 0 ||
// We do not support |dcont|. It is unclear what it does.
dcont != nullptr) {
OPENSSL_PUT_ERROR(CMS, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return 0;
}
bssl::ScopedCBB cbb;
if (!CBB_init(cbb.get(), 2048) ||
!pkcs7_add_external_signature(cbb.get(), cms->signer_info.signcert,
cms->signer_info.pkey, cms->signer_info.md,
data, cms->signer_info.use_key_id) ||
!CBB_finish(cbb.get(), &cms->der, &cms->der_len)) {
return 0;
}
return 1;
}
int i2d_CMS_bio(BIO *out, CMS_ContentInfo *cms) {
if (cms->der_len == 0) {
// Not yet finalized.
OPENSSL_PUT_ERROR(CMS, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return 0;
}
return BIO_write_all(out, cms->der, cms->der_len);
}
int i2d_CMS_bio_stream(BIO *out, CMS_ContentInfo *cms, BIO *in, int flags) {
// We do not support streaming mode.
if ((flags & CMS_STREAM) != 0 || in != nullptr) {
OPENSSL_PUT_ERROR(CMS, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return 0;
}
return i2d_CMS_bio(out, cms);
}