17
votes

New to gRPC and couldn't really find any example on how to enable SSL on the server side. I generated a key pair using openssl but it complains that the private key is invalid.

D0608 16:18:31.390303 Grpc.Core.Internal.UnmanagedLibrary Attempting to load native library "...\grpc_csharp_ext.dll"
D0608 16:18:31.424331 Grpc.Core.Internal.NativeExtension gRPC native library loaded successfully.
E0608 16:18:43.307324 0 ..\src\core\lib\tsi\ssl_transport_security.c:644: Invalid private key.
E0608 16:18:43.307824 0 ..\src\core\lib\security\security_connector.c:821: Handshaker factory creation failed with TSI_INVALID_ARGUMENT.
E0608 16:18:43.307824 0 ..\src\core\ext\transport\chttp2\server\secure\server_secure_chttp2.c:188: Unable to create secure server with credentials of type Ssl.

Here's my code

var keypair = new KeyCertificatePair(
            File.ReadAllText(@"root-ca.pem"),
            File.ReadAllText(@"ssl-private.key"));
SslServerCredentials creds = new SslServerCredentials(new List<KeyCertificatePair>() {keypair});
Server server = new Server
{
    Services = { GrpcTest.BindService(new GrpcTestImpl()) },
    Ports = { new ServerPort("127.0.0.1", Port, creds) }
};
3

3 Answers

62
votes

Here's what I did.

Using OpenSSL, generate certificates with the following:

@echo off
set OPENSSL_CONF=c:\OpenSSL-Win64\bin\openssl.cfg   

echo Generate CA key:
openssl genrsa -passout pass:1111 -des3 -out ca.key 4096

echo Generate CA certificate:
openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=MyRootCA"

echo Generate server key:
openssl genrsa -passout pass:1111 -des3 -out server.key 4096

echo Generate server signing request:
openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=%COMPUTERNAME%"

echo Self-sign server certificate:
openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

echo Remove passphrase from server key:
openssl rsa -passin pass:1111 -in server.key -out server.key

echo Generate client key
openssl genrsa -passout pass:1111 -des3 -out client.key 4096

echo Generate client signing request:
openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=%CLIENT-COMPUTERNAME%"

echo Self-sign client certificate:
openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

echo Remove passphrase from client key:
openssl rsa -passin pass:1111 -in client.key -out client.key

Change password 1111 to anything you like

Server:

var cacert = File.ReadAllText(@"ca.crt");
var servercert = File.ReadAllText(@"server.crt");
var serverkey = File.ReadAllText(@"server.key");
var keypair = new KeyCertificatePair(servercert, serverkey);
var sslCredentials = new SslServerCredentials(new List<KeyCertificatePair>() { keypair }, cacert, false);

var server = new Server
{
    Services = { GrpcTest.BindService(new GrpcTestImpl(writeToDisk)) },
    Ports = { new ServerPort("0.0.0.0", 555, sslCredentials) }
};
server.Start();

Client:

var cacert = File.ReadAllText(@"ca.crt");
var clientcert = File.ReadAllText(@"client.crt");
var clientkey = File.ReadAllText(@"client.key");
var ssl = new SslCredentials(cacert, new KeyCertificatePair(clientcert, clientkey));
channel = new Channel("localhost", 555, ssl);
client = new GrpcTest.GrpcTestClient(channel);

If "localhost" doesn't work, use the host name instead.

4
votes

If usage of Certificate Authority (CA) and Certificate Signing Request (CSR) is too sophisticated for your task, you can use self-signed certificates.

Let say, there is 1 server and 2 (or more) clients.

Execute at client1:

openssl req -x509 -newkey rsa:4096 -nodes -keyout client.key -out client.crt -days 3650 -subj '/CN=client1' # generate client1 cert and key
sudo bash -c 'echo "192.168.1.101 my.server" >> /etc/hosts' # create domain for server - if necessary only
scp client.crt [email protected]:/path/to/certs/client1.crt # copy public cert client1 to server machine

Execute at client2:

openssl req -x509 -newkey rsa:4096 -nodes -keyout client.key -out client.crt -days 3650 -subj '/CN=client2' # generate client2 cert and key
sudo bash -c 'echo "192.168.1.101 my.server" >> /etc/hosts' # create domain for server- if necessary only
scp client.crt [email protected]:/path/to/certs/client2.crt # copy public cert client2 to server machine

Execute at server:

openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 3650 -subj '/CN=my.server' # generate server cert and key
scp server.crt client1-user@client1-addr:/path/to/certs # copy public cert server to client1 machine
scp server.crt client2-user@client2-addr:/path/to/certs # copy public cert server to client2 machine
cat client1.crt client2.crt > client.crt # combine client certs into the single file

Server code:

var clientCert = File.ReadAllText(Path.Combine(certPath, "client.crt"));
var serverCert = File.ReadAllText(Path.Combine(certPath, "server.crt"));
var serverKey = File.ReadAllText(Path.Combine(certPath, "server.key"));
var keyPair = new KeyCertificatePair(serverCert, serverKey);
var credentials = new SslServerCredentials(new List<KeyCertificatePair> { keyPair }, clientCert, true);

var server = new Server
{
    Services = { MyService.BindService(new MyAdminService()) },
    Ports = { new ServerPort("0.0.0.0", 54321, credentials) }
};

Client code:

var serverCert = File.ReadAllText(Path.Combine(_certPath, "server.crt"));
var clientCert = File.ReadAllText(Path.Combine(_certPath, "client.crt"));
var clientKey = File.ReadAllText(Path.Combine(_certPath, "client.key"));
var credentials = new SslCredentials(serverCert, new KeyCertificatePair(clientCert, clientKey));

var channel = new Channel("my.server:54321", credentials);    
var client = new MyService.MyServiceClient(channel);

IMPORTANT!

To use TLS certificates, use a domain name when generate server certificate.

Client certificates can use any unique string.

Domain name should contain at least 1 dot (.), e.g. my.server or my.server.customzone

If use top-level domain like my-server, it causes a long waiting to resolve it (for me it always about 76 seconds).

Pros: - no need to generate CSR, pass it to machine with CA, sign it there and copy back to originating machine

Cons: - adding new client requires adding certificate to server

2
votes

If you have tried what @qmo has suggested and still not working and you're getting the same error saying "StatusCode=Unavailable, Detail="DNS resolution failed" I fixed it by adding a new record in my host file (located in C:\Windows\System32\drivers\etc for Windows).

127.0.0.1 DESKTOP-QNCI7UN

Where DESKTOP-QNCI7UN is the name of my machine. Then in the client I'm using:

channel = new Channel("DESKTOP-QNCI7UN", 50000, ssl);

By using "locahost" it was not working. So by using the machine name in the client + added record in the host file fixed the issue.