Add example binary for testing building the SDK after 'git clone' from our repo. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=227583629
459 lines
14 KiB
C++
459 lines
14 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright 2017 Google LLC.
|
|
//
|
|
// This software is licensed under the terms defined in the Widevine Master
|
|
// License Agreement. For a copy of this agreement, please contact
|
|
// widevine-licensing@google.com.
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "common/x509_cert.h"
|
|
|
|
#include <memory>
|
|
|
|
#include <cstdint>
|
|
#include "glog/logging.h"
|
|
#include "absl/strings/escaping.h"
|
|
#include "absl/synchronization/mutex.h"
|
|
#include "openssl/bio.h"
|
|
#include "openssl/evp.h"
|
|
#include "openssl/pem.h"
|
|
#include "openssl/pkcs7.h"
|
|
#include "openssl/x509.h"
|
|
#include "openssl/x509v3.h"
|
|
#include "common/openssl_util.h"
|
|
#include "common/rsa_key.h"
|
|
|
|
namespace {
|
|
// Serializes the X509 |certificate| into a PEM-encoded string. Returns true
|
|
// on success, false otherwise. The caller retains ownership of
|
|
// |certificate| and |serialized_certificate|. |serialized_certificate| must
|
|
// not be NULL.
|
|
bool PemEncodeX509Certificate(const X509& certificate,
|
|
std::string* serialized_certificate) {
|
|
CHECK(serialized_certificate) << "serialized_certificate can not be null.";
|
|
|
|
ScopedBIO bio(BIO_new(BIO_s_mem()));
|
|
if (bio == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
// The const_cast is necessary for the openssl call.
|
|
PEM_write_bio_X509(bio.get(), const_cast<X509*>(&certificate));
|
|
int serialized_size = BIO_pending(bio.get());
|
|
serialized_certificate->resize(serialized_size);
|
|
if (BIO_read(bio.get(), &(*serialized_certificate)[0], serialized_size) !=
|
|
serialized_size) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // anonymous namespace.
|
|
|
|
namespace widevine {
|
|
|
|
std::unique_ptr<X509Cert> X509Cert::FromOpenSslCert(ScopedX509 certificate) {
|
|
return std::unique_ptr<X509Cert>(new X509Cert(certificate.release()));
|
|
}
|
|
|
|
X509Cert::X509Cert() : openssl_cert_(NULL) {}
|
|
|
|
X509Cert::~X509Cert() {
|
|
if (openssl_cert_ != NULL) {
|
|
X509_free(openssl_cert_);
|
|
}
|
|
}
|
|
|
|
X509Cert::X509Cert(X509* openssl_cert) : openssl_cert_(openssl_cert) {}
|
|
|
|
Status X509Cert::LoadPem(const std::string& pem_cert) {
|
|
if (pem_cert.empty()) {
|
|
return Status(error::INVALID_ARGUMENT, "Empty PEM certificate");
|
|
}
|
|
BIO* bio(NULL);
|
|
X509* new_cert(NULL);
|
|
bio = BIO_new_mem_buf(const_cast<char*>(pem_cert.data()), pem_cert.size());
|
|
if (bio == NULL) {
|
|
return Status(error::INTERNAL, "BIO allocation failed");
|
|
}
|
|
Status status;
|
|
new_cert = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL);
|
|
if (new_cert == NULL) {
|
|
status = Status(error::INVALID_ARGUMENT, "PEM certificate load failed");
|
|
goto cleanup;
|
|
}
|
|
if (openssl_cert_ != NULL) {
|
|
X509_free(openssl_cert_);
|
|
}
|
|
openssl_cert_ = new_cert;
|
|
|
|
cleanup:
|
|
if (bio != NULL) {
|
|
BIO_free(bio);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
Status X509Cert::LoadDer(const std::string& der_cert) {
|
|
if (der_cert.empty()) {
|
|
return Status(error::INVALID_ARGUMENT, "Empty DER certificate");
|
|
}
|
|
const unsigned char* cert_data =
|
|
reinterpret_cast<const unsigned char*>(der_cert.data());
|
|
X509* new_cert = d2i_X509(NULL, &cert_data, der_cert.size());
|
|
if (new_cert == NULL) {
|
|
return Status(error::INVALID_ARGUMENT, "DER certificate load failed");
|
|
}
|
|
if (openssl_cert_ != NULL) {
|
|
X509_free(openssl_cert_);
|
|
}
|
|
openssl_cert_ = new_cert;
|
|
return OkStatus();
|
|
}
|
|
|
|
std::string X509Cert::GetPem() const {
|
|
std::string serialized_certificate;
|
|
if (!PemEncodeX509Certificate(*openssl_cert_, &serialized_certificate)) {
|
|
return "";
|
|
}
|
|
return serialized_certificate;
|
|
}
|
|
|
|
std::unique_ptr<RsaPublicKey> X509Cert::GetRsaPublicKey() const {
|
|
ScopedPKEY pkey(X509_get_pubkey(openssl_cert_));
|
|
return std::unique_ptr<RsaPublicKey>(
|
|
new RsaPublicKey(EVP_PKEY_get1_RSA(pkey.get())));
|
|
}
|
|
|
|
const std::string& X509Cert::GetSubjectName() {
|
|
if (subject_name_.empty() && (openssl_cert_ != NULL)) {
|
|
X509_NAME* subject = X509_get_subject_name(openssl_cert_);
|
|
if (subject != NULL) {
|
|
BIO* bio = BIO_new(BIO_s_mem());
|
|
if (bio != NULL) {
|
|
X509_NAME_print_ex(bio, subject, 0, 0);
|
|
int size = BIO_pending(bio);
|
|
std::unique_ptr<char[]> buffer(new char[size]);
|
|
int bytes_read = BIO_read(bio, buffer.get(), size);
|
|
if (bytes_read == size) {
|
|
subject_name_.assign(buffer.get(), bytes_read);
|
|
}
|
|
BIO_free(bio);
|
|
}
|
|
}
|
|
}
|
|
return subject_name_;
|
|
}
|
|
|
|
std::string X509Cert::GetSubjectNameField(const std::string& field) {
|
|
if (field.empty()) {
|
|
return std::string();
|
|
}
|
|
const std::string& subject = GetSubjectName();
|
|
size_t start_pos = subject.find(field + "=");
|
|
if (start_pos == std::string::npos) {
|
|
return std::string();
|
|
}
|
|
start_pos += field.size() + 1;
|
|
size_t end_pos = subject.find(",", start_pos);
|
|
if (end_pos == std::string::npos) {
|
|
end_pos = subject.size();
|
|
}
|
|
return subject.substr(start_pos, end_pos - start_pos);
|
|
}
|
|
|
|
std::string X509Cert::GetSerialNumber() const {
|
|
if (openssl_cert_ == NULL) {
|
|
return std::string();
|
|
}
|
|
BIGNUM* bn = ASN1_INTEGER_to_BN(X509_get_serialNumber(openssl_cert_), NULL);
|
|
if (bn == NULL) {
|
|
return std::string();
|
|
}
|
|
std::string result;
|
|
char* openssl_sn = BN_bn2hex(bn);
|
|
if (openssl_sn != NULL) {
|
|
result = absl::HexStringToBytes(openssl_sn);
|
|
OPENSSL_free(openssl_sn);
|
|
}
|
|
BN_free(bn);
|
|
return result;
|
|
}
|
|
|
|
bool X509Cert::GetNotBeforeSeconds(int64_t* valid_start_seconds) const {
|
|
if (openssl_cert_ == nullptr) {
|
|
return false;
|
|
}
|
|
return Asn1TimeToEpochSeconds(X509_get0_notBefore(openssl_cert_),
|
|
valid_start_seconds)
|
|
.ok();
|
|
}
|
|
|
|
bool X509Cert::GetNotAfterSeconds(int64_t* valid_end_seconds) const {
|
|
if (openssl_cert_ == nullptr) {
|
|
return false;
|
|
}
|
|
return Asn1TimeToEpochSeconds(X509_get0_notAfter(openssl_cert_),
|
|
valid_end_seconds)
|
|
.ok();
|
|
}
|
|
|
|
bool X509Cert::IsCaCertificate() const {
|
|
return X509_check_ca(openssl_cert_) != 0;
|
|
}
|
|
|
|
bool X509Cert::GetV3BooleanExtension(const std::string& oid, bool* value) const {
|
|
ScopedAsn1Object extension_name(OBJ_txt2obj(oid.c_str(), 1));
|
|
int ext_pos = X509_get_ext_by_OBJ(openssl_cert_, extension_name.get(), -1);
|
|
if (ext_pos < 0) return false;
|
|
X509_EXTENSION* extension(X509_get_ext(openssl_cert_, ext_pos));
|
|
if (!extension) return false;
|
|
ASN1_OCTET_STRING* extension_data(X509_EXTENSION_get_data(extension));
|
|
if (!extension_data) return false;
|
|
if ((extension_data->length != 3) || (extension_data->data[0] != 1) ||
|
|
(extension_data->data[1] != 1))
|
|
return false;
|
|
|
|
*value = extension_data->data[2] != 0;
|
|
return true;
|
|
}
|
|
|
|
Status X509Cert::Asn1TimeToEpochSeconds(const ASN1_TIME* asn1_time,
|
|
int64_t* epoch_seconds) const {
|
|
if (asn1_time == nullptr) {
|
|
// This code is exported to shared source. The exported code does not yet
|
|
// support MakeStatus.
|
|
// NOLINTNEXTLINE
|
|
return Status(error::INVALID_ARGUMENT, "asn1_time cannot be null.");
|
|
}
|
|
|
|
if (epoch_seconds == nullptr) {
|
|
// NOLINTNEXTLINE
|
|
return Status(error::INVALID_ARGUMENT, "epoch_seconds cannot be null.");
|
|
}
|
|
|
|
ScopedAsn1Time epoch_time(ASN1_TIME_new());
|
|
if (!ASN1_TIME_set(epoch_time.get(), 0)) {
|
|
// NOLINTNEXTLINE
|
|
return Status(error::INTERNAL, "Failed to set epoch time.");
|
|
}
|
|
|
|
int day = 0;
|
|
int seconds = 0;
|
|
if (!ASN1_TIME_diff(&day, &seconds, epoch_time.get(), asn1_time)) {
|
|
// NOLINTNEXTLINE
|
|
return Status(error::INTERNAL,
|
|
"Failed to convert asn1 time to epoch time.");
|
|
}
|
|
|
|
*epoch_seconds = 24L * 3600L * day + seconds;
|
|
return OkStatus();
|
|
}
|
|
|
|
X509CertChain::~X509CertChain() { Reset(); }
|
|
|
|
void X509CertChain::Reset() {
|
|
for (auto certp : cert_chain_) {
|
|
delete certp;
|
|
}
|
|
cert_chain_.clear();
|
|
}
|
|
|
|
Status X509CertChain::LoadPem(const std::string& pem_cert_chain) {
|
|
static const char kBeginCertificate[] = "-----BEGIN CERTIFICATE-----";
|
|
static const char kEndCertificate[] = "-----END CERTIFICATE-----";
|
|
|
|
Reset();
|
|
size_t begin_pos = pem_cert_chain.find(kBeginCertificate);
|
|
while (begin_pos != std::string::npos) {
|
|
size_t end_pos = pem_cert_chain.find(
|
|
kEndCertificate, begin_pos + sizeof(kBeginCertificate) - 1);
|
|
if (end_pos != std::string::npos) {
|
|
end_pos += sizeof(kEndCertificate) - 1;
|
|
std::unique_ptr<X509Cert> new_cert(new X509Cert);
|
|
Status status = new_cert->LoadPem(
|
|
pem_cert_chain.substr(begin_pos, end_pos - begin_pos));
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
cert_chain_.push_back(new_cert.release());
|
|
begin_pos = pem_cert_chain.find(kBeginCertificate, end_pos);
|
|
}
|
|
}
|
|
return OkStatus();
|
|
}
|
|
|
|
Status X509CertChain::LoadPkcs7(const std::string& pk7_cert_chain) {
|
|
ScopedX509Stack cert_stack(sk_X509_new_null());
|
|
CBS cbs;
|
|
CBS_init(&cbs, reinterpret_cast<const uint8_t*>(pk7_cert_chain.data()),
|
|
pk7_cert_chain.size());
|
|
if (!PKCS7_get_certificates(cert_stack.get(), &cbs)) {
|
|
return Status(error::INVALID_ARGUMENT,
|
|
"Unable to load PKCS#7 certificate chain");
|
|
}
|
|
|
|
while (sk_X509_num(cert_stack.get()) > 0) {
|
|
cert_chain_.insert(cert_chain_.begin(),
|
|
new X509Cert(sk_X509_pop(cert_stack.get())));
|
|
}
|
|
|
|
return OkStatus();
|
|
}
|
|
|
|
std::string X509CertChain::GetPkcs7() {
|
|
std::string pkcs7_cert;
|
|
ScopedX509Stack cert_stack(sk_X509_new_null());
|
|
for (X509Cert* cert : cert_chain_) {
|
|
// X509 stack takes ownership of certificates. Copy certificates to retain
|
|
// |cert_chain_|.
|
|
X509Cert cert_copy;
|
|
if (!cert_copy.LoadPem(cert->GetPem()).ok()) {
|
|
LOG(WARNING) << "Certificate chain serialization failed";
|
|
return "";
|
|
}
|
|
X509* openssl_cert_copy = const_cast<X509*>(cert_copy.openssl_cert());
|
|
cert_copy.openssl_cert_ = nullptr;
|
|
sk_X509_push(cert_stack.get(), openssl_cert_copy);
|
|
}
|
|
ScopedPKCS7 pkcs7(
|
|
PKCS7_sign(nullptr, nullptr, cert_stack.get(), nullptr, PKCS7_DETACHED));
|
|
if (!pkcs7) {
|
|
LOG(WARNING) << "Could not convert certificate chain to PKCS7";
|
|
return "";
|
|
}
|
|
ScopedBIO bio(BIO_new(BIO_s_mem()));
|
|
if (bio.get() == nullptr || !i2d_PKCS7_bio(bio.get(), pkcs7.get())) {
|
|
LOG(WARNING) << "Failed writing PKCS7 to bio";
|
|
return "";
|
|
}
|
|
int cert_size = BIO_pending(bio.get());
|
|
pkcs7_cert.resize(cert_size);
|
|
if (BIO_read(bio.get(), &pkcs7_cert[0], cert_size) != cert_size) {
|
|
LOG(WARNING) << "BIO_read failure";
|
|
return "";
|
|
}
|
|
return pkcs7_cert;
|
|
}
|
|
|
|
X509Cert* X509CertChain::GetCert(size_t cert_index) const {
|
|
if (cert_index >= cert_chain_.size()) {
|
|
return NULL;
|
|
}
|
|
return cert_chain_[cert_index];
|
|
}
|
|
|
|
X509CA::X509CA(X509Cert* ca_cert) : ca_cert_(ca_cert), openssl_store_(NULL) {}
|
|
|
|
X509CA::~X509CA() {
|
|
if (openssl_store_ != NULL) {
|
|
X509_STORE_free(openssl_store_);
|
|
}
|
|
}
|
|
|
|
Status X509CA::InitializeStore() {
|
|
absl::WriterMutexLock lock(&openssl_store_mutex_);
|
|
if (openssl_store_ == NULL) {
|
|
if (ca_cert_ == NULL) {
|
|
return Status(error::INTERNAL, "CA X.509Cert is NULL");
|
|
}
|
|
openssl_store_ = X509_STORE_new();
|
|
if (openssl_store_ == NULL) {
|
|
return Status(error::INTERNAL, "Failed to allocate X.509 store");
|
|
}
|
|
if (X509_STORE_add_cert(openssl_store_,
|
|
const_cast<X509*>(ca_cert_->openssl_cert())) == 0) {
|
|
X509_STORE_free(openssl_store_);
|
|
openssl_store_ = NULL;
|
|
|
|
return Status(error::INTERNAL,
|
|
"Failed to add X.509 CA certificate to store");
|
|
}
|
|
}
|
|
return OkStatus();
|
|
}
|
|
|
|
Status X509CA::VerifyCert(const X509Cert& cert) {
|
|
return OpenSslX509Verify(cert.openssl_cert(), nullptr);
|
|
}
|
|
|
|
Status X509CA::VerifyCertChain(const X509CertChain& cert_chain) {
|
|
if (cert_chain.GetNumCerts() < 1) {
|
|
return Status(error::INVALID_ARGUMENT,
|
|
"Cannot verify empty certificate chain");
|
|
}
|
|
|
|
ScopedX509StackOnly intermediates(sk_X509_new_null());
|
|
if (!intermediates) {
|
|
return Status(error::INTERNAL,
|
|
"Failed to allocate X.509 intermediate certificate stack");
|
|
}
|
|
const X509Cert* leaf_cert(nullptr);
|
|
for (size_t idx = 0; idx < cert_chain.GetNumCerts(); ++idx) {
|
|
if (cert_chain.GetCert(idx)->IsCaCertificate()) {
|
|
sk_X509_push(intermediates.get(),
|
|
const_cast<X509*>(cert_chain.GetCert(idx)->openssl_cert()));
|
|
} else {
|
|
leaf_cert = cert_chain.GetCert(idx);
|
|
}
|
|
}
|
|
if (!leaf_cert) {
|
|
return Status(error::INVALID_ARGUMENT,
|
|
"X.509 certificate chain without leaf certificate.");
|
|
}
|
|
return OpenSslX509Verify(leaf_cert->openssl_cert(), intermediates.get());
|
|
}
|
|
|
|
Status X509CA::VerifyCertWithChain(const X509Cert& cert,
|
|
const X509CertChain& cert_chain) {
|
|
ScopedX509StackOnly intermediates(sk_X509_new_null());
|
|
if (!intermediates) {
|
|
// MakeStatus is now preferred. But we don't support it in the exported
|
|
// version, yet. So, ignore lint here.
|
|
// NOLINTNEXTLINE
|
|
return Status(error::INTERNAL,
|
|
"Failed to allocate X.509 intermediate certificate stack");
|
|
}
|
|
for (size_t idx = 0; idx < cert_chain.GetNumCerts(); ++idx) {
|
|
sk_X509_push(intermediates.get(),
|
|
const_cast<X509*>(cert_chain.GetCert(idx)->openssl_cert()));
|
|
}
|
|
|
|
return OpenSslX509Verify(cert.openssl_cert(), intermediates.get());
|
|
}
|
|
|
|
Status X509CA::OpenSslX509Verify(const X509* cert,
|
|
STACK_OF(X509) * intermediates) {
|
|
DCHECK(cert);
|
|
|
|
absl::ReaderMutexLock lock(&openssl_store_mutex_);
|
|
if (openssl_store_ == NULL) {
|
|
openssl_store_mutex_.ReaderUnlock();
|
|
Status status = InitializeStore();
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
openssl_store_mutex_.ReaderLock();
|
|
}
|
|
ScopedX509StoreCtx store_ctx(X509_STORE_CTX_new());
|
|
if (!store_ctx) {
|
|
return Status(error::INTERNAL, "Failed to allocate X.509 store context");
|
|
}
|
|
if (X509_STORE_CTX_init(store_ctx.get(), openssl_store_,
|
|
const_cast<X509*>(cert), intermediates) == 0) {
|
|
return Status(error::INTERNAL, "Failed to initialize X.509 store context");
|
|
}
|
|
int x509_status = X509_verify_cert(store_ctx.get());
|
|
if (x509_status != 1) {
|
|
return Status(error::INTERNAL,
|
|
std::string("X.509 certificate chain validation failed: ") +
|
|
X509_verify_cert_error_string(
|
|
X509_STORE_CTX_get_error(store_ctx.get())));
|
|
}
|
|
|
|
return OkStatus();
|
|
}
|
|
|
|
} // namespace widevine
|