Using CertificateVerifier in GRPC C++ to manage custom tls handshake

I’m using C++ Grpc to implement a custom tls handshake on the server and client side. The handshake will:

  • Check fields in the certificate
  • Check vars related to the operating system

I believe grpc_tls_on_custom_verification_check_done_cb is something I can leverage to achieve my use case.
The issue I’m having is that I can’t find full examples of code that override that field. I’ve looked in the alts docs and grpc/examples/cpp.

I can see CertificateVerifier has a constructor and Verify that I can use but I’m not sure how to initialize and pass that into the AltsCredentials.

I’ve adapted the greeter client and server to mimick the code in the e2e tests.

My code so far:
https://github.com/grpc/grpc/blob/d43e35738ad9c624bd9eb2f9c56b93fb4cf849a7/examples/cpp/helloworld/greeter_client.cc

int main(int argc, char** argv) {
  absl::ParseCommandLine(argc, argv);
  // Instantiate the client. It requires a channel, out of which the actual RPCs
  // are created. This channel models a connection to an endpoint specified by
  // the argument "--target=" which is the only expected argument.
  std::string target_str = absl::GetFlag(FLAGS_target);

  // Add CertificateVerifier with custom certificate_verifier
  std::vector<IdentityKeyCertPair> identity_pair;
  identity_pair.emplace_back(IdentityKeyCertPair{client_cert, client_key});

  auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(client_cert, identity_pair);
  grpc::experimental::TlsChannelCredentialsOptions options;
  options.set_certificate_provider(std::move(certificate_provider));
  options.watch_root_certs();
  options.watch_identity_key_cert_pairs();

  auto verifier = ExternalCertificateVerifier::Create<ABCertificateVerifier>(true);
  options.set_certificate_verifier(std::move(verifier));
  options.set_verify_server_certs(true);
  options.set_check_call_host(false);
  auto tls_creds = TlsCredentials(options);

  GreeterClient greeter(grpc::CreateChannel(target_str, tls_creds));
  std::string user("world");
  std::string reply = greeter.SayHello(user);
  std::cout << "Greeter received: " << reply << std::endl;

  return 0;
}

https://github.com/grpc/grpc/blob/d43e35738ad9c624bd9eb2f9c56b93fb4cf849a7/examples/cpp/helloworld/greeter_server.cc

void RunServer(uint16_t port) {
  std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
  GreeterServiceImpl service;

  grpc::EnableDefaultHealthCheckService(true);
  grpc::reflection::InitProtoReflectionServerBuilderPlugin();
  ServerBuilder builder;

  // Add CertificateVerifier with custom certificate_verifier
  std::vector<IdentityKeyCertPair> identity_pair;
  identity_pair.emplace_back(IdentityKeyCertPair {server_cert, server_key});

  auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(server_cert, identity_pair);
  TlsServerCredentialsOptions options(certificate_provider);
  auto verifier = ExternalCertificateVerifier::Create<ABCertificateVerifier>(true);
  options.set_certificate_provider(std::move(certificate_provider));
  options.watch_root_certs();
  options.set_root_cert_name("root_cert_name");
  options.watch_identity_key_cert_pairs();
  options.set_identity_cert_name("identity_cert_name");
  options.set_cert_request_type(
      GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY);
  auto server_credentials = TlsServerCredentials(options);

  // Listen on the given address without any authentication mechanism.
  builder.AddListeningPort(server_address, server_credentials);
  // Register "service" as the instance through which we'll communicate with
  // clients. In this case it corresponds to an *synchronous* service.
  builder.RegisterService(&service);
  // Finally assemble the server.
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;

  // Wait for the server to shutdown. Note that some other thread must be
  // responsible for shutting down the server for this call to ever return.
  server->Wait();
}
#include "ab_certificate_verifier.h"

using grpc::experimental::TlsCustomVerificationCheckRequest;

bool ABCertificateVerifier::Verify(TlsCustomVerificationCheckRequest* request,
  std::function<void(grpc::Status)> callback,
  grpc::Status* sync_status) {
    if (!success_) {
      *sync_status = grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
                                  "SyncCertificateVerifier failed");
    } else {
      *sync_status = grpc::Status(grpc::StatusCode::OK, "");
    }
    if(request->peer_cert().empty()) {
      std::cout << "Empty certificate" << std::endl;
      return false;
    }

    // check environment variable, if not set, return false
    const char* env_var = std::getenv("AB_CERT_VERIFIER");
    if(env_var == nullptr) {
      std::cout << "AB_CERT_VERIFIER is not set" << std::endl;
      return false;
    }
    return true;
};

I’m using the same certificates and private keys from those tests.

