[Security - CrlProvider] Add AKID to CertificateInfo (#35931)

This PR adds the Authority Key Identifier to CertificateInfo. This value _can be_ important in finding the right CRLs to use if there are Issuer name overlaps or a more complicated CA setup with multiple signing keys.

We should observe no behavior change in our `CrlProvider` implementations, this is just adding an important field for users who implement it themselves.

Closes #35931

COPYBARA_INTEGRATE_REVIEW=https://github.com/grpc/grpc/pull/35931 from gtcooke94:AkidCheck dd048a53b6
PiperOrigin-RevId: 611143198
pull/36022/head
Gregory Cooke 1 year ago committed by Copybara-Service
parent d4b5e8d11d
commit d6089c336c
  1. 1
      include/grpc/grpc_crl_provider.h
  2. 10
      src/core/lib/security/credentials/tls/grpc_tls_crl_provider.h
  3. 11
      src/core/tsi/ssl_transport_security.cc
  4. 50
      src/core/tsi/ssl_transport_security_utils.cc
  5. 9
      src/core/tsi/ssl_transport_security_utils.h
  6. 13
      test/core/security/grpc_tls_crl_provider_test.cc
  7. 2
      test/core/tsi/BUILD
  8. 44
      test/core/tsi/ssl_transport_security_utils_test.cc
  9. 2
      test/core/tsi/test_creds/crl_data/BUILD
  10. 4
      test/core/tsi/test_creds/crl_data/README
  11. 32
      test/core/tsi/test_creds/crl_data/ca-with-akid.cnf
  12. 28
      test/core/tsi/test_creds/crl_data/ca_with_akid.key
  13. 24
      test/core/tsi/test_creds/crl_data/ca_with_akid.pem
  14. 35
      test/core/tsi/test_creds/crl_data/ca_with_akid_gen.sh
  15. 15
      test/core/tsi/test_creds/crl_data/crl_with_akid.crl

@ -47,6 +47,7 @@ class CertificateInfo {
public:
virtual ~CertificateInfo() = default;
virtual absl::string_view Issuer() const = 0;
virtual absl::string_view AuthorityKeyIdentifier() const = 0;
};
// The base class for CRL Provider implementations.

@ -79,13 +79,17 @@ class CrlImpl : public Crl {
class CertificateInfoImpl : public CertificateInfo {
public:
explicit CertificateInfoImpl(absl::string_view issuer) : issuer_(issuer) {}
// Returns a string representation of the issuer pulled from the
// certificate.
explicit CertificateInfoImpl(absl::string_view issuer,
absl::string_view authority_key_identifier = "")
: issuer_(issuer), authority_key_identifier_(authority_key_identifier) {}
absl::string_view Issuer() const override { return issuer_; }
absl::string_view AuthorityKeyIdentifier() const override {
return authority_key_identifier_;
}
private:
const std::string issuer_;
const std::string authority_key_identifier_;
};
// Defining this here lets us hide implementation details (and includes) from

@ -1009,7 +1009,16 @@ static int GetCrlFromProvider(X509_STORE_CTX* ctx, X509_CRL** crl_out,
gpr_log(GPR_INFO, "Could not get certificate issuer name");
return 0;
}
grpc_core::experimental::CertificateInfoImpl cert_impl(*issuer_name);
absl::StatusOr<std::string> akid = grpc_core::AkidFromCertificate(cert);
std::string akid_to_use;
if (!akid.ok()) {
gpr_log(GPR_INFO, "Could not get certificate authority key identifier.");
} else {
akid_to_use = *akid;
}
grpc_core::experimental::CertificateInfoImpl cert_impl(*issuer_name,
akid_to_use);
std::shared_ptr<grpc_core::experimental::Crl> internal_crl =
provider->GetCrl(cert_impl);
// There wasn't a CRL found in the provider. Returning 0 will end up causing

@ -317,4 +317,54 @@ absl::StatusOr<std::string> IssuerFromCert(X509* cert) {
return ret;
}
absl::StatusOr<std::string> AkidFromCertificate(X509* cert) {
if (cert == nullptr) {
return absl::InvalidArgumentError("cert cannot be null.");
}
ASN1_OCTET_STRING* akid = nullptr;
int j = X509_get_ext_by_NID(cert, NID_authority_key_identifier, -1);
// Can't have multiple occurrences
if (j >= 0) {
if (X509_get_ext_by_NID(cert, NID_authority_key_identifier, j) != -1) {
return absl::InvalidArgumentError("Could not get AKID from certificate.");
}
akid = X509_EXTENSION_get_data(X509_get_ext(cert, j));
} else {
return absl::InvalidArgumentError("Could not get AKID from certificate.");
}
unsigned char* buf = nullptr;
int len = i2d_ASN1_OCTET_STRING(akid, &buf);
if (len <= 0) {
return absl::InvalidArgumentError("Could not get AKID from certificate.");
}
std::string ret(reinterpret_cast<char const*>(buf), len);
OPENSSL_free(buf);
return ret;
}
absl::StatusOr<std::string> AkidFromCrl(X509_CRL* crl) {
if (crl == nullptr) {
return absl::InvalidArgumentError("Could not get AKID from crl.");
}
ASN1_OCTET_STRING* akid = nullptr;
int j = X509_CRL_get_ext_by_NID(crl, NID_authority_key_identifier, -1);
// Can't have multiple occurrences
if (j >= 0) {
if (X509_CRL_get_ext_by_NID(crl, NID_authority_key_identifier, j) != -1) {
return absl::InvalidArgumentError("Could not get AKID from crl.");
}
akid = X509_EXTENSION_get_data(X509_CRL_get_ext(crl, j));
} else {
return absl::InvalidArgumentError("Could not get AKID from crl.");
}
unsigned char* buf = nullptr;
int len = i2d_ASN1_OCTET_STRING(akid, &buf);
if (len <= 0) {
return absl::InvalidArgumentError("Could not get AKID from crl.");
}
std::string ret(reinterpret_cast<char const*>(buf), len);
OPENSSL_free(buf);
return ret;
}
} // namespace grpc_core

@ -160,6 +160,15 @@ bool HasCrlSignBit(X509* cert);
// return: a std::string of the DER encoding of the X509_NAME issuer name.
absl::StatusOr<std::string> IssuerFromCert(X509* cert);
// Gets a stable representation of the authority key identifier from an X509
// certificate.
// return: a std::string of the DER encoding of the AKID or a status on failure.
absl::StatusOr<std::string> AkidFromCertificate(X509* cert);
// Gets a stable representation of the authority key identifier from an X509
// crl.
// return: a std::string of the DER encoding of the AKID or a status on failure.
absl::StatusOr<std::string> AkidFromCrl(X509_CRL* crl);
} // namespace grpc_core
#endif // GRPC_SRC_CORE_TSI_SSL_TRANSPORT_SECURITY_UTILS_H

