| // 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 |