std::string client_cert = R"(-----BEGIN CERTIFICATE-----
  MIIEGzCCAwOgAwIBAgIUVwCmP2zKfeoWdaMbn32PjFgpdRswDQYJKoZIhvcNAQEL
  BQAwSjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G
  A1UECwwGR29vZ2xlMRAwDgYDVQQDDAd4cGlnb3JzMB4XDTIxMDQwOTE5MzgxOVoX
  DTMxMDQwNzE5MzgxOVowSjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYD
  VQQHDAJTRjEPMA0GA1UECwwGR29vZ2xlMRAwDgYDVQQDDAd4cGlnb3JzMIIBIjAN
  BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwIOdWFPDZIAu8B8eyX/WY/sIx3C8
  XnWOnM7BY4ZfcFJWEZEPsHbd7Lh/ffE623VTHN/LaSOOtKRpO4BgaTJG2YKNWDZ8
  M/1NYwDkddRFRhzdUA8tx9ASNhOUWLZf701kCxaNZISGiiuHkt6baz3ftVwei9uR
  OCEjlYHd8p+G+XkzBkWxHovsK5GSMmQFYAiMPHegRx8JXVs9wJYykRKzD8yrZ8lS
  /nLadCsejm6aYgTzzloTTU7TJJi4H3mUCt1O3DzeViNdxbKNORQN8FaP8OrF07c+
  IpyE9eGOexN+FerFmXs6wW07QCNMjdw6l0ikwYBozkFG46lcUovyc0W2aQIDAQAB
  o4H4MIH1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMIHaBgNVHREEgdIwgc+CE2Zv
  by50ZXN0LmRvbWFpbi5jb22CE2Jhci50ZXN0LmRvbWFpbi5jb22GIGh0dHBzOi8v
  Zm9vLnRlc3QuZG9tYWluLmNvbS90ZXN0hiBodHRwczovL2Jhci50ZXN0LmRvbWFp
  bi5jb20vdGVzdIYYc3BpZmZlOi8vZm9vLmNvbS9iYXIvYmF6gRNmb29AdGVzdC5k
  b21haW4uY29tgRNiYXJAdGVzdC5kb21haW4uY29thwTAqAcBhxAAEwAAAAAAAAAA
  AAAAAAAXiAMqAwQwDQYJKoZIhvcNAQELBQADggEBAIHzi/MWANQDYqpNDGVA6HGg
  vYPnwxjLXL/8apVf1ZMHzS/R6Eudu8ugppnnEL7Cjsd4oA0r/sJLjBvhaZtf0r4S
  GguWdmai2RR1ghwkCLPF/HlCqiBKwUfWrjTxq8GOwwodhW7lk4hLPzhFRzh/I93g
  uN5/ugPKcloWQ7X/0okMdkdPmk8uLpMckXNKj13Lupl/0BgDggghVXRTA2t0ujhx
  TvRWfYi5H1eJtNcj824PaIDifPiSOpzeXZi+na2XzzVmCz5n/e2H4nlTMVcN6YGG
  M3U3uJqjjjpKkCrrdNAJJpqqJpln4P6fVvO2ND1QmyE5YIKV3tZ8p38AJOheUcw=
  -----END CERTIFICATE-----)";

std::string client_key = R"(-----BEGIN PRIVATE KEY-----
  MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq
  6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+
  WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t
  PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86
  qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh
  23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte
  MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU
  koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj
  1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5
  nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB
  8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi
  y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t
  sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB
  gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y
  biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC
  Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l
  dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP
  V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp
  Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1
  QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA
  xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys
  DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83
  FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv
  nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH
  awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r
  uGIYs9Ek7YXlXIRVrzMwcsrt1w==
  -----END PRIVATE KEY-----)";

