Skip to content
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

Merged
merged 34 commits into from
Aug 7, 2020

Conversation

danzh2010
Copy link
Contributor

@danzh2010 danzh2010 commented Jul 13, 2020

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

danzh1989 added 8 commits July 1, 2020 16:31
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>
Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010
Copy link
Contributor Author

/assign @PiotrSikora @wu-bin

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>
Copy link
Contributor

@wu-bin wu-bin left a 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.

Comment on lines +24 to +32
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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 8 to 22
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();
}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link

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) {
Copy link
Contributor

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 ==?

Copy link
Contributor Author

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.

Copy link

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).

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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>
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,
Copy link

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.

Copy link
Contributor Author

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());
Copy link

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.)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link

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.

Comment on lines 8 to 22
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();
}
Copy link

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) {
Copy link

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 {
Copy link

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.

Copy link
Contributor Author

@danzh2010 danzh2010 Jul 15, 2020

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.

Copy link

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?

Copy link
Contributor Author

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,
Copy link

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.)

Copy link
Contributor

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.

Copy link
Contributor Author

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>
Signed-off-by: Dan Zhang <danzh@google.com>
if (certs.empty()) {
return quic::QUIC_FAILURE;
}
bssl::UniquePtr<STACK_OF(X509)> intermediates(sk_X509_new_null());
Copy link
Contributor

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.

@@ -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,
Copy link
Contributor

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.

source/extensions/transport_sockets/tls/context_impl.h Outdated Show resolved Hide resolved
tools/spelling/spelling_dictionary.txt Outdated Show resolved Hide resolved
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010
Copy link
Contributor Author

ping?

Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010
Copy link
Contributor Author

ping?

Copy link
Contributor

@ggreenway ggreenway left a 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

@@ -44,6 +47,43 @@ class CodecClientCallbacksForTest : public Http::CodecClientCallbacks {
Http::StreamResetReason last_stream_reset_reason_{Http::StreamResetReason::LocalReset};
};

std::unique_ptr<QuicClientTransportSocketFactory>
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@ggreenway
Copy link
Contributor

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>
@danzh2010
Copy link
Contributor Author

It is confirmed that std::string_view needs to be changed to absl::string_view. See #12341 for discussion/rationale.

done

Copy link
Contributor

@ggreenway ggreenway left a 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>
Signed-off-by: Dan Zhang <danzh@google.com>
Copy link
Contributor

@ggreenway ggreenway left a 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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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

Copy link
Contributor Author

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) {
Copy link
Contributor

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

Copy link
Contributor Author

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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add test coverage

Copy link
Contributor Author

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.";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add test coverage

Copy link
Contributor Author

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: {
Copy link
Contributor

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

Copy link
Contributor Author

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 =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test coverage

Copy link
Contributor Author

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";
Copy link
Contributor

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

@ggreenway
Copy link
Contributor

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>
Copy link
Contributor

@ggreenway ggreenway left a 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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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>
@ggreenway
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@ggreenway ggreenway merged commit 43b110a into envoyproxy:master Aug 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants