34a1d37575
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>
173 lines
5.3 KiB
C++
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);
|
|
}
|