std::string server_cert = R"(-----BEGIN CERTIFICATE-----
  MIIDQTCCAikCFGyX00RCepOv/qCJ1oVdTtY92U84MA0GCSqGSIb3DQEBCwUAMFYx
  CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
  cm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMMBnRlc3RjYTAeFw0yMDAzMTgw
  MTA3MzhaFw0zMDAzMTYwMTA3MzhaMGQxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApT
  b21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxHTAb
  BgNVBAMMFCoudGVzdC5nb29nbGUuY29tLmF1MIIBIjANBgkqhkiG9w0BAQEFAAOC
  AQ8AMIIBCgKCAQEAnovWirrQzYNpqwjiAluIU+vi1SXyOSeg2d8qm+jAc23P/1Xl
  I9Tgl/77eb7z5tUG+VzZNTDiBTxFurdv9DIY9lW0H4w3vz4WUYcLuDwbbqCQQ3gb
  CpHZ8Q9CPuBtF0W3k7VLBjBgYFkgHahzA9h5xIYMnUcNJK2I2aeGvxzAp2WdVhWp
  sNTwr0CugLlkkpOIhxmmJJV1Gind60ZZLo6+//4oXL6sHifjnWk8Fw/M+D9GHxXv
  25FlqbqiGc9Tq/58YjQbb1ipSPKUdit8JKFrFvum5xTfLvNZ7LRw2q3Ues8twe0F
  5HtNquLHXVBEXMDRcj6qbHHrTKZ4YJYY65pwFwIDAQABMA0GCSqGSIb3DQEBCwUA
  A4IBAQCCGvZpM+t83xWPCsz5FyuCqA6LI+j0NMMmKpe1wJ8JcK2o9Qw4d0wPxWdy
  0O7Ti2YlJS3gups00zsaFhQymIKUBi5Gc+1VC7qHUUrVtkoIRe6QSpcVlxPVczlD
  If1egkjBCUZKVSWqYRKB6AMqjpp7/dF06j6zAaAH54jaLv9VmiBtsFyd017AsC9W
  +OG2ke2dNtXySfVX4VusCcji86qb5sr6hNIQWMXk6dZoLDsZvwvVi7KnrqQOza8J
  klcJXV8Hsnq/faHr/ZmsIA65N0+H8KuYfbO+s5nKPG9th6ZZAu4aY2VDei++TH+H
  EAQhivPNUC1DgCmx0P7vKLhgka7S
  -----END CERTIFICATE-----)";

std::string server_key = R"(-----BEGIN PRIVATE KEY-----
  MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq
  6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+
  WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t
  PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86
  qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh
  23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte
  MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU
  koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj
  1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5
  nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB
  8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi
  y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t
  sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB
  gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y
  biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC
  Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l
  dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP
  V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp
  Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1
  QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA
  xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys
  DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83
  FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv
  nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH
  awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r
  uGIYs9Ek7YXlXIRVrzMwcsrt1w==
  -----END PRIVATE KEY-----)";

This setup yields warnings and failure to communicate, logs for server:

INFO: Running bazel wrapper (see //tools/bazel for details), bazel version 7.3.1 will be used instead of system-wide bazel installation.
INFO: Analyzed target //examples/cpp/helloworld:greeter_server (4 packages loaded, 36 targets configured).
INFO: Found 1 target...
Target //examples/cpp/helloworld:greeter_server up-to-date:
  bazel-bin/examples/cpp/helloworld/greeter_server
INFO: Elapsed time: 4.544s, Critical Path: 4.12s
INFO: 3 processes: 1 internal, 2 linux-sandbox.
INFO: Build completed successfully, 3 total actions
INFO: Running command line: bazel-bin/examples/cpp/helloworld/greeter_server
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1725871960.344178   45019 ssl_transport_security.cc:794] Invalid cert chain file.
E0000 00:00:1725871960.344233   45019 ssl_utils.cc:502] Handshaker factory creation failed with TSI_INVALID_ARGUMENT
E0000 00:00:1725871960.344256   45019 tls_security_connector.cc:715] Update handshaker factory failed.
Server listening on 0.0.0.0:50051

client

INFO: Running bazel wrapper (see //tools/bazel for details), bazel version 7.3.1 will be used instead of system-wide bazel installation.
INFO: Analyzed target //examples/cpp/helloworld:greeter_client (0 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //examples/cpp/helloworld:greeter_client up-to-date:
  bazel-bin/examples/cpp/helloworld/greeter_client
INFO: Elapsed time: 2.329s, Critical Path: 2.00s
INFO: 2 processes: 1 internal, 1 linux-sandbox.
INFO: Build completed successfully, 2 total actions
INFO: Running command line: bazel-bin/examples/cpp/helloworld/greeter_client
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1725871985.814376   47042 ssl_transport_security.cc:794] Invalid cert chain file.
E0000 00:00:1725871985.814441   47042 ssl_utils.cc:463] Handshaker factory creation failed with TSI_INVALID_ARGUMENT
E0000 00:00:1725871985.814457   47042 tls_security_connector.cc:451] Update handshaker factory failed.
E0000 00:00:1725871985.814603   47042 ssl_transport_security.cc:794] Invalid cert chain file.
E0000 00:00:1725871985.814623   47042 ssl_utils.cc:463] Handshaker factory creation failed with TSI_INVALID_ARGUMENT
E0000 00:00:1725871985.814633   47042 tls_security_connector.cc:451] Update handshaker factory failed.
14: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:50051: Failed to create security handshaker.
Greeter received: RPC failed

I can see the e2e tests are also not working on my machine due to failure to start the server. Will look into that.

