| // 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 "crypto/default/x509_certificate.h" | |
| #include <cstdint> | |
| #include <ctime> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/no_destructor.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/substitute.h" | |
| #include "absl/time/time.h" | |
| #include "crypto/algorithms.h" | |
| #include "crypto/default/compliance_checker.h" | |
| #include "openssl/asn1.h" | |
| #include "openssl/base.h" | |
| #include "openssl/bio.h" | |
| #include "openssl/bn.h" | |
| #include "openssl/evp.h" | |
| #include "openssl/obj.h" | |
| #include "openssl/pem.h" | |
| #include "openssl/pool.h" | |
| #include "openssl/rsa.h" | |
| #include "openssl/x509.h" | |
| #include "proto/common.pb.h" | |
| #include "tink/cleartext_keyset_handle.h" | |
| #include "tink/public_key_verify.h" | |
| #include "tink/signature/config_v0.h" | |
| #include "tink/signature/signature_pem_keyset_reader.h" | |
| namespace credentio { | |
| namespace { | |
| using ::crypto::tink::PemKeyParams; | |
| using ::crypto::tink::SignaturePemKeysetReaderBuilder; | |
| // c2pa-al from | |
| // https://github.com/c2pa-org/conformance-public/blob/main/docs/current/schemas/mib/oid.txt | |
| constexpr char kAssuranceLevelOid[] = "1.3.6.1.4.1.62558.3"; | |
| // c2pa-cpl-record from | |
| // https://github.com/c2pa-org/conformance-public/blob/main/docs/current/schemas/mib/oid.txt | |
| constexpr char kConformanceRecordOid[] = "1.3.6.1.4.1.62558.4"; | |
| absl::Status CheckCompatibleWithAlgorithm(EVP_PKEY* pub_key, | |
| SigningAlgorithm algorithm) { | |
| int key_type = EVP_PKEY_id(pub_key); | |
| switch (algorithm) { | |
| case SigningAlgorithm::kPs256: | |
| case SigningAlgorithm::kPs384: | |
| case SigningAlgorithm::kPs512: | |
| if (key_type == EVP_PKEY_RSA || key_type == EVP_PKEY_RSA_PSS) { | |
| return absl::OkStatus(); | |
| } | |
| break; | |
| case SigningAlgorithm::kEs256: | |
| case SigningAlgorithm::kEs384: | |
| case SigningAlgorithm::kEs512: | |
| if (key_type == EVP_PKEY_EC) { | |
| return absl::OkStatus(); | |
| } | |
| break; | |
| case SigningAlgorithm::kEdDsa: | |
| if (key_type == EVP_PKEY_ED25519) { | |
| return absl::OkStatus(); | |
| } | |
| break; | |
| } | |
| return absl::InvalidArgumentError(absl::Substitute( | |
| "Wrong key type ($0) for algorithm $1", key_type, algorithm)); | |
| } | |
| // Returns a list of C2PA supported algorithms. | |
| // See | |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#certificate-requirements. | |
| // Note: bssl::GetDefaultEVPAlgorithms() does not include RSA_PSS. | |
| const std::vector<const EVP_PKEY_ALG*>& GetC2paEVPAlgorithms() { | |
| static const absl::NoDestructor<std::vector<const EVP_PKEY_ALG*>> kAlgorithms( | |
| { | |
| EVP_pkey_ec_p256(), | |
| EVP_pkey_ec_p384(), | |
| EVP_pkey_ec_p521(), | |
| EVP_pkey_rsa(), | |
| EVP_pkey_rsa_pss_sha256(), | |
| EVP_pkey_rsa_pss_sha384(), | |
| EVP_pkey_rsa_pss_sha512(), | |
| EVP_pkey_ed25519(), | |
| }); | |
| return *kAlgorithms; | |
| } | |
| absl::StatusOr<bssl::UniquePtr<X509>> GetCertificateFromDER( | |
| absl::string_view der_data) { | |
| auto crypto_buffer = bssl::UniquePtr<CRYPTO_BUFFER>( | |
| CRYPTO_BUFFER_new(reinterpret_cast<const uint8_t*>(der_data.data()), | |
| der_data.length(), /*pool=*/nullptr)); | |
| const auto& algorithms = GetC2paEVPAlgorithms(); | |
| bssl::UniquePtr<X509> cert(X509_parse_with_algorithms( | |
| crypto_buffer.get(), algorithms.data(), algorithms.size())); | |
| if (cert == nullptr) { | |
| return absl::InvalidArgumentError( | |
| "Failed to parse the X.509 certificate from DER bytes"); | |
| } | |
| return cert; | |
| } | |
| absl::StatusOr<std::string> PemEncode(EVP_PKEY* pub_key) { | |
| bssl::UniquePtr<BIO> bio(BIO_new(BIO_s_mem())); | |
| if (PEM_write_bio_PUBKEY(bio.get(), pub_key) != 1) { | |
| return absl::InternalError("Failed to write pub key to BIO"); | |
| } | |
| const uint8_t* pem; | |
| size_t pem_length = 0; | |
| if (BIO_mem_contents(bio.get(), &pem, &pem_length) != 1) { | |
| return absl::InternalError("Failed to get PEM string from BIO"); | |
| } | |
| std::string pem_str(reinterpret_cast<const char*>(pem), pem_length); | |
| return pem_str; | |
| } | |
| bssl::UniquePtr<EVP_PKEY> MaybeConvertPssToRsa( | |
| bssl::UniquePtr<EVP_PKEY> pub_key) { | |
| // Tink uses BoringSSL to parse public keys from PEM, which does not support | |
| // id-RSASSA-PSS by default. Since Tink uses key templates, it doesn't care if | |
| // the original public key had the "restricted" RSA-PSS OID or the "standard" | |
| // RSA OID. It only cares about the modulus and exponent, then applies the PSS | |
| // logic defined in the GetPemKeyParams. Therefore, for compatibility, we turn | |
| // the "restricted" RSA-PSS public key to a "generic" RSA public key. | |
| RSA* rsa_params = EVP_PKEY_get1_RSA(pub_key.get()); | |
| if (rsa_params != nullptr) { | |
| // Ideally, we should check if it is a RSA_PSS key. However, BoringSSL does | |
| // not implement RSA_get0_pss_params yet. | |
| bssl::UniquePtr<EVP_PKEY> rsa_key(EVP_PKEY_new()); | |
| EVP_PKEY_assign_RSA(rsa_key.get(), rsa_params); | |
| return rsa_key; | |
| } | |
| return pub_key; | |
| } | |
| std::string X509NamePrintEx(const X509_NAME* name, uint64_t flags) { | |
| if (!name) return ""; | |
| bssl::UniquePtr<BIO> bio(BIO_new(BIO_s_mem())); | |
| X509_NAME_print_ex(bio.get(), name, 0, flags); | |
| int size = BIO_pending(bio.get()); | |
| std::string buffer(size, '\0'); | |
| int bytes_read = BIO_read(bio.get(), buffer.data(), size); | |
| if (bytes_read != size) { | |
| return ""; | |
| } | |
| return buffer; | |
| } | |
| } // namespace | |
| std::string X509Certificate::GetSubject() const { | |
| return X509NamePrintEx(X509_get_subject_name(cert_.get()), XN_FLAG_RFC2253); | |
| } | |
| std::string X509Certificate::GetIssuer() const { | |
| return X509NamePrintEx(X509_get_issuer_name(cert_.get()), XN_FLAG_RFC2253); | |
| } | |
| absl::StatusOr<std::unique_ptr<X509Certificate>> X509Certificate::Create( | |
| absl::string_view der) { | |
| ABSL_ASSIGN_OR_RETURN(auto x509, GetCertificateFromDER(der)); | |
| return std::make_unique<X509Certificate>(std::move(x509)); | |
| } | |
| absl::Status X509Certificate::IsValidC2paCertificate(bool is_leaf) const { | |
| return ComplianceChecker::IsValidC2paCertificate(cert_.get(), is_leaf); | |
| } | |
| absl::Status X509Certificate::VerifySignature( | |
| absl::string_view signature, absl::string_view data, | |
| SigningAlgorithm algorithm) const { | |
| bssl::UniquePtr<EVP_PKEY> pub_key(X509_get_pubkey(cert_.get())); | |
| if (pub_key == nullptr) { | |
| return absl::InvalidArgumentError( | |
| "Certificate does not have a valid public key"); | |
| } | |
| pub_key = MaybeConvertPssToRsa(std::move(pub_key)); | |
| ABSL_ASSIGN_OR_RETURN(auto pem_str, PemEncode(pub_key.get())); | |
| auto builder = SignaturePemKeysetReaderBuilder( | |
| SignaturePemKeysetReaderBuilder::PemReaderType::PUBLIC_KEY_VERIFY); | |
| auto params = GetPemKeyParams(algorithm); | |
| if (!params.ok()) { | |
| return absl::Status(params.status().code(), | |
| absl::StrCat(params.status().message(), | |
| "; Failed to get PEM key params")); | |
| } | |
| for (const auto& param : *params) { | |
| builder.Add({.serialized_key = pem_str, .parameters = param}); | |
| } | |
| auto reader = builder.Build(); | |
| if (!reader.ok()) { | |
| return absl::Status( | |
| reader.status().code(), | |
| absl::StrCat(reader.status().message(), | |
| "; Failed to construct the PEM KeysetReader")); | |
| } | |
| auto handle = crypto::tink::CleartextKeysetHandle::Read(*std::move(reader)); | |
| if (!handle.ok()) { | |
| return absl::Status(handle.status().code(), | |
| absl::StrCat(handle.status().message(), | |
| "; Failed to construct the KeysetHandle")); | |
| } | |
| auto verifier = (*handle)->GetPrimitive<crypto::tink::PublicKeyVerify>( | |
| crypto::tink::ConfigSignatureV0()); | |
| if (!verifier.ok()) { | |
| return absl::Status( | |
| verifier.status().code(), | |
| absl::StrCat(verifier.status().message(), | |
| "; Failed to construct the PublicKeyVerify primitive")); | |
| } | |
| auto status = (*verifier)->Verify(signature, data); | |
| if (!status.ok()) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "Tink verification failed with the status error: ", status.message())); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| std::string X509Certificate::DebugString() const { | |
| bssl::UniquePtr<BIO> bio(BIO_new(BIO_s_mem())); | |
| if (!X509_print(bio.get(), cert_.get())) { | |
| return "<X509_print failed>"; | |
| } | |
| const uint8_t* data; | |
| size_t len; | |
| if (!BIO_mem_contents(bio.get(), &data, &len) || len == 0) { | |
| return "BIO_mem_contents failed"; | |
| } | |
| return std::string(reinterpret_cast<const char*>(data), len); | |
| } | |
| absl::StatusOr<absl::Time> X509Certificate::StartTime() const { | |
| const ASN1_TIME* not_before = X509_get0_notBefore(cert_.get()); | |
| if (not_before == nullptr) { | |
| return absl::InternalError("failed to get certificate start time"); | |
| } | |
| int64_t posix_time; | |
| if (ASN1_TIME_to_posix(not_before, &posix_time) == 0) { | |
| return absl::InternalError("Invalid certificate start time"); | |
| } | |
| return absl::FromUnixSeconds(posix_time); | |
| } | |
| absl::StatusOr<absl::Time> X509Certificate::EndTime() const { | |
| int64_t posix_time; | |
| const ASN1_TIME* not_after = X509_get0_notAfter(cert_.get()); | |
| if (not_after == nullptr) { | |
| return absl::InternalError("failed to get certificate end time"); | |
| } | |
| if (ASN1_TIME_to_posix(not_after, &posix_time) == 0) { | |
| return absl::InternalError("Invalid certificate end time"); | |
| } | |
| return absl::FromUnixSeconds(posix_time); | |
| } | |
| absl::StatusOr<std::vector<PemKeyParams>> X509Certificate::GetPemKeyParams( | |
| SigningAlgorithm algorithm) const { | |
| bssl::UniquePtr<EVP_PKEY> pub_key(X509_get_pubkey(cert_.get())); | |
| if (pub_key == nullptr) { | |
| return absl::InvalidArgumentError( | |
| "Certificate does not have a valid public key"); | |
| } | |
| ABSL_RETURN_IF_ERROR(CheckCompatibleWithAlgorithm(pub_key.get(), algorithm)); | |
| size_t key_size = EVP_PKEY_bits(pub_key.get()); | |
| switch (algorithm) { | |
| case SigningAlgorithm::kPs256: | |
| return std::vector<PemKeyParams>{PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_RSA, | |
| .algorithm = crypto::tink::PemAlgorithm::RSASSA_PSS, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA256, | |
| }}; | |
| case SigningAlgorithm::kPs384: | |
| return std::vector<PemKeyParams>{PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_RSA, | |
| .algorithm = crypto::tink::PemAlgorithm::RSASSA_PSS, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA384, | |
| }}; | |
| case SigningAlgorithm::kPs512: | |
| return std::vector<PemKeyParams>{PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_RSA, | |
| .algorithm = crypto::tink::PemAlgorithm::RSASSA_PSS, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA512, | |
| }}; | |
| case SigningAlgorithm::kEs256: | |
| return std::vector<PemKeyParams>{ | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_IEEE, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA256, | |
| }, | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_DER, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA256, | |
| }}; | |
| case SigningAlgorithm::kEs384: | |
| return std::vector<PemKeyParams>{ | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_IEEE, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA384, | |
| }, | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_DER, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA384, | |
| }}; | |
| case SigningAlgorithm::kEs512: | |
| return std::vector<PemKeyParams>{ | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_IEEE, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA512, | |
| }, | |
| PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ECDSA_DER, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA512, | |
| }, | |
| }; | |
| case SigningAlgorithm::kEdDsa: | |
| return std::vector<PemKeyParams>{PemKeyParams{ | |
| .key_type = crypto::tink::PemKeyType::PEM_EC, | |
| .algorithm = crypto::tink::PemAlgorithm::ED25519, | |
| .key_size_in_bits = key_size, | |
| .hash_type = google::crypto::tink::HashType::SHA512, | |
| }}; | |
| } | |
| } | |
| absl::StatusOr<std::string> X509Certificate::GetSerialNumberHex() const { | |
| const ASN1_INTEGER* serial = X509_get0_serialNumber(cert_.get()); | |
| if (serial == nullptr) { | |
| return absl::InternalError("failed to get certificate serial number"); | |
| } | |
| bssl::UniquePtr<BIGNUM> bn(ASN1_INTEGER_to_BN(serial, nullptr)); | |
| if (!bn) { | |
| return absl::InternalError("failed to convert serial number to BIGNUM"); | |
| } | |
| bssl::UniquePtr<char> hex(BN_bn2hex(bn.get())); | |
| if (!hex) { | |
| return absl::InternalError("failed to convert BIGNUM to hex"); | |
| } | |
| return absl::AsciiStrToLower(hex.get()); | |
| } | |
| absl::StatusOr<std::string> X509Certificate::GetAssuranceLevel() const { | |
| static const ASN1_OBJECT* const assurance_level_oid = []() { | |
| return OBJ_txt2obj(kAssuranceLevelOid, /*dont_search_names=*/1); | |
| }(); | |
| if (assurance_level_oid == nullptr) { | |
| return absl::InternalError("Failed to parse assurance level OID"); | |
| } | |
| int index = X509_get_ext_by_OBJ(cert_.get(), assurance_level_oid, -1); | |
| if (index == -1) { | |
| return absl::NotFoundError("assurance level extension not found"); | |
| } | |
| const X509_EXTENSION* extension = X509_get_ext(cert_.get(), index); | |
| if (extension == nullptr) { | |
| return absl::NotFoundError("cannot extract assurance level extension"); | |
| } | |
| const ASN1_OCTET_STRING* octet_str = X509_EXTENSION_get_data(extension); | |
| if (octet_str == nullptr) { | |
| return absl::NotFoundError("cannot extract assurance level extension data"); | |
| } | |
| const unsigned char* p = ASN1_STRING_get0_data(octet_str); | |
| int64_t len = ASN1_STRING_length(octet_str); | |
| bssl::UniquePtr<ASN1_OBJECT> val_obj(d2i_ASN1_OBJECT(nullptr, &p, len)); | |
| if (!val_obj) { | |
| return absl::NotFoundError("cannot parse assurance level value"); | |
| } | |
| char buf[128]; | |
| int res = OBJ_obj2txt(buf, sizeof(buf), val_obj.get(), /*no_name=*/1); | |
| if (res <= 0) { | |
| return absl::NotFoundError( | |
| "cannot convert assurance level value to string"); | |
| } | |
| return std::string(buf); | |
| } | |
| absl::StatusOr<std::string> X509Certificate::GetConformingProductId() const { | |
| static const ASN1_OBJECT* const conformance_record_oid = []() { | |
| return OBJ_txt2obj(kConformanceRecordOid, /*dont_search_names=*/1); | |
| }(); | |
| if (conformance_record_oid == nullptr) { | |
| return absl::InternalError("Failed to parse conformance record OID"); | |
| } | |
| int index = X509_get_ext_by_OBJ(cert_.get(), conformance_record_oid, -1); | |
| if (index == -1) { | |
| return absl::NotFoundError("Conformance record extension not found"); | |
| } | |
| const X509_EXTENSION* extension = X509_get_ext(cert_.get(), index); | |
| if (extension == nullptr) { | |
| return absl::NotFoundError( | |
| "Failed to retrieve conformance record extension"); | |
| } | |
| const ASN1_OCTET_STRING* octet_str = X509_EXTENSION_get_data(extension); | |
| if (octet_str == nullptr) { | |
| return absl::NotFoundError("Conformance record extension data is null"); | |
| } | |
| const unsigned char* p = ASN1_STRING_get0_data(octet_str); | |
| int64_t len = ASN1_STRING_length(octet_str); | |
| bssl::UniquePtr<ASN1_UTF8STRING> utf8_str( | |
| d2i_ASN1_UTF8STRING(nullptr, &p, len)); | |
| if (!utf8_str) { | |
| return absl::NotFoundError("Failed to parse UUID as UTF8String"); | |
| } | |
| return std::string( | |
| reinterpret_cast<const char*>(ASN1_STRING_get0_data(utf8_str.get())), | |
| ASN1_STRING_length(utf8_str.get())); | |
| } | |
| } // namespace credentio |