blob: b73dd6d93e120327b07ed12f1a8eda0de5ddfe04 [file]
// Copyright 2026 Google LLC
//
// 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 "testing/cms.h"
#include <cstdint>
#include <ctime>
#include <memory>
#include <string>
#include <vector>
#include "absl/functional/function_ref.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "crypto/algorithms.h"
#include "crypto/default/hasher.h"
#include "crypto/hash.h"
#include "openssl/asn1.h"
#include "openssl/base.h"
#include "openssl/bytestring.h"
#include "openssl/mem.h"
#include "openssl/obj.h"
#include "openssl/x509.h"
namespace credentio_testing {
namespace {
constexpr absl::string_view kOidMessageDigest = "1.2.840.113549.1.9.4";
constexpr absl::string_view kOidContentType = "1.2.840.113549.1.9.3";
constexpr absl::string_view kOidTstInfo = "1.2.840.113549.1.9.16.1.4";
constexpr absl::string_view kOidData = "1.2.840.113549.1.7.1";
constexpr absl::string_view kOidSigningTime = "1.2.840.113549.1.9.5";
constexpr absl::string_view kOidSmimeCaps = "1.2.840.113549.1.9.15";
constexpr absl::string_view kOidAes256Cbc = "2.16.840.1.101.3.4.1.42";
constexpr absl::string_view kOidAes192Cbc = "2.16.840.1.101.3.4.1.22";
constexpr absl::string_view kOidAes128Cbc = "2.16.840.1.101.3.4.1.2";
constexpr absl::string_view kOidDesEde3Cbc = "1.2.840.113549.3.7";
constexpr absl::string_view kOidRsaesOaep = "1.2.840.113549.1.1.7";
constexpr absl::string_view kOidSha256 = "2.16.840.1.101.3.4.2.1";
constexpr absl::string_view kOidSignedData = "1.2.840.113549.1.7.2";
constexpr absl::string_view kOidRsaEncryption = "1.2.840.113549.1.1.1";
bool SerializeCertificate(const X509& certificate, std::vector<uint8_t>* data) {
int len = i2d_X509(&certificate, nullptr);
if (len <= 0) {
return false;
}
data->resize(len);
uint8_t* dataptr = data->data();
return i2d_X509(&certificate, &dataptr) == len;
}
bool SerializeCertificateIssuerName(const X509& certificate,
std::vector<uint8_t>* data) {
int len = i2d_X509_NAME(X509_get_issuer_name(&certificate), nullptr);
if (len <= 0) {
return false;
}
data->resize(len);
uint8_t* dataptr = data->data();
return i2d_X509_NAME(X509_get_issuer_name(&certificate), &dataptr) == len;
}
bool SerializeCertificateSerialNumber(const X509& certificate,
std::vector<uint8_t>* data) {
const ASN1_INTEGER* serial_asn1 = X509_get0_serialNumber(&certificate);
if (serial_asn1 == nullptr) {
return false;
}
int len = i2d_ASN1_INTEGER(serial_asn1, nullptr);
if (len <= 0) {
return false;
}
data->resize(len);
uint8_t* dataptr = data->data();
return i2d_ASN1_INTEGER(serial_asn1, &dataptr) == len;
}
bool SerializeSigningTime(time_t signing_time, std::vector<uint8_t>* data) {
bssl::UniquePtr<ASN1_TIME> asn1_time(ASN1_TIME_set(nullptr, signing_time));
if (asn1_time == nullptr) {
return false;
}
int len = i2d_ASN1_TIME(asn1_time.get(), nullptr);
if (len <= 0) {
return false;
}
data->resize(len);
uint8_t* dataptr = data->data();
return i2d_ASN1_TIME(asn1_time.get(), &dataptr) == len;
}
bool AddOid(CBB* cbb, absl::string_view oid) {
return CBB_add_asn1_oid_from_text(cbb, oid.data(), oid.length());
}
} // namespace
absl::StatusOr<std::string> CreateSignedCms(
absl::Span<const absl::string_view> contents,
const X509& signer_certificate,
absl::FunctionRef<absl::StatusOr<std::vector<uint8_t>>(absl::string_view)>
sign_callback,
const SignedCmsOptions& options) {
ABSL_ASSIGN_OR_RETURN(auto hasher,
CreateHasher(credentio::HashAlgorithm::kSha256));
for (const auto& part : contents) {
hasher->Update(part);
}
std::string hash = hasher->Digest();
std::vector<uint8_t> signing_time_der, issuer_name, serial_number;
if (!SerializeCertificateIssuerName(signer_certificate, &issuer_name) ||
!SerializeCertificateSerialNumber(signer_certificate, &serial_number) ||
!SerializeSigningTime(absl::ToUnixSeconds(options.signing_time),
&signing_time_der)) {
return absl::InternalError("Failed to serialize certificate components.");
}
bssl::ScopedCBB attributes;
CBB att_seq, oid, value_set, value;
if (CBB_init(attributes.get(), 1024) != 1) {
return absl::InternalError("Memory allocation failed for attributes.");
}
// Message Digest
if (CBB_add_asn1(attributes.get(), &att_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&att_seq, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidMessageDigest) ||
CBB_add_asn1(&att_seq, &value_set, CBS_ASN1_SET) != 1 ||
CBB_add_asn1(&value_set, &value, CBS_ASN1_OCTETSTRING) != 1 ||
CBB_add_bytes(&value, reinterpret_cast<const uint8_t*>(hash.data()),
hash.size()) != 1) {
return absl::InternalError("Failed to add message digest attribute.");
}
// Content Type
if (CBB_add_asn1(attributes.get(), &att_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&att_seq, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidContentType) ||
CBB_add_asn1(&att_seq, &value_set, CBS_ASN1_SET) != 1 ||
CBB_add_asn1(&value_set, &value, CBS_ASN1_OBJECT) != 1) {
return absl::InternalError("Failed to add content type attribute.");
}
if (options.is_time_stamp_token) {
if (!AddOid(&value, kOidTstInfo)) {
return absl::InternalError("Failed to add TSTInfo OID.");
}
} else {
if (!AddOid(&value, kOidData)) {
return absl::InternalError("Failed to add data OID.");
}
}
// Signing Time
if (CBB_add_asn1(attributes.get(), &att_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&att_seq, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidSigningTime) ||
CBB_add_asn1(&att_seq, &value_set, CBS_ASN1_SET) != 1 ||
CBB_add_bytes(&value_set, signing_time_der.data(),
signing_time_der.size()) != 1) {
return absl::InternalError("Failed to add signing time attribute.");
}
// SMIME-CAPS
CBB cap_seq, cap_item;
if (CBB_add_asn1(attributes.get(), &att_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&att_seq, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidSmimeCaps) ||
CBB_add_asn1(&att_seq, &value_set, CBS_ASN1_SET) != 1 ||
CBB_add_asn1(&value_set, &cap_seq, CBS_ASN1_SEQUENCE) != 1) {
return absl::InternalError("Failed to add SMIME-CAPS attribute.");
}
// AES-256-CBC
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidAes256Cbc)) {
return absl::InternalError("Failed to add AES-256-CBC capability.");
}
// AES-192-CBC
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidAes192Cbc)) {
return absl::InternalError("Failed to add AES-192-CBC capability.");
}
// AES-128-CBC
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidAes128Cbc)) {
return absl::InternalError("Failed to add AES-128-CBC capability.");
}
// DES-EDE3-CBC
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidDesEde3Cbc)) {
return absl::InternalError("Failed to add DES-EDE3-CBC capability.");
}
// RSAES-OAEP
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidRsaesOaep)) {
return absl::InternalError("Failed to add RSAES-OAEP capability.");
}
// SHA256
if (CBB_add_asn1(&cap_seq, &cap_item, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&cap_item, &oid, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&oid, kOidSha256)) {
return absl::InternalError("Failed to add SHA256 capability.");
}
// Extra Signed Attributes
if (!options.extra_signed_attributes.empty()) {
if (CBB_add_bytes(attributes.get(),
reinterpret_cast<const uint8_t*>(
options.extra_signed_attributes.data()),
options.extra_signed_attributes.length()) != 1) {
return absl::InternalError("Failed to add extra signed attributes.");
}
}
uint8_t* raw_attr_data;
size_t raw_attr_len;
if (CBB_finish(attributes.get(), &raw_attr_data, &raw_attr_len) != 1) {
return absl::InternalError("Failed to finish attributes CBB.");
}
bssl::UniquePtr<uint8_t> raw_attr_data_uniq(raw_attr_data);
// Sign the attributes. The signature is computed over the SET of attributes.
bssl::ScopedCBB pk_signed_attributes;
CBB pk_attributes_set;
if (CBB_init(pk_signed_attributes.get(), raw_attr_len + 5) != 1 ||
CBB_add_asn1(pk_signed_attributes.get(), &pk_attributes_set,
CBS_ASN1_SET) != 1 ||
CBB_add_bytes(&pk_attributes_set, raw_attr_data, raw_attr_len) != 1 ||
CBB_flush(pk_signed_attributes.get()) != 1) {
return absl::InternalError("Failed to prepare attributes for signing.");
}
ABSL_ASSIGN_OR_RETURN(
std::vector<uint8_t> signature,
sign_callback(absl::string_view(
reinterpret_cast<const char*>(CBB_data(pk_signed_attributes.get())),
CBB_len(pk_signed_attributes.get()))));
// Assemble CMS
bssl::ScopedCBB cms;
CBB content_type, signed_data, digest_algs, digest_alg_seq, content_seq,
certs, algo_params, temp, signed_seq;
if (CBB_init(cms.get(), 2048) != 1) {
return absl::InternalError("Memory allocation for CMS failed.");
}
uint64_t version = options.is_time_stamp_token ? 3 : 1;
if (CBB_add_asn1(cms.get(), &content_type, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&content_type, &temp, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&temp, kOidSignedData) ||
CBB_add_asn1(&content_type, &signed_data,
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC) != 1 ||
CBB_add_asn1(&signed_data, &signed_seq, CBS_ASN1_SEQUENCE) != 1 ||
// Version
CBB_add_asn1_uint64(&signed_seq, version) != 1 ||
// Digest algorithms
CBB_add_asn1(&signed_seq, &digest_algs, CBS_ASN1_SET) != 1 ||
CBB_add_asn1(&digest_algs, &digest_alg_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&digest_alg_seq, &temp, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&temp, kOidSha256) ||
CBB_add_asn1(&digest_alg_seq, &algo_params, CBS_ASN1_NULL) != 1 ||
// Content
CBB_add_asn1(&signed_seq, &content_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&content_seq, &temp, CBS_ASN1_OBJECT) != 1) {
return absl::InternalError("Failed to build CMS header.");
}
if (options.is_time_stamp_token) {
if (!AddOid(&temp, kOidTstInfo)) {
return absl::InternalError("Failed to add TSTInfo OID to header.");
}
} else {
if (!AddOid(&temp, kOidData)) {
return absl::InternalError("Failed to add data OID to header.");
}
}
// Opaque Content
CBB octet_stream;
if (CBB_add_asn1(&content_seq, &octet_stream,
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC) != 1 ||
CBB_add_asn1(&octet_stream, &temp, CBS_ASN1_OCTETSTRING) != 1) {
return absl::InternalError("Failed to add opaque content wrapper.");
}
for (const auto& part : contents) {
if (CBB_add_bytes(&temp, reinterpret_cast<const uint8_t*>(part.data()),
part.size()) != 1) {
return absl::InternalError("Failed to add opaque content bytes.");
}
}
// Certificates
if (options.include_user_certificate || !options.ca_certificates.empty()) {
if (CBB_add_asn1(&signed_seq, &certs,
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC) != 1) {
return absl::InternalError("Failed to add certificates wrapper.");
}
std::vector<uint8_t> der;
for (const X509* cert : options.ca_certificates) {
if (!SerializeCertificate(*cert, &der)) {
return absl::InternalError("Failed to serialize CA certificate.");
}
if (CBB_add_bytes(&certs, der.data(), der.size()) != 1) {
return absl::InternalError("Failed to add CA certificate.");
}
}
if (options.include_user_certificate) {
if (!SerializeCertificate(signer_certificate, &der)) {
return absl::InternalError("Failed to serialize user certificate.");
}
if (CBB_add_bytes(&certs, der.data(), der.size()) != 1) {
return absl::InternalError("Failed to add user certificate.");
}
}
}
// Signer Info
CBB signer_info_set, signer_info, issuer_and_sn;
if (CBB_add_asn1(&signed_seq, &signer_info_set, CBS_ASN1_SET) != 1 ||
CBB_add_asn1(&signer_info_set, &signer_info, CBS_ASN1_SEQUENCE) != 1 ||
// Version
CBB_add_asn1_uint64(&signer_info, 1) != 1 ||
CBB_add_asn1(&signer_info, &issuer_and_sn, CBS_ASN1_SEQUENCE) != 1 ||
// Signer info: Signer identifier: Issuer and serial number
CBB_add_bytes(&issuer_and_sn, issuer_name.data(), issuer_name.size()) !=
1 ||
CBB_add_bytes(&issuer_and_sn, serial_number.data(),
serial_number.size()) != 1 ||
// Signer info: Digest algorithm
CBB_add_asn1(&signer_info, &digest_alg_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&digest_alg_seq, &temp, CBS_ASN1_OBJECT) != 1 ||
!AddOid(&temp, kOidSha256) ||
CBB_add_asn1(&digest_alg_seq, &algo_params, CBS_ASN1_NULL) != 1 ||
// Signer info: Signed attributes
CBB_add_asn1(&signer_info, &temp,
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC) != 1 ||
CBB_add_bytes(&temp, raw_attr_data, raw_attr_len) != 1 ||
// Signer info: Signature algorithm
CBB_add_asn1(&signer_info, &digest_alg_seq, CBS_ASN1_SEQUENCE) != 1 ||
CBB_add_asn1(&digest_alg_seq, &temp, CBS_ASN1_OBJECT) != 1) {
return absl::InternalError("Failed to build SignerInfo.");
}
// Hardcode RsaEncryption for now to match simple_cms behavior in tests, as
// TimestampVerifier doesn't strictly check this.
if (!AddOid(&temp, kOidRsaEncryption) ||
CBB_add_asn1(&digest_alg_seq, &algo_params, CBS_ASN1_NULL) != 1 ||
// Signer info: Signature
CBB_add_asn1(&signer_info, &temp, CBS_ASN1_OCTETSTRING) != 1 ||
CBB_add_bytes(&temp, signature.data(), signature.size()) != 1) {
return absl::InternalError("Failed to add signature to SignerInfo.");
}
uint8_t* cms_data;
size_t cms_len;
if (CBB_finish(cms.get(), &cms_data, &cms_len) != 1) {
return absl::InternalError("Failed to finish CMS CBB.");
}
bssl::UniquePtr<uint8_t> cms_data_uniq(cms_data);
return std::string(reinterpret_cast<char*>(cms_data), cms_len);
}
} // namespace credentio_testing