I now realize the fault of my ways, was using the certificates from the unit tests and those refer to a specific endpoint, also had the wrong root certificate.
I’ve created a self signed certificate and keys and everything seems to be working. Will try to add this as an example to the grpc repo so that others can benefit.

greeter client.cc

std::string ca_cert = R"(-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIUEgsv2XdOGiDu0s8OmDVT6vpvjk4wDQYJKoZIhvcNAQEL
BQAwDzENMAsGA1UEAwwETXlDQTAeFw0yNDA5MDkxNTE4MjJaFw0yNTA5MDkxNTE4
MjJaMA8xDTALBgNVBAMMBE15Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC0hoNdKMfmZkp3RouARUsxED0Bu4OaOjZftvvoVdqMoMqJsJEyi4zAR+ho
0bs/0yGw4MQfQyXVtv/+P/0r+pSPFQeNweyPqzFKe0eMaBL2DNxWHNbAUCvivUI7
2oGOul1M7rupI2bEKRUEnq8oAjV8f1HVwWY6huBH4FLTdsggsnc1WWqjZceNDGVz
Qq3lo27apMAhfeLlVqgGXV1Z23u3vQ5/8xYnT/Gk/bvRfEr2LrFx7WFsBk7ajgQy
QXbmkHh0GVDbCAZCuJIk3YzbqTDiUwXdcFaDFcjsHoCojONq+IroR/sFs7/LHtjr
/IwkyO3sacXpMPX5I+mQPBXeTZihAgMBAAGjUzBRMB0GA1UdDgQWBBQzOqHNiqo2
euaxWILuUIj52NW2ujAfBgNVHSMEGDAWgBQzOqHNiqo2euaxWILuUIj52NW2ujAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAsGVCbMe7ClQCtYZFE
lakrC3rv8ue7ip6zNdXk2cyCTK5aEEqt/dP2EINyB4RWbXZLfpHC/oU4qW82jfrf
QCFqeIQj94gFTR2JDVMbYrYyJiUVsdcWnUvLWA3CLdRFquG96tmfPrAzVdhV0Utj
g0phovVcc5pyqeDwS1CqM5RncEFS+fHfvqvaf3EoSbKTdbngxLcXUYQEywo+KVlg
C8j5bTwasraqPXZjl5EAYXBhXx6J0t33UiJrS0XamJEOPOTGcAFaGKb95pbHBCX1
BeXo1uFx7Vtk8Xo56QhjkkOXFBJePZqzuK649icHWN+QKPOMO8469LNjWksn/Xrz
gHnL
-----END CERTIFICATE-----
)";

std::string server_cert = R"(-----BEGIN CERTIFICATE-----
MIICqjCCAZICFBXSuBVnXcJLFDq8kx64p4WjETeKMA0GCSqGSIb3DQEBCwUAMA8x
DTALBgNVBAMMBE15Q0EwHhcNMjQwOTA5MTUxOTE4WhcNMjUwOTA5MTUxOTE4WjAU
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC6TtLbYnCuQa5SKOreDamYzE/Ka5mVoOFoh0T+hlGMIjjw+aB+TfnCa/3u
xbhkH7bXPBGmFTm92lPOMSOBJxaOx5cEaRdYoPTTZchBT8BRndB6dymj3U7mpwwS
WwOieHalv/WXm5TzodI+utTVIkmcvgL5WQuAQvVVSsqgwfxMf3F4CKutocsYlDhG
HcxQY/pUr4rGU21bhU9VH+h0uujuJmnLPKkILvbRX3cTcu3psLNvrcjdVBzHLEq4
gcMwxNAU7HQM1/7t/0s0w86g1D6aWYdPxwJ4R9cnWZ2e/u3J4e2mpSfDtkRtQ/PY
eW0F+bHGEl/UG2IHOud0EJFTQoG7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGHd
R4eWG51mW/DAOa7m5+IIVgS2NhAXfsOotfwQeX2dY49Hkgaf63q1f1lfHc1HUjkR
Vg+8ypxUohtqzEM1RYDAHGFoiDFEdCNFvGqiCusS7Ql1OwAkECH3oU8P/2u+wgju
hdpnTiQvvUKObmTIJVYzzpuNmHWx758lD2X69aXH5bV2eTnhPlxsK+ct5YfgtUSJ
guJkvtbtxzeuoiUL3frYztyZN0xUw1j7FeTxyLJxYVPqluGUqkRKyLqcg8v5JTt8
YxatRU+lvptxLAOvBeLyCsr6fXwsMPH5UMNeH5mBEIg9Vec1XiyS8XmX/eTE2n+p
shiBdkDyfSTUBKPPjcc=
-----END CERTIFICATE-----
)";

