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