@ -284,6 +284,19 @@ TEST_F(DirectoryReloaderCrlProviderTest, WithBadInitialDirectoryStatus) {
EXPECT_EQ(reload_errors.size(), 1);
}
TEST(CertificateInfoImplTest, CanFetchValues) {
experimental::CertificateInfoImpl cert =
CertificateInfoImpl("issuer", "akid");
EXPECT_EQ(cert.Issuer(), "issuer");
EXPECT_EQ(cert.AuthorityKeyIdentifier(), "akid");
}
TEST(CertificateInfoImplTest, NoAkid) {
experimental::CertificateInfoImpl cert = CertificateInfoImpl("issuer");
EXPECT_EQ(cert.Issuer(), "issuer");
EXPECT_EQ(cert.AuthorityKeyIdentifier(), "");
}
} // namespace testing
} // namespace grpc_core

@ -66,6 +66,8 @@ grpc_cc_test(
srcs = ["ssl_transport_security_utils_test.cc"],
data = [
"//test/core/tsi/test_creds/crl_data:ca.pem",
"//test/core/tsi/test_creds/crl_data:ca_with_akid.pem",
"//test/core/tsi/test_creds/crl_data:crl_with_akid.crl",
"//test/core/tsi/test_creds/crl_data:evil_ca.pem",
"//test/core/tsi/test_creds/crl_data:intermediate_ca.pem",
"//test/core/tsi/test_creds/crl_data:leaf_signed_by_intermediate.pem",

@ -56,6 +56,9 @@ const char* kIntermediateCrlIssuer =
const char* kLeafCert =
"test/core/tsi/test_creds/crl_data/leaf_signed_by_intermediate.pem";
const char* kEvilCa = "test/core/tsi/test_creds/crl_data/evil_ca.pem";
const char* kCaWithAkid = "test/core/tsi/test_creds/crl_data/ca_with_akid.pem";
const char* kCrlWithAkid =
"test/core/tsi/test_creds/crl_data/crl_with_akid.crl";
using ::testing::ContainerEq;
using ::testing::NotNull;
@ -472,6 +475,9 @@ class CrlUtils : public ::testing::Test {
ASSERT_EQ(invalid_signature_crl.status(), absl::OkStatus())
<< invalid_signature_crl.status();
invalid_signature_crl_ = ReadCrl(invalid_signature_crl->as_string_view());
absl::StatusOr<Slice> akid_crl = LoadFile(kCrlWithAkid, false);
ASSERT_EQ(akid_crl.status(), absl::OkStatus()) << akid_crl.status();
akid_crl_ = ReadCrl(akid_crl->as_string_view());
absl::StatusOr<Slice> root_ca = LoadFile(kCrlIssuer, false);
ASSERT_EQ(root_ca.status(), absl::OkStatus());
@ -486,26 +492,33 @@ class CrlUtils : public ::testing::Test {
absl::StatusOr<Slice> evil_ca = LoadFile(kEvilCa, false);
ASSERT_EQ(evil_ca.status(), absl::OkStatus());
evil_ca_ = ReadPemCert(evil_ca->as_string_view());
absl::StatusOr<Slice> ca_with_akid = LoadFile(kCaWithAkid, false);
ASSERT_EQ(ca_with_akid.status(), absl::OkStatus());
ca_with_akid_ = ReadPemCert(ca_with_akid->as_string_view());
}
void TearDown() override {
X509_CRL_free(root_crl_);
X509_CRL_free(intermediate_crl_);
X509_CRL_free(invalid_signature_crl_);
X509_CRL_free(akid_crl_);
X509_free(root_ca_);
X509_free(intermediate_ca_);
X509_free(leaf_cert_);
X509_free(evil_ca_);
X509_free(ca_with_akid_);
}
protected:
X509_CRL* root_crl_;
X509_CRL* intermediate_crl_;
X509_CRL* invalid_signature_crl_;
X509_CRL* akid_crl_;
X509* root_ca_;
X509* intermediate_ca_;
X509* leaf_cert_;
X509* evil_ca_;
X509* ca_with_akid_;
};
TEST_F(CrlUtils, VerifySignatureValid) {
@ -629,6 +642,37 @@ TEST_F(CrlUtils, IssuerFromCertNull) {
EXPECT_EQ(issuer.status().code(), absl::StatusCode::kInvalidArgument);
}
TEST_F(CrlUtils, CertCrlAkidValid) {
auto akid = AkidFromCertificate(ca_with_akid_);
EXPECT_EQ(akid.status(), absl::OkStatus());
auto crl_akid = AkidFromCrl(akid_crl_);
EXPECT_EQ(crl_akid.status(), absl::OkStatus());
EXPECT_NE(*akid, "");
// It's easiest to compare that these two pull the same value, it's very
// difficult to create the known AKID value as a test constant, so we just
// check that they are not empty and that they are the same.
EXPECT_EQ(*akid, *crl_akid);
}
TEST_F(CrlUtils, CertNoAkid) {
auto akid = AkidFromCertificate(root_ca_);
EXPECT_EQ(akid.status().code(), absl::StatusCode::kInvalidArgument);
}
TEST_F(CrlUtils, CrlNoAkid) {
auto akid = AkidFromCrl(root_crl_);
EXPECT_EQ(akid.status().code(), absl::StatusCode::kInvalidArgument);
}
TEST_F(CrlUtils, CertAkidNullptr) {
auto akid = AkidFromCertificate(nullptr);
EXPECT_EQ(akid.status().code(), absl::StatusCode::kInvalidArgument);
}
TEST_F(CrlUtils, CrlAkidNullptr) {
auto akid = AkidFromCrl(nullptr);
EXPECT_EQ(akid.status().code(), absl::StatusCode::kInvalidArgument);
}
} // namespace testing
} // namespace grpc_core

@ -26,4 +26,6 @@ exports_files([
"intermediate_ca.key",
"intermediate_ca.pem",
"evil_ca.pem",
"ca_with_akid.pem",
"crl_with_akid.crl",
])

@ -51,6 +51,10 @@ Generate a CA with the same issuer name but a different public key than the base
Run `evil_ca_gen.sh` from the `test/core/tsi/test_creds/crl_data` directory
Generate a CA and CRL with an Authority Key Identifier
----------------------------------------------------------------------------
Run `ca_with_akid_gen.sh` from the `test/core/tsi/test_creds/crl_data` directory
Clean up:
---------

@ -0,0 +1,32 @@
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = AU
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Some-State
organizationName = Organization Name (eg, company)
organizationName_default = Internet Widgits Pty Ltd
commonName = Common Name (eg, YOUR name)
commonName_default = testca
[v3_req]
basicConstraints = CA:true
keyUsage = critical, keyCertSign, cRLSign
authorityKeyIdentifier=keyid:always,issuer:always
[ ca ]
default_ca = my_ca
[ my_ca ]
default_md = sha256
database = index.txt
crlnumber = crlnumber
default_crl_days = 3650
crl_extensions = crl_ext
[crl_ext]
# Authority Key Identifier extension
authorityKeyIdentifier=keyid:always,issuer:always

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZZZIO2gh52M/8
nBhrq6mhBRDPtXKOaFZXLhW02Z/fGdP3cm7L2j9irCPXm6Lew6e/NhjsqWIIXhev
xXdHZ5Hm+8r6+IN4RRSzO5Qkpzb/7IQleaIqJlAqcU7N3qHZpGweswznoqg/5D1E
AiLc4ZDyTQa71rrrYXaABI95xddkjjThac1mzfgg0mYKy8FRMWsnxyyZMeTHMGtR
uenyUR+CZdiQooi1TaJvLh70Kh9h2r2mpZHW82N6DlHWLMrIw0wbfVewebj4I0Kg
aNGZacwawcxvZrucEDmxM84bB72oRsPi2uP27XftQv+sgwUO/9LpU2O7QTBKY3T8
2xul5FnfAgMBAAECggEADeP/CqLk0sBHPdlNeCe+TXMQ6WyrFq1VAvoHWghjvjIc
yJ1PtgfKcAgWvDKOHBS9Varjuc9y250Df3ArGG1mNa+V/yY85ETptzAR3auviHe4
09orQe0zxxp5Ug8tIUZvwCBprS1H+dkgDwXh62IgTFMeoIbuZ5bRJwnjjwDUq/CK
BRrfOCeUlic3UhhPjkiNe+ky4PIPU7tr6C7op8RWvCoEr/7YyopmWnfF2AM75E1W
hVXIESVT32Eo/938HOHTKw3Donk0B2O2Q4cHW15xExbpkdolGTuh2Mic1urndnih
vXziacOffyOj16u8gWJ+swfYtRXg0nhiK3ami6SIuQKBgQD123dQPvz5rr3FRb/R
LZj+kDLhcduQlk1bfrm5yzdVZ8PyfbSQ9Iualxpabfyc57vpxmpGHBW2QhqhVToU
ZZLog+eZjLVAWTDgLWy6qhPx7/dh4SfIVOreXHmsjDJMoGpSSmX1QwSwY2f36bxB
lAmzcpCORxsvUIFG6LuHOlmQNwKBgQDiXYdcmZGssdsSVpl0bY+yOfDdIjSoYroQ
D/aCF7H7tuBTcUH6uZAe9f6oQzseLxg0lG/oa/Ks1s616ZSY7cjLOORZlvwqFpJs
xCc6F+kJXGbpDpWUpkG/sCfrujGUCaimaIUT9H7ZKONgU1LfaUsn1nbMeJskkMfx
3uLnRQ2fmQKBgQDOgEXY6u8EsJbIiWsxwQDOYEO8RCvNZ9EV1n0c5ulVHNDibl8p
mZ1gfSYvak5RY/rbwkIlHRXHfgJsG++qjh40mgX/XMYohEGfKcg3iP8zqQC5/6mw
hFK57iZsnVzqK5rh/4df16iqlvQOsQ3kbvku9j0go+zbct0CuBw62vG7RQKBgCIK
DHPZR/WfHSFJ0nOWkhgr7FNkdGSpy+7kZ54yb/o5CsyhaFmKk+iD91JYIcitLkeh
1p4ttWVWO+lRAZ5pi1s75+Ks+Khfko82g+uRcuKMeZEsN0QOKC7qD2a8Lf5j4W98
oh5ZEsYXBvISNZEQ5VNNRboDnNjHyLlPWfGLCbxpAoGAZ0Q4Nq5ubm6FS3IdsXes
DBoXh9Hc4xiM3AMX0mK9cpLYzK0Ot4ouuYF2aZNLzPilYJLPnSyJ9D5Waxb7Ygpy
O28+oeg8FKTIQoizTGh4OCCEqWsczBdRhLF4EFixY+8O2X6EtwzMEwx0tXPalZ+x
jtbKjvkafVw+Q84xMuiIfcY=
-----END PRIVATE KEY-----

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEETCCAvmgAwIBAgIUL5bnlxnEnAXfHgVRwjkpFaSqXNcwDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0
MDIxNDIxMTkxOFoXDTM0MDIxMTIxMTkxOFowVjELMAkGA1UEBhMCQVUxEzARBgNV
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEA2WWSDtoIedjP/JwYa6upoQUQz7VyjmhWVy4VtNmf3xnT93Juy9o/Yqwj15ui
3sOnvzYY7KliCF4Xr8V3R2eR5vvK+viDeEUUszuUJKc2/+yEJXmiKiZQKnFOzd6h
2aRsHrMM56KoP+Q9RAIi3OGQ8k0Gu9a662F2gASPecXXZI404WnNZs34INJmCsvB
UTFrJ8csmTHkxzBrUbnp8lEfgmXYkKKItU2iby4e9CofYdq9pqWR1vNjeg5R1izK
yMNMG31XsHm4+CNCoGjRmWnMGsHMb2a7nBA5sTPOGwe9qEbD4trj9u137UL/rIMF
Dv/S6VNju0EwSmN0/NsbpeRZ3wIDAQABo4HWMIHTMAwGA1UdEwQFMAMBAf8wDgYD
VR0PAQH/BAQDAgEGMIGTBgNVHSMEgYswgYiAFC2LQNwfnL4NfSeNh6J1dsKS7z4z
oVqkWDBWMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8wDQYDVQQDDAZ0ZXN0Y2GCFC+W
55cZxJwF3x4FUcI5KRWkqlzXMB0GA1UdDgQWBBQti0DcH5y+DX0njYeidXbCku8+
MzANBgkqhkiG9w0BAQsFAAOCAQEAcJnNStIat0i8ZthQLgFrJVqcvMRQ6LYLWLX9
fBkoQNcgmMl2jD9oQU19YTTQ2SSFuTihxcZdstMTRpO5+s8Cqf4G6r7XIcFD75/X
UItis5YF2lmxd1Ivrd0uUoWqjPghiiAyx6o3oUA2h+v6XJCebhHoG4KwGpeX0/9F
SsbYdS4c8gn4jf3fzUZD3/fo0dXFjFB10xd9ac8wn7pP63Wtgu/ZA28bfe8rS/kv
jsuXnlIYNYIlcKRvGrtmRPOieqoxPygZudDwSUnmttvjkt01UKrYzKdqO4Wf7sLa
9YqPr6srTmz+GfF5Cp89Txy11sF8155PeJBrodnEjVO+8mQ+pg==
-----END CERTIFICATE-----

@ -0,0 +1,35 @@
#!/bin/bash
# Copyright 2024 gRPC authors.
#
# 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
#
# http://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.
rm -rf ca_with_akid/
mkdir ca_with_akid/
cp ca-with-akid.cnf ca_with_akid/
pushd ca_with_akid/ || exit
touch index.txt
echo 1 > ./serial
echo 1000 > ./crlnumber
openssl req -x509 -new -newkey rsa:2048 -nodes -keyout ca_with_akid.key -out ca_with_akid.pem \
-config ca-with-akid.cnf -days 3650 -extensions v3_req
openssl ca -gencrl -out crl_with_akid.crl -keyfile ca_with_akid.key -cert ca_with_akid.pem -crldays 3650 -config ca-with-akid.cnf
popd || exit
cp "./ca_with_akid/ca_with_akid.key" ./
cp "./ca_with_akid/ca_with_akid.pem" ./
cp "./ca_with_akid/crl_with_akid.crl" ./
rm -rf ca_with_akid

@ -0,0 +1,15 @@
-----BEGIN X509 CRL-----
MIICSTCCATECAQEwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCQVUxEzARBgNV
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
ZDEPMA0GA1UEAwwGdGVzdGNhFw0yNDAyMTQyMTE5MThaFw0zNDAyMTEyMTE5MTha
oIGmMIGjMIGTBgNVHSMEgYswgYiAFC2LQNwfnL4NfSeNh6J1dsKS7z4zoVqkWDBW
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8wDQYDVQQDDAZ0ZXN0Y2GCFC+W55cZxJwF
3x4FUcI5KRWkqlzXMAsGA1UdFAQEAgIQADANBgkqhkiG9w0BAQsFAAOCAQEAd6hA
m5fO0ebf2YuqFKr1CQ60R089xCl9ezyklLp5VnIoFkrUPYFyk9866jkVh8ckuZEF
OoOFDte5HicWm5SuDV8qtsHIo8TV+KgFKEdVJ60CTGtK/wsfIhxsrlmqIfa+U88e
hSWsfFB+vWCP9XOE4CERQJwdMNCXFB7DAEzsrygsyn8owRLuFaHSDkJERc0ZAXQv
sOhhBlsyReqLQMqycnm0X8p5HxnxELmLwmn8Gt6610CrN/ql+N3QEp9dxaATbNcz
AjAm8RyPEA+VWYsZOTgMVcS9+guR8a17E+BY1rr1z9VOFWIR0VFqrGL71nn1TL2n
iXVUyJFbTZwE2hZR6w==
-----END X509 CRL-----
Loading…
Cancel
Save