-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
quiche: implement certificate verification #12063
Conversation
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
/assign @PiotrSikora @wu-bin |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Dan. I have some comments but as we chatted offline, I'd prefer to get a review from Victor or Nick.
source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc
Outdated
Show resolved
Hide resolved
size_t payload_size = sizeof(quic::kProofSignatureLabel) + sizeof(uint32_t) + chlo_hash.size() + | ||
server_config.size(); | ||
auto payload = std::make_unique<char[]>(payload_size); | ||
quic::QuicDataWriter payload_writer(payload_size, payload.get(), | ||
quiche::Endianness::HOST_BYTE_ORDER); | ||
bool success = | ||
payload_writer.WriteBytes(quic::kProofSignatureLabel, sizeof(quic::kProofSignatureLabel)) && | ||
payload_writer.WriteUInt32(chlo_hash.size()) && payload_writer.WriteStringPiece(chlo_hash) && | ||
payload_writer.WriteStringPiece(server_config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems similar to quic::ProofSourceX509::GetProof, is it possible to reuse that code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payload generation is actually shared across all ProofSource implementation not just with X509 ProofSource. I think it makes more sense for GetProof() to take the generated payload as argument. But this requires large upstream code refactoring which I don't think worth doing just for the sake of code sharing.
static X509* parseDERCertificate(const std::string& der_bytes, std::string* error_details) { | ||
const uint8_t* data; | ||
const uint8_t* orig_data; | ||
orig_data = data = reinterpret_cast<const uint8_t*>(der_bytes.data()); | ||
bssl::UniquePtr<X509> cert(d2i_X509(nullptr, &data, der_bytes.size())); | ||
if (!cert.get()) { | ||
*error_details = "d2i_X509"; | ||
return nullptr; | ||
} | ||
if (data < orig_data || static_cast<size_t>(data - orig_data) != der_bytes.size()) { | ||
// Trailing garbage. | ||
return nullptr; | ||
} | ||
return cert.release(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this function into to quiche and use it from here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseDERCertificate() is not used anywhere else in QUICHE. So it would look like a dead function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't want the boringssl X509 code in quiche.
quic::CertificateView::ParseSingleCertificate(certs[0]); | ||
ASSERT(cert_view != nullptr); | ||
for (const absl::string_view config_san : cert_view->subject_alt_name_domains()) { | ||
if (config_san == hostname) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that config_san is a wildcard domain? If so, should we do regex match instead of ==?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think SAN can be wildcard. But better to leave it to Nick to confirm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it could be a wildcard. A regex is the wrong way to match the wildcard. For a given hostname, there is a single wildcard entry that could cover it: replace the leftmost label in the hostname with *
. This could be changed to if (config_san == hostname || config_san == wildcard)
where wildcard
is the substitution described above (assuming there's more than one label in the hostname).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
std::unique_ptr<EnvoyQuicProofVerifier> verifier_; | ||
}; | ||
|
||
TEST_F(EnvoyQuicProofVerifierTest, VerifyFilterChainSuccess) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VerifyCertChainSuccess?
Same for other test names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Signed-off-by: Dan Zhang <danzh@google.com>
|
||
// TODO(danzh) Get the signature algorithm from leaf cert. | ||
auto signature_callback = std::make_unique<SignatureCallback>(std::move(callback), chain); | ||
ComputeTlsSignature(server_address, client_address, hostname, SSL_SIGN_RSA_PSS_RSAE_SHA256, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling ComputeTlsSignature here looks a bit confusing when reading the code. I realize that GetProof and ComputeTlsSignature both do similar things (and ComputeTlsSignature doesn't need to do any processing on the input before signing), but having a helper method for the shared signing code would improve readability here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
if (certs.empty()) { | ||
return quic::QUIC_FAILURE; | ||
} | ||
bssl::UniquePtr<STACK_OF(X509)> intermediates(sk_X509_new_null()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way we can avoid using the boringssl X.509 code? (It's a mess and should be replaced. The chromium cert verifier is a candidate replacement.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I make the interface takes X509_STORE_CTX and X509 objects because ContextImpl::doVerifyCertChain() heavily uses X509 code. We can move the conversion of const std::vector<std::string>& certs
to X509
into ContextImpl, but basically there is no way to avoid using these boringssl interfaces unless we don't reuse envoy cert verification code. But I think it makes sense to be consistent with tcp SSL socket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a separate issue being sorted out for how to move envoy off of this boringssl code. The current status, AFAIK, is that the chromium cert verifier isn't in a state that can be consumed by other projects yet, so there isn't any readily available replacement. @PiotrSikora and I are discussing, and he has posted #10621 as an option.
I think for this PR, you should leave this as-is using boringssl, and this can be updated when we address the issue in envoy as a whole.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree you should reuse the existing envoy cert verification code.
static X509* parseDERCertificate(const std::string& der_bytes, std::string* error_details) { | ||
const uint8_t* data; | ||
const uint8_t* orig_data; | ||
orig_data = data = reinterpret_cast<const uint8_t*>(der_bytes.data()); | ||
bssl::UniquePtr<X509> cert(d2i_X509(nullptr, &data, der_bytes.size())); | ||
if (!cert.get()) { | ||
*error_details = "d2i_X509"; | ||
return nullptr; | ||
} | ||
if (data < orig_data || static_cast<size_t>(data - orig_data) != der_bytes.size()) { | ||
// Trailing garbage. | ||
return nullptr; | ||
} | ||
return cert.release(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't want the boringssl X509 code in quiche.
quic::CertificateView::ParseSingleCertificate(certs[0]); | ||
ASSERT(cert_view != nullptr); | ||
for (const absl::string_view config_san : cert_view->subject_alt_name_domains()) { | ||
if (config_san == hostname) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it could be a wildcard. A regex is the wrong way to match the wildcard. For a given hostname, there is a single wildcard entry that could cover it: replace the leftmost label in the hostname with *
. This could be changed to if (config_san == hostname || config_san == wildcard)
where wildcard
is the substitution described above (assuming there's more than one label in the hostname).
|
||
// A partial implementation of quic::ProofVerifier which does signature | ||
// verification using SSL_SIGN_RSA_PSS_RSAE_SHA256. | ||
class EnvoyQuicProofVerifierBase : public quic::ProofVerifier { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why EnvoyQuicProofVerifierBase and EnvoyQuicProofVerifier are separate classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cert chain verification part is missing in base class. In EnvoyQuicProofVerifier it gets root cert from envoy config to verify the chain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that that part is missing in the base class. Why is it missing from the base class instead of combining the two classes into one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is another TestProofVerifer inheriting from this base class but doesn't retrieve certs from Envoy infrastructure.
@@ -101,6 +101,14 @@ class ContextImpl : public virtual Envoy::Ssl::Context { | |||
|
|||
std::vector<Ssl::PrivateKeyMethodProviderSharedPtr> getPrivateKeyMethodProviders(); | |||
|
|||
// Called by verifyCallback to do the actual cert chain verification. | |||
int doVerifyCertChain(X509_STORE_CTX* store_ctx, Ssl::SslExtendedSocketInfo* ssl_extended_info, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion would be to refactor the cert verification code in ContextImpl to allow for the QUIC code to pass in a cert chain (as a vector of buffers of bytes, not X509 structs) and a hostname so that none of the x509 code needs to be in the ProofVerifier impl. (This might also mean adding an additional function to get error details.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree; it would be nice if all the boringssl X509
code was contained here. But if it makes it messier, I'm ok with leaving it how it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added another interface here for quiche integration code to call. It takes care of X509 CTX stuff
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc
Outdated
Show resolved
Hide resolved
if (certs.empty()) { | ||
return quic::QUIC_FAILURE; | ||
} | ||
bssl::UniquePtr<STACK_OF(X509)> intermediates(sk_X509_new_null()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a separate issue being sorted out for how to move envoy off of this boringssl code. The current status, AFAIK, is that the chromium cert verifier isn't in a state that can be consumed by other projects yet, so there isn't any readily available replacement. @PiotrSikora and I are discussing, and he has posted #10621 as an option.
I think for this PR, you should leave this as-is using boringssl, and this can be updated when we address the issue in envoy as a whole.
source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.cc
Outdated
Show resolved
Hide resolved
@@ -101,6 +101,14 @@ class ContextImpl : public virtual Envoy::Ssl::Context { | |||
|
|||
std::vector<Ssl::PrivateKeyMethodProviderSharedPtr> getPrivateKeyMethodProviders(); | |||
|
|||
// Called by verifyCallback to do the actual cert chain verification. | |||
int doVerifyCertChain(X509_STORE_CTX* store_ctx, Ssl::SslExtendedSocketInfo* ssl_extended_info, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree; it would be nice if all the boringssl X509
code was contained here. But if it makes it messier, I'm ok with leaving it how it is.
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
ping? |
Signed-off-by: Dan Zhang <danzh@google.com>
ping? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good.
Please add test coverage for the error cases you're adding. The report says the following are especially in need of more tests:
EnvoyQuicProofSourceBase::GetProof
EnvoyQuicProofSourceBase::Run
EnvoyQuicProofVerifier::VerifyCertChain
EnvoyQuicProofVerifierBase
Minor style note I saw in a few places: when declaring a pure virtual method, envoy style is to use PURE
instead of = 0
.
There's an open issue right now discussing whether it's safe to use std::string_view
due to older supported OS versions for envoy mobile. The outcome isn't finalized yet, but it would be safer to convert all std::string_view
into absl::string_view
. cc @junr03
/wait
source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.cc
Outdated
Show resolved
Hide resolved
test/extensions/quic_listeners/quiche/envoy_quic_proof_source_test.cc
Outdated
Show resolved
Hide resolved
source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h
Outdated
Show resolved
Hide resolved
test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc
Outdated
Show resolved
Hide resolved
@@ -44,6 +47,43 @@ class CodecClientCallbacksForTest : public Http::CodecClientCallbacks { | |||
Http::StreamResetReason last_stream_reset_reason_{Http::StreamResetReason::LocalReset}; | |||
}; | |||
|
|||
std::unique_ptr<QuicClientTransportSocketFactory> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add 1 simple integration test validating cert validation failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added CertVerificationFailure which failed because of SAN mismatch.
It is confirmed that std::string_view needs to be changed to absl::string_view. See #12341 for discussion/rationale. |
Signed-off-by: Dan Zhang <danzh@google.com>
done |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good.
check_format is failing. I'd like to take a look at the coverage report before reviewing further, but coverage doesn't run until formatting passes.
I'd still really appreciate another set of eyes on this, since this is security-critical code. @PiotrSikora ? @envoyproxy/maintainers ? Anyone from google who works on related stuff?
/wait
Signed-off-by: Dan Zhang <danzh@google.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the code looks good overall. Mostly just need to improve error handling test coverage now.
/wait
|
||
std::string error_details; | ||
bssl::UniquePtr<X509> cert = parseDERCertificate(chain->certs[0], &error_details); | ||
if (cert == nullptr) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add test coverage for the error case. If it can't be tested, should it be changed to an ASSERT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
bssl::UniquePtr<EVP_PKEY> pub_key(X509_get_pubkey(cert.get())); | ||
int sign_alg = deduceSignatureAlgorithmFromPublicKey(pub_key.get(), &error_details); | ||
if (sign_alg == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add test coverage for the error case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
bssl::UniquePtr<X509> leaf; | ||
for (size_t i = 0; i < certs.size(); i++) { | ||
bssl::UniquePtr<X509> cert = parseDERCertificate(certs[i], error_details); | ||
if (!cert) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test coverage for the error case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
return false; | ||
} | ||
int sign_alg = deduceSignatureAlgorithmFromPublicKey(cert_view->public_key(), error_details); | ||
if (sign_alg == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test coverage for the error case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
orig_data = data = reinterpret_cast<const uint8_t*>(der_bytes.data()); | ||
bssl::UniquePtr<X509> cert(d2i_X509(nullptr, &data, der_bytes.size())); | ||
if (!cert.get()) { | ||
*error_details = "d2i_X509: fail to parse DER"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add test coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
return nullptr; | ||
} | ||
if (data < orig_data || static_cast<size_t>(data - orig_data) != der_bytes.size()) { | ||
*error_details = "There is trailing garbage in DER."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add test coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
int sign_alg = 0; | ||
const int pkey_id = EVP_PKEY_id(public_key); | ||
switch (pkey_id) { | ||
case EVP_PKEY_EC: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add test coverage for this case, including the error code for wrong curve/group
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
#else | ||
if (rsa_key_length < 2048 / 8) { | ||
*error_details = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} break; | ||
default: | ||
*error_details = | ||
"Invalid leaf cert, only RSA and ECDSA certificates are supported in FIPS mode"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this also applies to non-fips mode now; fix the error string
I'm not sure what happened in clang_tidy; all the errors were in standard library headers. Maybe try merging from master to see if it goes away? |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, modulo one very minor nit. Waiting for the coverage build to finish so I can take a look, but I think you covered all the cases that were lacking.
@@ -166,6 +194,43 @@ TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureInvalidLeafCert) { | |||
EXPECT_EQ("Invalid leaf cert.", error_details); | |||
} | |||
|
|||
TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureUnsupportedRsaKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the test uses an unsupported EC curve, but the name says unsupported RSA key. Please fix the test name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Signed-off-by: Dan Zhang <danzh@google.com>
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
Signed-off-by: Dan Zhang danzh@google.com
Implement quic::ProofVerifier which consists of cert verification and signature verification.
Cert verification:
Share cert verification code with Extensions::TransportSockets::Tls::ClientContextImpl. And initialize ProofVerifier using Envoy::Ssl::ClientContextConfig protobuf.
Signature verification:
Use quic::CertificateViewer to verify signature.
Risk Level: low, not in use
Testing: added new unit test for cert verifier and test interaction with ProofSource
Part of #9434 #2557