std::string server_key = R"(-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6TtLbYnCuQa5S
KOreDamYzE/Ka5mVoOFoh0T+hlGMIjjw+aB+TfnCa/3uxbhkH7bXPBGmFTm92lPO
MSOBJxaOx5cEaRdYoPTTZchBT8BRndB6dymj3U7mpwwSWwOieHalv/WXm5TzodI+
utTVIkmcvgL5WQuAQvVVSsqgwfxMf3F4CKutocsYlDhGHcxQY/pUr4rGU21bhU9V
H+h0uujuJmnLPKkILvbRX3cTcu3psLNvrcjdVBzHLEq4gcMwxNAU7HQM1/7t/0s0
w86g1D6aWYdPxwJ4R9cnWZ2e/u3J4e2mpSfDtkRtQ/PYeW0F+bHGEl/UG2IHOud0
EJFTQoG7AgMBAAECggEABhLOajsfTM70qg84v/jWfhYD4hlppv28hhJ7tNzeweed
ec9bOdER0RKBgZMqRlXAjs88RdRvwI5wmkpkPXdhL/yZsCgCNtOobqSsHMa4yWIe
w/SaEgQgBQI4ovepCOUVUfnxWd2qwy9a2QUbgxVqnj8/78rFG+DaCFnFmUHH3nEn
MXXfixKY6oE11DErZXVGNGxwy6ygfMn5/nBI9EjkAtl649rqpqeGL6xP5vQwqumr
ZjLrrxjv+9yIBiW0FHZrW7i4dPF6tYwtA+X80KeJGPheIfUlw3dxD+FFJqhKRVuS
Mqh15aXJEliqzVu3Ap54EnJkALIAvpbSRpABEcv5IQKBgQD7/q4UqJ3FgAkF7I3n
N1gro4oQzqFdIM1LHTjAsM4N746gpNmNvFYD68qOLYSiWiQ7aOrEA2XBb+jDULRU
UDUqZ3vJDaVvrQ/baWSLYxUoJ8C0Iu6yA++DCsmKM38Jir4Pp5MkO5EWD3gN54iD
pRpb1YHL8imsh9gO16BsfrqLJQKBgQC9ROAx07XzTbsj2yWKFqz/20YwaF+183ys
/TVaqamBPJBIATYSUiMjxnKBFZVnTtRMzG9lG9ckEoCS2qCyoAkJztJsiqe138VS
68O5T91XChfyeibmWU53V3NM/3vyKXUVAjaj85mXei+4sosYjK8wjQ3qDaPIfgAP
THDu/GazXwKBgHUBdhcFi+xOXOIxSlpXqkro7oyLRQWW23vLH7To42Q5HUKeCJ31
GwNLEowdun4f2L71IjzNTwwYSD2YVYLokycTUbiy62QFOV2pfBP0d7hjbOi3Z5mk
liuEcLwI2S23DDT8nCewuNdDa30ZSpvFp42If3IRCSShFsMdf9GgrkE5AoGAAg/g
CWrvDomIQmm+zPRWSitPZnOcp1TRxOi1ThmPGTNZtw8cUbLHYzpkQPfFOuzm7zdC
920IOQJimDb9jTSlJJA2Rqx0C002zyJ2bWxrUulvPVsLVXMfobk4LlySMx80gVgW
1E5xG+9e2bpIPao6tmKzBhvD7wlAYupISLJDRC0CgYBgzyDOjf23s7XFnw51V72k
yzI5DW7YMrVoujQyk+S00wTmlEa2NARAJj7rPDZpGPW6VM0/n18fnT2raF+LHxeZ
O1+smifQPAzrRbj7R/hwdZuLyyQPiRqGZHaOfnBBaCxvklnBUsupujH0vTy0N1TO
gQpkG/wCNnIGHr6jcq3rKg==
-----END PRIVATE KEY-----
)";

    int main(int argc, char** argv) {
      absl::ParseCommandLine(argc, argv);
      // Instantiate the client. It requires a channel, out of which the actual RPCs
      // are created. This channel models a connection to an endpoint specified by
      // the argument "--target=" which is the only expected argument.
      std::string target_str = absl::GetFlag(FLAGS_target);
    
      const auto channel_credentials = CreateTlsChannelCredentials(ca_cert, server_cert, server_key); 
      GreeterClient greeter(grpc::CreateChannel(target_str, channel_credentials));
      std::string user("world");
      std::string reply = greeter.SayHello(user);
      std::cout << "Greeter received: " << reply << std::endl;
    
      return 0;
    }

greeter_server.cc

