| // 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 <variant> | |
| #include <vector> | |
| #include "absl/base/no_destructor.h" | |
| #include "absl/functional/overload.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/key_status.h" | |
| #include "tink/keyset_handle.h" | |
| #include "tink/partial_key_access.h" | |
| #include "tink/pem/signature_key_parser.h" | |
| #include "tink/public_key_verify.h" | |
| #include "tink/signature/config_v0.h" | |
| #include "tink/signature/ecdsa_parameters.h" | |
| #include "tink/signature/ecdsa_public_key.h" | |
| #include "tink/signature/ed25519_parameters.h" | |
| #include "tink/signature/ed25519_public_key.h" | |
| #include "tink/signature/rsa_ssa_pss_parameters.h" | |
| #include "tink/signature/rsa_ssa_pss_public_key.h" | |
| #include "tink/signature/signature_config.h" | |
| namespace credentio { | |
| namespace { | |
| using ::crypto::tink::BigInteger; | |
| using ::crypto::tink::EcdsaParameters; | |
| using ::crypto::tink::Ed25519Parameters; | |
| using ::crypto::tink::GetPartialKeyAccess; | |
| using ::crypto::tink::KeysetHandleBuilder; | |
| using ::crypto::tink::RsaSsaPssParameters; | |
| // 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::VerifyCoseSignature( | |
| absl::string_view signature, absl::string_view data, | |
| SigningAlgorithm algorithm) const { | |
| static const absl::NoDestructor<absl::Status> registration_status( | |
| []() { return crypto::tink::SignatureConfig::Register(); }()); | |
| ABSL_RETURN_IF_ERROR(*registration_status); | |
| 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())); | |
| absl::StatusOr<std::vector<X509Certificate::TinkKeyParameters>> params = | |
| GetPemKeyParams(algorithm); | |
| if (!params.ok()) { | |
| return absl::Status( | |
| params.status().code(), | |
| absl::StrCat("Tink verification failed: ", params.status().message())); | |
| } | |
| crypto::tink::KeysetHandleBuilder builder; | |
| bool is_primary = true; | |
| using TinkPublicKey = std::variant<crypto::tink::EcdsaPublicKey, | |
| crypto::tink::RsaSsaPssPublicKey, | |
| crypto::tink::Ed25519PublicKey>; | |
| for (const auto& param : *params) { | |
| absl::StatusOr<TinkPublicKey> public_key = | |
| std::visit(absl::Overload( | |
| [&pem_str](const EcdsaParameters& params) | |
| -> absl::StatusOr<TinkPublicKey> { | |
| return tink_pem::PemToEcdsaPublicKey( | |
| pem_str, params, GetPartialKeyAccess()); | |
| }, | |
| [&pem_str](const RsaSsaPssParameters& params) | |
| -> absl::StatusOr<TinkPublicKey> { | |
| return tink_pem::PemToRsaSsaPssPublicKey( | |
| pem_str, params, GetPartialKeyAccess()); | |
| }, | |
| [&pem_str](const Ed25519Parameters& params) | |
| -> absl::StatusOr<TinkPublicKey> { | |
| return tink_pem::PemToEd25519PublicKey( | |
| pem_str, params, GetPartialKeyAccess()); | |
| }), | |
| param); | |
| if (!public_key.ok()) { | |
| return public_key.status(); | |
| } | |
| std::visit( | |
| [&builder, is_primary](const auto& key) { | |
| builder.AddEntry(KeysetHandleBuilder::Entry::CreateFromCopyableKey( | |
| key, crypto::tink::KeyStatus::kEnabled, is_primary)); | |
| }, | |
| *public_key); | |
| is_primary = false; | |
| } | |
| auto handle = builder.Build(); | |
| 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); | |
| } | |
| namespace { | |
| absl::StatusOr<RsaSsaPssParameters> GetRsaSsaPssParameters( | |
| size_t key_size_in_bits, SigningAlgorithm algorithm, | |
| const BigInteger& public_exponent) { | |
| crypto::tink::RsaSsaPssParameters::HashType hash_type; | |
| int salt_length; | |
| switch (algorithm) { | |
| case SigningAlgorithm::kPs256: | |
| hash_type = RsaSsaPssParameters::HashType::kSha256; | |
| salt_length = 32; | |
| break; | |
| case SigningAlgorithm::kPs384: | |
| hash_type = RsaSsaPssParameters::HashType::kSha384; | |
| salt_length = 48; | |
| break; | |
| case SigningAlgorithm::kPs512: | |
| hash_type = RsaSsaPssParameters::HashType::kSha512; | |
| salt_length = 64; | |
| break; | |
| default: | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid signing algorithm: ", algorithm)); | |
| } | |
| return RsaSsaPssParameters::Builder() | |
| .SetModulusSizeInBits(key_size_in_bits) | |
| .SetPublicExponent(public_exponent) | |
| .SetSigHashType(hash_type) | |
| .SetMgf1HashType(hash_type) | |
| .SetSaltLengthInBytes(salt_length) | |
| .SetVariant(RsaSsaPssParameters::Variant::kNoPrefix) | |
| .Build(); | |
| } | |
| absl::StatusOr<BigInteger> GetPublicExponent(EVP_PKEY& pub_key) { | |
| RSA* rsa = EVP_PKEY_get0_RSA(&pub_key); | |
| if (rsa == nullptr) { | |
| return absl::InvalidArgumentError("Invalid RSA public key"); | |
| } | |
| const BIGNUM* e = nullptr; | |
| RSA_get0_key(rsa, /*n=*/nullptr, &e, /*d=*/nullptr); | |
| if (e == nullptr) { | |
| return absl::InvalidArgumentError("Invalid RSA public key exponent"); | |
| } | |
| std::string e_bytes(BN_num_bytes(e), '\0'); | |
| BN_bn2bin(e, reinterpret_cast<uint8_t*>(e_bytes.data())); | |
| return BigInteger(e_bytes); | |
| } | |
| absl::StatusOr<EcdsaParameters> GetEcdsaParameters(SigningAlgorithm algorithm) { | |
| EcdsaParameters::HashType hash_type; | |
| EcdsaParameters::CurveType curve_type; | |
| switch (algorithm) { | |
| case SigningAlgorithm::kEs256: | |
| hash_type = EcdsaParameters::HashType::kSha256; | |
| curve_type = EcdsaParameters::CurveType::kNistP256; | |
| break; | |
| case SigningAlgorithm::kEs384: | |
| hash_type = EcdsaParameters::HashType::kSha384; | |
| curve_type = EcdsaParameters::CurveType::kNistP384; | |
| break; | |
| case SigningAlgorithm::kEs512: | |
| hash_type = EcdsaParameters::HashType::kSha512; | |
| curve_type = EcdsaParameters::CurveType::kNistP521; | |
| break; | |
| default: | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid signing algorithm: ", algorithm)); | |
| } | |
| // COSE requires IEEE P1363 encoding. | |
| return EcdsaParameters::Builder() | |
| .SetCurveType(curve_type) | |
| .SetHashType(hash_type) | |
| .SetSignatureEncoding(EcdsaParameters::SignatureEncoding::kIeeeP1363) | |
| .SetVariant(EcdsaParameters::Variant::kNoPrefix) | |
| .Build(); | |
| } | |
| } // namespace | |
| absl::StatusOr<std::vector<X509Certificate::TinkKeyParameters>> | |
| 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)); | |
| switch (algorithm) { | |
| case SigningAlgorithm::kPs256: | |
| case SigningAlgorithm::kPs384: | |
| case SigningAlgorithm::kPs512: { | |
| ABSL_ASSIGN_OR_RETURN(crypto::tink::BigInteger public_exponent, | |
| GetPublicExponent(*pub_key)); | |
| size_t key_size_in_bytes = EVP_PKEY_size(pub_key.get()); | |
| absl::StatusOr<RsaSsaPssParameters> params = GetRsaSsaPssParameters( | |
| key_size_in_bytes * 8, algorithm, public_exponent); | |
| if (!params.ok()) { | |
| return absl::Status(params.status().code(), | |
| absl::StrCat(params.status().message(), | |
| "; Failed to get RSA-PSS parameters")); | |
| } | |
| return std::vector<X509Certificate::TinkKeyParameters>{ | |
| *std::move(params)}; | |
| } | |
| case SigningAlgorithm::kEs256: | |
| case SigningAlgorithm::kEs384: | |
| case SigningAlgorithm::kEs512: { | |
| ABSL_ASSIGN_OR_RETURN(EcdsaParameters params, | |
| GetEcdsaParameters(algorithm)); | |
| return std::vector<X509Certificate::TinkKeyParameters>{params}; | |
| } | |
| case SigningAlgorithm::kEdDsa: { | |
| ABSL_ASSIGN_OR_RETURN( | |
| Ed25519Parameters params, | |
| Ed25519Parameters::Create(Ed25519Parameters::Variant::kNoPrefix)); | |
| return std::vector<X509Certificate::TinkKeyParameters>{params}; | |
| } | |
| default: | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid signing algorithm: ", algorithm)); | |
| } | |
| } | |
| 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 |