diff --git a/README.md b/README.md index 7821d1f..e88961c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ composer require thesis/grpc - [Implementing the Server](#implementing-the-server) - [Starting the Server](#starting-the-server) - [Using the Client](#using-the-client) +- [TLS and mTLS](#tls-and-mtls) - [Target Addressing](#target-addressing) - [Load Balancing](#load-balancing) - [Endpoint Resolution](#endpoint-resolution) @@ -171,6 +172,67 @@ $client = new AuthenticationServiceClient( ); ``` +### TLS and mTLS + +The library supports both standard TLS (server authentication) and mTLS (mutual client/server authentication) via `TransportCredentials` on both the server and client sides. + +For TLS, the server needs a certificate and private key, while the client needs a trusted CA and the expected server name: + +```php +use Amp\Socket\Certificate; +use Thesis\Grpc\Client; +use Thesis\Grpc\Server; + +$server = new Server\Builder() + ->withTransportCredentials( + new Server\TransportCredentials() + ->withDefaultCertificate(new Certificate('/certs/server.crt', '/certs/server.key')), + ) + ->build(); + +$client = new Client\Builder() + ->withTransportCredentials( + new Client\TransportCredentials() + ->withCaCert('/certs/ca.crt') + ->withPeerName('localhost'), + ) + ->build(); +``` + +For mTLS, additionally enable client verification on the server and provide a client certificate on the client side: + +```php +use Amp\Socket\Certificate; +use Thesis\Grpc\Client; +use Thesis\Grpc\Server; + +$server = new Server\Builder() + ->withTransportCredentials( + new Server\TransportCredentials() + ->withDefaultCertificate(new Certificate('/certs/server.crt', '/certs/server.key')) + ->withCaCert('/certs/ca.crt') + ->withPeerName('client') + ->withVerifyPeer(), + ) + ->build(); + +$client = new Client\Builder() + ->withHost('ipv4:127.0.0.1:50051') + ->withTransportCredentials( + new Client\TransportCredentials() + ->withCaCert('/certs/ca.crt') + ->withPeerName('localhost') + ->withCertificate(new Certificate('/certs/client.crt', '/certs/client.key')), + ) + ->build(); +``` + +Practical recommendations: + +- Make sure the server certificate includes SAN entries (`DNS`/`IP`) matching the value passed to `withPeerName()` on the client. +- For mTLS, the client certificate should include the proper extension (`extendedKeyUsage=clientAuth`), and the server certificate should include `extendedKeyUsage=serverAuth`. +- Use certificates signed by a trusted CA and modern signature algorithms (for example, SHA-256). + ### Target Addressing The client resolves server addresses using the [gRPC Name Resolution](https://github.com/grpc/grpc/blob/master/doc/naming.md) specification. @@ -910,4 +972,4 @@ $server->stop(new TimeoutCancellation(30)); ``` During shutdown, the server notifies all active handlers via the `Cancellation` argument passed to each handler method. -This means your handler implementations should check cancellation and avoid ignoring the `$cancellation` argument — otherwise the server will have no way to signal them to stop, and shutdown will block until they finish on their own. \ No newline at end of file +This means your handler implementations should check cancellation and avoid ignoring the `$cancellation` argument — otherwise the server will have no way to signal them to stop, and shutdown will block until they finish on their own. diff --git a/src/Client/Builder.php b/src/Client/Builder.php index e78af49..7c34de8 100644 --- a/src/Client/Builder.php +++ b/src/Client/Builder.php @@ -185,7 +185,8 @@ public function build(): Client $compressor = $this->compressor ?? IdentityCompressor::Compressor; $protobuf = $this->protobuf ?? Decoder\Builder::buildDefault(); $loadBalancerFactory = $this->loadBalancerFactory ?? new LoadBalancer\PickFirstFactory(); - $uriFactory = new Http2\UriFactory($this->credentials !== null ? Internal\HttpScheme::Https : Internal\HttpScheme::Http); + $tlsContext = $this->credentials?->createContext(); + $uriFactory = new Http2\UriFactory($tlsContext !== null ? Internal\HttpScheme::Https : Internal\HttpScheme::Http); $resolver = $this->endpointResolvers[$target->scheme] ?? match ($target->scheme) { Scheme::Dns => new EndpointResolver\DnsResolver(), @@ -208,7 +209,7 @@ public function build(): Client $this->connector, new ConnectContext() ->withConnectTimeout($this->connectTimeout) - ->withTlsContext($this->credentials?->createClientContext()), + ->withTlsContext($tlsContext), ), )) ->skipDefaultUserAgent() diff --git a/src/Client/TransportCredentials.php b/src/Client/TransportCredentials.php index 222d5b9..3f5d7e8 100644 --- a/src/Client/TransportCredentials.php +++ b/src/Client/TransportCredentials.php @@ -4,12 +4,85 @@ namespace Thesis\Grpc\Client; +use Amp\Socket\Certificate; use Amp\Socket\ClientTlsContext; /** * @api */ -interface TransportCredentials +final class TransportCredentials { - public function createClientContext(): ClientTlsContext; + private string $peerName = ''; + + /** @var ?non-empty-string */ + private ?string $caCert = null; + + /** @var ?non-empty-string */ + private ?string $caPath = null; + + private bool $verifyPeer = true; + + private ?Certificate $certificate = null; + + public function withPeerName(string $peerName): self + { + $credentials = clone $this; + $credentials->peerName = $peerName; + + return $credentials; + } + + /** + * @param non-empty-string $caCert + */ + public function withCaCert(string $caCert): self + { + $credentials = clone $this; + $credentials->caCert = $caCert; + + return $credentials; + } + + /** + * @param non-empty-string $caPath + */ + public function withCaPath(string $caPath): self + { + $credentials = clone $this; + $credentials->caPath = $caPath; + + return $credentials; + } + + public function withVerifyPeer(bool $verifyPeer = true): self + { + $credentials = clone $this; + $credentials->verifyPeer = $verifyPeer; + + return $credentials; + } + + public function withCertificate(Certificate $certificate): self + { + $credentials = clone $this; + $credentials->certificate = $certificate; + + return $credentials; + } + + public function createContext(): ClientTlsContext + { + $context = new ClientTlsContext($this->peerName) + ->withCaFile($this->caCert) + ->withCaPath($this->caPath) + ->withCertificate($this->certificate) + ->withApplicationLayerProtocols(['h2']) + ->withoutPeerVerification(); + + if ($this->verifyPeer) { + $context = $context->withPeerVerification(); + } + + return $context; + } } diff --git a/src/Credentials/SecureTransportCredentials.php b/src/Credentials/SecureTransportCredentials.php deleted file mode 100644 index 0bbf2e1..0000000 --- a/src/Credentials/SecureTransportCredentials.php +++ /dev/null @@ -1,129 +0,0 @@ - */ - private array $certificates = []; - - public function withPeerName(string $peerName): self - { - $credentials = clone $this; - $credentials->peerName = $peerName; - - return $credentials; - } - - /** - * @param non-empty-string $caCert - */ - public function withCaCert(string $caCert): self - { - $credentials = clone $this; - $credentials->caCert = $caCert; - - return $credentials; - } - - /** - * @param non-empty-string $caPath - */ - public function withCaPath(string $caPath): self - { - $credentials = clone $this; - $credentials->caPath = $caPath; - - return $credentials; - } - - public function withVerifyPeer(bool $verifyPeer = true): self - { - $credentials = clone $this; - $credentials->verifyPeer = $verifyPeer; - - return $credentials; - } - - public function withCertificate(Certificate $certificate): self - { - $credentials = clone $this; - $credentials->certificate = $certificate; - - return $credentials; - } - - /** - * @param array $certificates - */ - public function withCertificates(array $certificates): self - { - $credentials = clone $this; - $credentials->certificates = [ - ...$credentials->certificates, - ...$certificates, - ]; - - return $credentials; - } - - #[\Override] - public function createClientContext(): ClientTlsContext - { - $context = new ClientTlsContext($this->peerName) - ->withCaFile($this->caCert) - ->withCaPath($this->caPath) - ->withoutPeerVerification() - ->withCertificate($this->certificate) - ->withApplicationLayerProtocols(['2']); - - if ($this->verifyPeer) { - $context = $context->withPeerVerification(); - } - - return $context; - } - - #[\Override] - public function createServerContext(): ServerTlsContext - { - $context = new ServerTlsContext() - ->withCaFile($this->caCert) - ->withCaPath($this->caPath) - ->withoutPeerVerification() - ->withPeerName($this->peerName) - ->withCertificates($this->certificates) - ->withApplicationLayerProtocols(['2']); - - if ($this->verifyPeer) { - $context = $context->withPeerVerification(); - } - - return $context; - } -} diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 0fd2935..f1ae227 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -367,7 +367,7 @@ public function build(): Server } $bindContext = new BindContext() - ->withTlsContext($this->credentials?->createServerContext()); + ->withTlsContext($this->credentials?->createContext()); foreach ($addresses as $address) { $server->expose($address, $bindContext); diff --git a/src/Server/TransportCredentials.php b/src/Server/TransportCredentials.php index b05ad53..85f7140 100644 --- a/src/Server/TransportCredentials.php +++ b/src/Server/TransportCredentials.php @@ -4,12 +4,108 @@ namespace Thesis\Grpc\Server; +use Amp\Socket\Certificate; use Amp\Socket\ServerTlsContext; /** * @api */ -interface TransportCredentials +final class TransportCredentials { - public function createServerContext(): ServerTlsContext; + private ?Certificate $defaultCertificate = null; + + /** @var array */ + private array $certificates = []; + + /** @var ?non-empty-string */ + private ?string $peerName = null; + + /** @var ?non-empty-string */ + private ?string $caCert = null; + + /** @var ?non-empty-string */ + private ?string $caPath = null; + + private bool $verifyPeer = false; + + public function withDefaultCertificate(Certificate $certificate): self + { + $credentials = clone $this; + $credentials->defaultCertificate = $certificate; + + return $credentials; + } + + /** + * @param array $certificates + */ + public function withCertificates(array $certificates): self + { + $credentials = clone $this; + $credentials->certificates = [ + ...$credentials->certificates, + ...$certificates, + ]; + + return $credentials; + } + + /** + * @param ?non-empty-string $peerName + */ + public function withPeerName(?string $peerName = null): self + { + $credentials = clone $this; + $credentials->peerName = $peerName; + + return $credentials; + } + + /** + * @param non-empty-string $caCert + */ + public function withCaCert(string $caCert): self + { + $credentials = clone $this; + $credentials->caCert = $caCert; + + return $credentials; + } + + /** + * @param non-empty-string $caPath + */ + public function withCaPath(string $caPath): self + { + $credentials = clone $this; + $credentials->caPath = $caPath; + + return $credentials; + } + + public function withVerifyPeer(bool $verifyPeer = true): self + { + $credentials = clone $this; + $credentials->verifyPeer = $verifyPeer; + + return $credentials; + } + + public function createContext(): ServerTlsContext + { + $context = new ServerTlsContext() + ->withDefaultCertificate($this->defaultCertificate) + ->withCertificates($this->certificates) + ->withPeerName($this->peerName) + ->withCaFile($this->caCert) + ->withCaPath($this->caPath) + ->withApplicationLayerProtocols(['h2']) + ->withoutPeerVerification(); + + if ($this->verifyPeer) { + $context = $context->withPeerVerification(); + } + + return $context; + } } diff --git a/tests/Stub/tls/ca.crt b/tests/Stub/tls/ca.crt new file mode 100644 index 0000000..6a0f9a4 --- /dev/null +++ b/tests/Stub/tls/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFTCCAf2gAwIBAgIUA8oFGUbSDjFxLs1/FOwDmIB71pAwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA0MDgxNzE4MDdaFw0zNjA0MDUx +NzE4MDdaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDWjM3ZaLms1LTHOzUKld/08Q3jokgIpAUhH1DdXdNxlCLR1aFB +4+XZnlLyOZ40E6C23mviG9SBFbzLkqhSJ+nkDomFdzfvEAtS12QhS95BVsewoKyc +HkkOII7l36Q4+ZAhU3vlZ5MCwz4mduebOumtxzqO3p7WWT2EqIxvxFo3SOYSGJkW +jFopiU/ScdSZT35+AWLGjZijUo8DqzNFyJbIyEm+qxPc1DHQs9oMxS9JE6uz9MqX +BgjoNyRwnvsu16zEbCfjWNm58shsEj0u6bNj4inMqUUBNTgDMkAmpCy89iuSQ54j +zwY8RyyW/HTKwvXzLFoavcLDUI8rmXeo6en/AgMBAAGjYzBhMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSeqyv99TEQiIxnC+NCC3T4 +5k0ORjAfBgNVHSMEGDAWgBSeqyv99TEQiIxnC+NCC3T45k0ORjANBgkqhkiG9w0B +AQsFAAOCAQEAoQxCLibnMMrxiR/Dv97WrDth3Poa5ybYH+hP5BKcndFjX3GtsKWN +LBcQl9rg9pRHnC+TIueYmfrQ+HdtXP2mKANdKANQpiuQEE65h69gBl7VC4Pna3D4 +vz4xN+zJA6MHHBRWsBonktwqpoUixDOSkUKP0cQEHUz37TLw3a5tq6CpNQJ9GZcU +Jpz47+a2Ze3RSd2EGeswdvoDWV2KeDl+FkC/Tl6FKYi8LIVw8B6ySaRqw7neRSh4 +X7hf/vAyHnU549MxJmUh3IotQMDsSYsAygzSiM6DNuR4RL2Zx+fHmgeqA7pLit/v +mCC+oCeORzvW+zt4Pcjas0cXmtrYWbbAlw== +-----END CERTIFICATE----- diff --git a/tests/Stub/tls/client.crt b/tests/Stub/tls/client.crt new file mode 100644 index 0000000..ca3ee9b --- /dev/null +++ b/tests/Stub/tls/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgIUECO3Vf5VteftZvDXsCeDflnQOdswDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA0MDgxNzE4MDdaFw0zNjA0MDUx +NzE4MDdaMBExDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAJzg/oMvJLDY+A7xVFAXvcgnYa9huG0xyYdoVf6/Zxg3bvWQ9dfu +WlOR5fjI99VSdULTp0AgeL8zwXXVfnV/TLLrlYvMWledzf8oWQJYjNZ6FsYYYgjl +QJ9gMYhvwgjZvpOepoTXdRd8at7GLJfuBQpXb2JMx2/93ApM+zGXQs5lUgO+3AgP +Bv+1C1x5P6y7aIPKLh4cTw1Hb1KPyCFzBZhXFhe13dBTdDXJkqcsNYewuR1eaxGn +sU23lfysor5MrPBf52PfQXyJiIcSWp5X1AzwWI/eOBWygSuRr3jtZIaMUa0YBJUF +lizt+MCUhkWIA26tCcNV9+Awt6hlogZpUO8CAwEAAaOBjzCBjDAMBgNVHRMBAf8E +AjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjAXBgNVHREE +EDAOggZjbGllbnSHBH8AAAEwHQYDVR0OBBYEFBAIPbsoyBXJdhwINh5FFFn9oKZW +MB8GA1UdIwQYMBaAFJ6rK/31MRCIjGcL40ILdPjmTQ5GMA0GCSqGSIb3DQEBCwUA +A4IBAQDUEwObLHQLhwVz8+04MnvleK2cWVNkD3PJzTRBB7Y9tR7xpuyehuXwFUwg +CB3RWZ2KdgU1i4JCRdoPJ3G6DzGRrNRjnsPWOLf/uXt+Gi3pS8EM3XASXDbVyJ2b +1tApArWBsybTnyB5BTODoYXhW3eE6upXUUChGU5/lFOrxiaISa9olr8f6qWaXuPQ +tLeLQ/YmK4NxbLb/gRONoGEFSCpwnbZ0zvakq/SwvIdp1SdMUmB/nC790AJN85Ra +//OqdvykOMon9llH4l8iuhueHVvck3OKN4Wy1Z2StRrryl7XopOk8em8EyUyOkh5 +dy8x6I7UACfXvqdShHij76GINK/n +-----END CERTIFICATE----- diff --git a/tests/Stub/tls/client.key b/tests/Stub/tls/client.key new file mode 100644 index 0000000..965f94c --- /dev/null +++ b/tests/Stub/tls/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCc4P6DLySw2PgO +8VRQF73IJ2GvYbhtMcmHaFX+v2cYN271kPXX7lpTkeX4yPfVUnVC06dAIHi/M8F1 +1X51f0yy65WLzFpXnc3/KFkCWIzWehbGGGII5UCfYDGIb8II2b6TnqaE13UXfGre +xiyX7gUKV29iTMdv/dwKTPsxl0LOZVIDvtwIDwb/tQtceT+su2iDyi4eHE8NR29S +j8ghcwWYVxYXtd3QU3Q1yZKnLDWHsLkdXmsRp7FNt5X8rKK+TKzwX+dj30F8iYiH +ElqeV9QM8FiP3jgVsoErka947WSGjFGtGASVBZYs7fjAlIZFiANurQnDVffgMLeo +ZaIGaVDvAgMBAAECggEAJP7OJmOUtHvGK8fviR/AKpZW7AQQePe3vc32EahTiJtx ++EkiILTd6j1KzVufTlguvP6qv22o6/Yplj8tnknR7ZOu5ZPgbi8RqC18qBf3xpBu +FHMlk1F05uDJbZJlYK7wyWEthcBFpa86iKSVWZLQ73plEvpmQdvM60TyK+Su5XsS +c+u0oBS7/zkwwrtLYwjcZHhp3OUydI+vLfUR1okzsBHreZhKnWjIS2+0AarRhQxm +86MQoOHR66bkTj53WtnGJWHouCZTl2swt/11bZcN5XUdoQirqkchy1F8ROgNe6xY +P5XrXbIyVbPx6Ol0uHlf3SZJNd2kVapNFEbGFG1SAQKBgQDa5X97XNWV5DkYf4hK +NYXAnVIK+hcNsIwmCmQt1gb3UYd094fHEKd+vOqoEOIGVYmYaC13Yd7jBwhVv3Dz +mXkCqi7Az9bL5xTDN7L+OXknN/5PgLIX8KrlE3lnZLe89duj+H+aTsIn4TtuoGax +9Wl0xNP0q38ye6xLJt+Suhrh7wKBgQC3eGMuL0S0RqW9j4kmIDoOC9uiDPZO/6Nr +6DWtWZmz+wUKO9RfouBjuE/PjFIuiRmHTk+CLEYNFG6NQrP/KQxuRqemQRhhvn1E +ErHgfiwFn8YsAd3II8pv1/haQo0KeYUIeoB7Czgd8b/Do5ft46fAy3APLVu6jfeM +pcTEF9uBAQKBgQC22BL3oeELNrr2mrjme/TAoujAII0XM0yuDeoGW4wwFFsYuhch +kejHNdixEIEsl1C7DPJFkWwTqXEt5x7bbGwnbK5nAnVCl0DBnIoYTzvDJcN492BM +HQ0zEmRLmFbeMWQJgG5eH8GQfFQfoT6HEh7Hmg4Ohx5VtfaCQ/Nm5avE4wKBgBNd +NHKacJDqZ/HS5XHNsvLqk3rODW0XFCN+dbPfIhbZ5pVG7UpOwYok2N+Z+s6Ij9Y1 +ELU3gQk+X9Cu+ppytaop8OXeNvmSBlyZ8IY2KuVJELxyUfses+4MCF1WatZxLvZP +H6Jc88ZNTqUSbiWggoMZtbEUY3sUIs2IbXUY44UBAoGBAKDfInsGtYf1D6y3Y2yZ +LDa8Hx0Itey5fCHoEtVJAzAE7nNx5iiV7I89puf7TAWBJq8zUQHvl6+apENno2vE +nFuBtbU53jDWUQ28frhDYMc/E3ZbDYPW1fU66ShvtrMNt0Ht/F2tP6q2oouvc9CL ++Ka6vmg3TpW8wyamU2+9ocbr +-----END PRIVATE KEY----- diff --git a/tests/Stub/tls/server.crt b/tests/Stub/tls/server.crt new file mode 100644 index 0000000..defb830 --- /dev/null +++ b/tests/Stub/tls/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRzCCAi+gAwIBAgIUECO3Vf5VteftZvDXsCeDflnQOdowDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA0MDgxNzE4MDdaFw0zNjA0MDUx +NzE4MDdaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOgLaBYN44Tl+AUWzO/PwiNZl0VySlnqojXrWuKYj85YCewI +NHrYHnAcx6nVfLVHNsJEB72/ecyEjKxcKh6XtuIYP1ueQ7Nj16lq2axK5qliJcdU +LYYqnA0M9puljV4ujwX2+IGnZub/wSsV3bMz5p72RXNEfQS4ukUkFe9kc3mTkrk0 +OjFgia0Lr+3qQq2A0b1NL6YOpprfeL7eZ3zcJqdqaWkG5JL4Rw8YhDdjesNllVVV +EI+CVM8IEne36aJ3OYSv9Wet9RQmA5TKjJCONcDTMIpU7nS7Eof3Q3jMSdDBd0Z7 +KYcis3bqunqYR4f/OIQnZAVFTlK204fBqg1OaWMCAwEAAaOBkjCBjzAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNV +HREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFKWKvXc0izpeAhkSWSUo +xdWtO8eEMB8GA1UdIwQYMBaAFJ6rK/31MRCIjGcL40ILdPjmTQ5GMA0GCSqGSIb3 +DQEBCwUAA4IBAQBASgelbjFYZCskFL7DXYFtIPRyY6qvqyLLCtvPfMw5ocM4PQ7P +0+ar6d/8ZUfcZF6KNjeFNEFhPp8tsnFc3Y547JI1ziDKOPmI3bDoFqgujDfBJThT +Y1ckS0oDrknpLFvOyD4qjx1HmGgpUfnu2wlJOPxuwAGhniPCTKpxLpEJpJYxYTP4 +sb3kYj9iL4CFpBGQ0pllY4OWm+9dwatprZr11tCDE2aHHbRMI3fleXxkn4FLTsD1 +i2Om1glMkyKT6PLHR1uqldm1kkqdBo7cMPvitTHvld7Jlc2hWCQxfTye0bWirsyz +MfwaqlW6ti+YFrmMwpRM8wBIlgE5PAIBVSID +-----END CERTIFICATE----- diff --git a/tests/Stub/tls/server.key b/tests/Stub/tls/server.key new file mode 100644 index 0000000..423d88f --- /dev/null +++ b/tests/Stub/tls/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDoC2gWDeOE5fgF +Fszvz8IjWZdFckpZ6qI161rimI/OWAnsCDR62B5wHMep1Xy1RzbCRAe9v3nMhIys +XCoel7biGD9bnkOzY9epatmsSuapYiXHVC2GKpwNDPabpY1eLo8F9viBp2bm/8Er +Fd2zM+ae9kVzRH0EuLpFJBXvZHN5k5K5NDoxYImtC6/t6kKtgNG9TS+mDqaa33i+ +3md83CanamlpBuSS+EcPGIQ3Y3rDZZVVVRCPglTPCBJ3t+midzmEr/VnrfUUJgOU +yoyQjjXA0zCKVO50uxKH90N4zEnQwXdGeymHIrN26rp6mEeH/ziEJ2QFRU5SttOH +waoNTmljAgMBAAECggEAAwqbO6bE86n7/T0WRJJpjgMPm4Zq+RC9PY2vLEcpo1++ +CYz1+yI4C+okp9I3hzy+t/8iRtdqpkaafWvb1XjRmdcNhYrmgkRtwDe5UKvOqCu0 +KQ+uZRLL5tPjq5iY6MIdd8uL6E8kZyS1CHaJ6F5myHCb6m3cYaJ2No/JhdDAPrit +EKgCeHmYhF48ALCq09kkoRdqMcAF8kzTwnCAUuBt6YQTK35X+aO+Ghi1eAaZPFmz +frRrwW6kZWFWmyW3yL2iO1C3pKAGZk+mOorsK3ecJav0SP/vaiVQh6kbacWGqUbG +fHt1TCqMbgyKycX4lVT1+1V6yZ2A32308TfzHNKEqQKBgQD40VEY7EHOWJLfnDi+ +rI16lBnUolVvAp30Sxq5+wwTPweRkYS42qDhP2CrGRVdKasoreedRAHkWqZbNHoF +7vXBJLvHCRd9mGBxl8K+Z+wMTC3viNgpLkYOxvUFeXtgF4oqB7vD21pSKlRkO9eT +egT0juf4BiU8l4h9Hm1C6KXs9QKBgQDuviRb1fA+LAPhB+HwVGe5cB3QFm8ZskBO +up2aBejf3c9qmMViQ02j2QNgf7zmx6RqvrLAhrB4yuMPoVHglZRA9UcLGdgGIqYe +AEN+xNywYA1GrpJnE5OCa5W9liH+pNILmo39xg3weBtNgi1f7COYzl4U8jlBbzyI +IPFce34F9wKBgQDd6zL9m5qFxBIbRmaGCbiV34m6UJ8KR7dBr8rCndqDP/AsMTHC +/ZwHylMZx2F6mjOCMLknVwSo+wDsz70Vla+y+GRSfRVqe4FpTwXudRnfTgKaZd8F +u7PYcjMPEu6rIdOk8QjTAs1oA0EJtSXs7yOETAExAaoshKrDjWO+DWoQEQKBgHiR +a/k5BNfRELBvc6oUZkGfHl89RSRcAeAAnKDxRRbxhNNXja8/QBF0AiGUwQhnYGBM +KCfUnYCTLbJyDNB3meRoOlWf2pnWm7/g8DUwJCeqimnctbD6kO83d+rDzC02faFA +j17Oy7FaZqdxC4s6zQtZhOfdIA8spww0XtAOyaknAoGAaucPnpKWtUYatwTT/BVr +KIR/h99Yquqeb+eMisOXXtIruckED7aiah2gqCDkwpRQp+zV38OXeqAO40NMA0lh +JEHrNlqoJ6jxmOKUPHXuSYPwwIkgf9aMo5SQG2gIZNPQQ8II7XxI0hqarlV1NogR +OsrEiwNMDU/73B3NgRJGy2Y= +-----END PRIVATE KEY----- diff --git a/tests/TlsTest.php b/tests/TlsTest.php new file mode 100644 index 0000000..2e02668 --- /dev/null +++ b/tests/TlsTest.php @@ -0,0 +1,118 @@ +withServices(new EchoServiceServerRegistry(new TlsEchoServer())) + ->withTransportCredentials( + new Server\TransportCredentials() + ->withDefaultCertificate(new Certificate( + self::path('server.crt'), + self::path('server.key'), + )), + ) + ->build(); + + $server->start(); + + try { + $client = new EchoServiceClient( + new Client\Builder() + ->withTransportCredentials( + new Client\TransportCredentials() + ->withCaCert(self::path('ca.crt')) + ->withPeerName('localhost'), + ) + ->build(), + ); + + $response = $client->echo(new EchoRequest('tls')); + self::assertSame('tls', $response->sentence); + } finally { + $server->stop(); + } + } + + public function testmTLS(): void + { + $caCert = self::path('ca.crt'); + + $server = new Server\Builder() + ->withServices(new EchoServiceServerRegistry(new TlsEchoServer())) + ->withTransportCredentials( + new Server\TransportCredentials() + ->withDefaultCertificate(new Certificate( + self::path('server.crt'), + self::path('server.key'), + )) + ->withCaCert($caCert) + ->withPeerName('client') + ->withVerifyPeer(), + ) + ->build(); + + $server->start(); + + try { + $client = new EchoServiceClient( + new Client\Builder() + ->withTransportCredentials( + new Client\TransportCredentials() + ->withCaCert($caCert) + ->withPeerName('localhost') + ->withCertificate(new Certificate( + self::path('client.crt'), + self::path('client.key'), + )), + ) + ->build(), + ); + + $response = $client->echo(new EchoRequest('mtls')); + self::assertSame('mtls', $response->sentence); + } finally { + $server->stop(); + } + } + + /** + * @param non-empty-string $filename + * @return non-empty-string + */ + private static function path(string $filename): string + { + return __DIR__ . "/Stub/tls/{$filename}"; + } +} + +/** + * @internal + */ +final readonly class TlsEchoServer implements EchoServiceServer +{ + #[\Override] + public function echo(EchoRequest $request, Metadata $md, Cancellation $cancellation): EchoResponse + { + return new EchoResponse($request->sentence); + } +}