std::string root_cert = R"(-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIUEgsv2XdOGiDu0s8OmDVT6vpvjk4wDQYJKoZIhvcNAQEL
BQAwDzENMAsGA1UEAwwETXlDQTAeFw0yNDA5MDkxNTE4MjJaFw0yNTA5MDkxNTE4
MjJaMA8xDTALBgNVBAMMBE15Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC0hoNdKMfmZkp3RouARUsxED0Bu4OaOjZftvvoVdqMoMqJsJEyi4zAR+ho
0bs/0yGw4MQfQyXVtv/+P/0r+pSPFQeNweyPqzFKe0eMaBL2DNxWHNbAUCvivUI7
2oGOul1M7rupI2bEKRUEnq8oAjV8f1HVwWY6huBH4FLTdsggsnc1WWqjZceNDGVz
Qq3lo27apMAhfeLlVqgGXV1Z23u3vQ5/8xYnT/Gk/bvRfEr2LrFx7WFsBk7ajgQy
QXbmkHh0GVDbCAZCuJIk3YzbqTDiUwXdcFaDFcjsHoCojONq+IroR/sFs7/LHtjr
/IwkyO3sacXpMPX5I+mQPBXeTZihAgMBAAGjUzBRMB0GA1UdDgQWBBQzOqHNiqo2
euaxWILuUIj52NW2ujAfBgNVHSMEGDAWgBQzOqHNiqo2euaxWILuUIj52NW2ujAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAsGVCbMe7ClQCtYZFE
lakrC3rv8ue7ip6zNdXk2cyCTK5aEEqt/dP2EINyB4RWbXZLfpHC/oU4qW82jfrf
QCFqeIQj94gFTR2JDVMbYrYyJiUVsdcWnUvLWA3CLdRFquG96tmfPrAzVdhV0Utj
g0phovVcc5pyqeDwS1CqM5RncEFS+fHfvqvaf3EoSbKTdbngxLcXUYQEywo+KVlg
C8j5bTwasraqPXZjl5EAYXBhXx6J0t33UiJrS0XamJEOPOTGcAFaGKb95pbHBCX1
BeXo1uFx7Vtk8Xo56QhjkkOXFBJePZqzuK649icHWN+QKPOMO8469LNjWksn/Xrz
gHnL
-----END CERTIFICATE-----
)";

std::string server_cert = R"(-----BEGIN CERTIFICATE-----
MIICqjCCAZICFBXSuBVnXcJLFDq8kx64p4WjETeKMA0GCSqGSIb3DQEBCwUAMA8x
DTALBgNVBAMMBE15Q0EwHhcNMjQwOTA5MTUxOTE4WhcNMjUwOTA5MTUxOTE4WjAU
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC6TtLbYnCuQa5SKOreDamYzE/Ka5mVoOFoh0T+hlGMIjjw+aB+TfnCa/3u
xbhkH7bXPBGmFTm92lPOMSOBJxaOx5cEaRdYoPTTZchBT8BRndB6dymj3U7mpwwS
WwOieHalv/WXm5TzodI+utTVIkmcvgL5WQuAQvVVSsqgwfxMf3F4CKutocsYlDhG
HcxQY/pUr4rGU21bhU9VH+h0uujuJmnLPKkILvbRX3cTcu3psLNvrcjdVBzHLEq4
gcMwxNAU7HQM1/7t/0s0w86g1D6aWYdPxwJ4R9cnWZ2e/u3J4e2mpSfDtkRtQ/PY
eW0F+bHGEl/UG2IHOud0EJFTQoG7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGHd
R4eWG51mW/DAOa7m5+IIVgS2NhAXfsOotfwQeX2dY49Hkgaf63q1f1lfHc1HUjkR
Vg+8ypxUohtqzEM1RYDAHGFoiDFEdCNFvGqiCusS7Ql1OwAkECH3oU8P/2u+wgju
hdpnTiQvvUKObmTIJVYzzpuNmHWx758lD2X69aXH5bV2eTnhPlxsK+ct5YfgtUSJ
guJkvtbtxzeuoiUL3frYztyZN0xUw1j7FeTxyLJxYVPqluGUqkRKyLqcg8v5JTt8
YxatRU+lvptxLAOvBeLyCsr6fXwsMPH5UMNeH5mBEIg9Vec1XiyS8XmX/eTE2n+p
shiBdkDyfSTUBKPPjcc=
-----END CERTIFICATE-----
)";

std::string server_key = R"(-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6TtLbYnCuQa5S
KOreDamYzE/Ka5mVoOFoh0T+hlGMIjjw+aB+TfnCa/3uxbhkH7bXPBGmFTm92lPO
MSOBJxaOx5cEaRdYoPTTZchBT8BRndB6dymj3U7mpwwSWwOieHalv/WXm5TzodI+
utTVIkmcvgL5WQuAQvVVSsqgwfxMf3F4CKutocsYlDhGHcxQY/pUr4rGU21bhU9V
H+h0uujuJmnLPKkILvbRX3cTcu3psLNvrcjdVBzHLEq4gcMwxNAU7HQM1/7t/0s0
w86g1D6aWYdPxwJ4R9cnWZ2e/u3J4e2mpSfDtkRtQ/PYeW0F+bHGEl/UG2IHOud0
EJFTQoG7AgMBAAECggEABhLOajsfTM70qg84v/jWfhYD4hlppv28hhJ7tNzeweed
ec9bOdER0RKBgZMqRlXAjs88RdRvwI5wmkpkPXdhL/yZsCgCNtOobqSsHMa4yWIe
w/SaEgQgBQI4ovepCOUVUfnxWd2qwy9a2QUbgxVqnj8/78rFG+DaCFnFmUHH3nEn
MXXfixKY6oE11DErZXVGNGxwy6ygfMn5/nBI9EjkAtl649rqpqeGL6xP5vQwqumr
ZjLrrxjv+9yIBiW0FHZrW7i4dPF6tYwtA+X80KeJGPheIfUlw3dxD+FFJqhKRVuS
Mqh15aXJEliqzVu3Ap54EnJkALIAvpbSRpABEcv5IQKBgQD7/q4UqJ3FgAkF7I3n
N1gro4oQzqFdIM1LHTjAsM4N746gpNmNvFYD68qOLYSiWiQ7aOrEA2XBb+jDULRU
UDUqZ3vJDaVvrQ/baWSLYxUoJ8C0Iu6yA++DCsmKM38Jir4Pp5MkO5EWD3gN54iD
pRpb1YHL8imsh9gO16BsfrqLJQKBgQC9ROAx07XzTbsj2yWKFqz/20YwaF+183ys
/TVaqamBPJBIATYSUiMjxnKBFZVnTtRMzG9lG9ckEoCS2qCyoAkJztJsiqe138VS
68O5T91XChfyeibmWU53V3NM/3vyKXUVAjaj85mXei+4sosYjK8wjQ3qDaPIfgAP
THDu/GazXwKBgHUBdhcFi+xOXOIxSlpXqkro7oyLRQWW23vLH7To42Q5HUKeCJ31
GwNLEowdun4f2L71IjzNTwwYSD2YVYLokycTUbiy62QFOV2pfBP0d7hjbOi3Z5mk
liuEcLwI2S23DDT8nCewuNdDa30ZSpvFp42If3IRCSShFsMdf9GgrkE5AoGAAg/g
CWrvDomIQmm+zPRWSitPZnOcp1TRxOi1ThmPGTNZtw8cUbLHYzpkQPfFOuzm7zdC
920IOQJimDb9jTSlJJA2Rqx0C002zyJ2bWxrUulvPVsLVXMfobk4LlySMx80gVgW
1E5xG+9e2bpIPao6tmKzBhvD7wlAYupISLJDRC0CgYBgzyDOjf23s7XFnw51V72k
yzI5DW7YMrVoujQyk+S00wTmlEa2NARAJj7rPDZpGPW6VM0/n18fnT2raF+LHxeZ
O1+smifQPAzrRbj7R/hwdZuLyyQPiRqGZHaOfnBBaCxvklnBUsupujH0vTy0N1TO
gQpkG/wCNnIGHr6jcq3rKg==
-----END PRIVATE KEY-----
)";

void RunServer(uint16_t port) {
  std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
  GreeterServiceImpl service;

  grpc::EnableDefaultHealthCheckService(true);
  grpc::reflection::InitProtoReflectionServerBuilderPlugin();
  ServerBuilder builder;

  // Listen on the given address without any authentication mechanism.
  const auto server_credentials = CreateTlsServerCredentials(root_cert, server_cert, server_key);
  builder.AddListeningPort(server_address, server_credentials); 
  // Register "service" as the instance through which we'll communicate with
  // clients. In this case it corresponds to an *synchronous* service.
  builder.RegisterService(&service);
  // Finally assemble the server.
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;

  // Wait for the server to shutdown. Note that some other thread must be
  // responsible for shutting down the server for this call to ever return.
  server->Wait();
}

certificate_verifier.h

#pragma once

#include <memory>

#include <grpcpp/grpcpp.h>

using grpc::experimental::ExternalCertificateVerifier;
using grpc::experimental::TlsCustomVerificationCheckRequest;

std::shared_ptr<grpc::ChannelCredentials> CreateTlsChannelCredentials(
  std::string ca_cert,
  std::string server_cert,
  std::string server_key);

std::shared_ptr<grpc::ServerCredentials> CreateTlsServerCredentials(
  std::string root_cert,
  std::string server_cert,
  std::string server_key);

class TestCertificateVerifier
    : public ExternalCertificateVerifier {
  public:
    explicit TestCertificateVerifier(bool success) : success_(success) {}
    ~TestCertificateVerifier() override {}

    bool Verify(TlsCustomVerificationCheckRequest* request,
      std::function<void(grpc::Status)> callback,
      grpc::Status* sync_status) override;
    void Cancel(TlsCustomVerificationCheckRequest* request) override {
    }
  private:
    bool success_ = false;
};

certificate_verifier.cc

#include "ab_certificate_verifier.h"

#include <iostream>
#include <string>

#include <grpcpp/security/credentials.h>
#include <grpcpp/security/tls_certificate_provider.h>

using grpc::experimental::TlsServerCredentials;
using grpc::experimental::TlsServerCredentialsOptions;
using grpc::experimental::StaticDataCertificateProvider;
using grpc::experimental::IdentityKeyCertPair;
using grpc::experimental::TlsCustomVerificationCheckRequest;

std::shared_ptr<grpc::ChannelCredentials> CreateTlsChannelCredentials(
    std::string ca_cert,
    std::string server_cert,
    std::string server_key) {
  std::vector<IdentityKeyCertPair> identity_pair;
  identity_pair.emplace_back(IdentityKeyCertPair{server_key, server_cert});

  auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(ca_cert, identity_pair);
  grpc::experimental::TlsChannelCredentialsOptions options;
  options.set_certificate_provider(std::move(certificate_provider));
  options.watch_root_certs();
  options.watch_identity_key_cert_pairs();
  auto verifier = ExternalCertificateVerifier::Create<TestCertificateVerifier>(true);
  options.set_certificate_verifier(std::move(verifier));
  options.set_verify_server_certs(true);
  options.set_check_call_host(false);
  return grpc::experimental::TlsCredentials(options);
}

std::shared_ptr<grpc::ServerCredentials> CreateTlsServerCredentials(
    std::string root_cert,
    std::string server_cert,
    std::string server_key) {
  std::vector<IdentityKeyCertPair> identity_key_cert_pairs;
  identity_key_cert_pairs.emplace_back(IdentityKeyCertPair{server_key, server_cert});
  auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(root_cert, identity_key_cert_pairs);
  TlsServerCredentialsOptions options(certificate_provider);
  options.set_certificate_provider(std::move(certificate_provider));

  auto verifier = ExternalCertificateVerifier::Create<TestCertificateVerifier>(true);
  options.set_certificate_verifier(std::move(verifier));
  
  options.watch_root_certs();
  options.set_root_cert_name("root");
  options.watch_identity_key_cert_pairs();
  options.set_identity_cert_name("identity");
  options.set_cert_request_type(
      GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY);
  return TlsServerCredentials(options);
}

bool TestCertificateVerifier::Verify(TlsCustomVerificationCheckRequest* request,
  std::function<void(grpc::Status)> callback,
  grpc::Status* sync_status) {
    if (!success_) {
      *sync_status = grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
                                  "SyncCertificateVerifier failed");
    } else {
      *sync_status = grpc::Status(grpc::StatusCode::OK, "");
    }

    // Add custom verification here
    if(request->peer_cert().empty()) {
      std::cout << "Empty certificate" << std::endl;
      return false;
    }

    return true;
};

BUILD

licenses(["notice"])

cc_binary(
    name = "greeter_client",
    srcs = ["greeter_client.cc"],
    defines = ["BAZEL_BUILD"],
    deps = [
        "//:grpc++",
        "//examples/protos:helloworld_cc_grpc",
        "//examples/cpp/helloworld:ab_certificate_verifier",
        "@com_google_absl//absl/flags:flag",
        "@com_google_absl//absl/flags:parse",
    ],
)

cc_binary(
    name = "greeter_server",
    srcs = ["greeter_server.cc"],
    defines = ["BAZEL_BUILD"],
    deps = [
        "//:grpc++",
        "//:grpc++_reflection",
        "//examples/protos:helloworld_cc_grpc",
        "//examples/cpp/helloworld:ab_certificate_verifier",
        "@com_google_absl//absl/flags:flag",
        "@com_google_absl//absl/flags:parse",
        "@com_google_absl//absl/strings:str_format",
    ],
)

cc_library(
    name = "ab_certificate_verifier",
    srcs = ["ab_certificate_verifier.cc"],
    hdrs = ["ab_certificate_verifier.h"],
    defines = ["BAZEL_BUILD"],
    deps = [
        "//:grpc++",
        "//examples/protos:helloworld_cc_grpc",
        "@com_google_absl//absl/flags:flag",
        "@com_google_absl//absl/flags:parse",
    ],
)

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật