Sample client/server application which does the following:
-
Suppose there are two CAs that issue client certificates (
ca1
,ca2
). -
These two CAs also issue server a certificate each with different SNIs (
tee.collaborator1.com
,tee.collaborator2.com
) -
The server will load the two server certificates from these two CAs.
-
The server will enforce mTLS where the client certificates are issued by the corresponding CAs.
-
The client will load the client certificates and server CA certificate for one of the two issuers and connect to the server while specifying an appropriate
SNI
value -
During TLS negotiation, the server will return the server cert that maps to
SNI
sent by the client -
During TLS negotiation, the server will return the set of client certificate CA's it accepts.
-
Client will verify the server's cert CA issuer from the loaded CA in step 5.
-
Client will negotiate mTLS using the client certs in step 5.
-
Server will verify the client certificate using the server certs from step 3.
-
Server will optionally invoke an external OCSP request to verify live that the client certificate presented has not been revoked
also see
Run Server
go run server/server.go
Try to make a connection to the server without specifying any certificates:
The response will show a 'default' server certificate tee.operator.com
and the list of CA's it accepts.
The default certificate was shown because we did not specify an SNI value (eg -servername tee.collaborator1.com
)
$ openssl s_client --connect localhost:8081
---
Certificate chain
0 s:C = US, O = Google, OU = Enterprise, CN = tee.operator.com <<<<<<<<
i:C = US, O = Operator, OU = Enterprise, CN = Enterprise Root CA
---
Server certificate
subject=C = US, O = Google, OU = Enterprise, CN = tee.operator.com
issuer=C = US, O = Operator, OU = Enterprise, CN = Enterprise Root CA
---
Acceptable client certificate CA names
C = US, O = Collaborator 1, OU = Enterprise, CN = Collaborator 1 Root CA
C = US, O = Collaborator 2, OU = Enterprise, CN = Collaborator 2 Root CA
Now run the clients:
## collaborator 1
go run client/client.go --serverName tee.collaborator1.com --serverCertRootCA ca1/root-ca.crt --clientCert ca1/client.crt --clientKey ca1/client.key
## collaborator 2
go run client/client.go --serverName tee.collaborator2.com --serverCertRootCA ca2/root-ca.crt --clientCert ca2/client.crt --clientKey ca2/client.key
What you should see on the server config is a pair of connections from the clients:
$ go run server/server.go
Subject CN=client.collaborator1.com,OU=Enterprise,O=Google,L=US
Client Certificate hash KK8-CThe_W9i0FWTX7L-DMbvsuCvv5K10m6fWJO--MA
Subject CN=client.collaborator2.com,OU=Enterprise,O=Google,C=US
Client Certificate hash Pkapc_CwEq12asjxrJ5mdn5_1MV2NNDosQ44hxbcwG0
The sever also generates and prints the client certificate hash for use with Token Binding
$ echo `openssl x509 -in ca1/client.crt -outform DER | openssl dgst -sha256 | cut -d" " -f2` | xxd -r -p - | openssl enc -a | tr -d '=' | tr '/+' '_-'
KK8-CThe_W9i0FWTX7L-DMbvsuCvv5K10m6fWJO--MA
also see
- Certificate Bound Tokens using Security Token Exchange Server (STS)
- Envoy WASM and LUA filters for Certificate Bound Tokens
Both the client and server will print the common Exported Key Material rfc5705 from the established ConnectionState.ExportKeyingMaterial. This can be used for binding.
The net result is you should get the same per-conn data on both ends of the connection
$ go run server/server.go
Starting Server..
EKM my_nonce: 1efe1f9d73fcaefb59d10a898592759c6a1bae792f8fbfce80ca7db72202e06b <<<<<<<<<<<<<<<
$ go run client/client.go --serverName tee.collaborator1.com --serverCertRootCA ca1/root-ca.crt --clientCert ca1/client.crt --clientKey ca1/client.key
EKM my_nonce: 1efe1f9d73fcaefb59d10a898592759c6a1bae792f8fbfce80ca7db72202e06b <<<<<<<<<<<<<<<
If you want to see the decrypted tls traces using wireshark, enable the SSLKEYLOGFILE
writer within the TLSConfig
sudo tcpdump -s0 -iany -w trace.cap port 8081
export SSLKEYLOGFILE=/tmp/keylog.log
go run server/server.go
run client
cd tcpdump/
wireshark trace.cap --log-level=debug -otls.keylog_file:keylog.log
- Client->Server SNI
- Server->Client Client CA issuers
- Server->Client HTTP Decrypted
also see
- OpenSSL 3.0.0 docker with TLS trace enabled (enable-ssl-trace) and FIPS (enable-fips)
- Using Wireshark to decrypt TLS gRPC Client-Server protobuf messages
The server can also make an OCSP API call to verify the client' certificates revocation status at runtime.
To use this, you need to run an ocsp server which in the following is trough openssl
cd ca1/ca_scratchpad
## start OCSP Server
openssl ocsp -index ca/root-ca/db/root-ca.db -port 9999 -rsigner ca/root-ca.crt -rkey ca/root-ca/private/root-ca.key -CA ca/root-ca.crt -text -ndays 10
## in new windows, test ocsp server with revoked cert
openssl ocsp -CA ca/root-ca.crt -CAfile ca/root-ca.crt -issuer ca/root-ca.crt -cert certs/revoked.crt -url http://localhost:9999 -resp_text
### note Cert Status: revoked
### test ocsp server with valid cert
openssl ocsp -CA ca/root-ca.crt -CAfile ca/root-ca.crt -issuer ca/root-ca.crt -cert certs/tee.crt -url http://localhost:9999 -resp_text
### note Cert Status: good
now run the server with ocsp enabled
## run server
$ go run server/server.go --check_ocsp
## run client
$ go run client/client.go --serverName tee.collaborator1.com --serverCertRootCA ca1/root-ca.crt --clientCert ca1/client.crt --clientKey ca1/client.key
The ocsp server will show the request, validation and response
$ openssl ocsp -index ca/root-ca/db/root-ca.db -port 9999 -rsigner ca/root-ca.crt -rkey ca/root-ca/private/root-ca.key -CA ca/root-ca.crt -text -ndays 10
ACCEPT 0.0.0.0:9999 PID=187589
ocsp: waiting for OCSP client connections...
ocsp: Received request, 1st line: POST / HTTP/1.1
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 10CA8300F670BDF813C03C0CD3DACE5EA8AAB355
Issuer Key Hash: 750D12CCDB33ED58068CADED0E9E2F00E96FC165
Serial Number: 01
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: C = US, O = Collaborator 1, OU = Enterprise, CN = Collaborator 1 Root CA
Produced At: Apr 17 12:20:54 2023 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 10CA8300F670BDF813C03C0CD3DACE5EA8AAB355
Issuer Key Hash: 750D12CCDB33ED58068CADED0E9E2F00E96FC165
Serial Number: 01
Cert Status: good <<<<<<<<<<<<<<<<<<
This Update: Apr 17 12:20:54 2023 GMT
Next Update: Apr 27 12:20:54 2023 GMT
Notice that the initial TLS negotiation sends back BOTH the client cert CA's it accepts:
Acceptable client certificate CA names
C = US, O = Collaborator 1, OU = Enterprise, CN = Collaborator 1 Root CA
C = US, O = Collaborator 2, OU = Enterprise, CN = Collaborator 2 Root CA
if you would rather only send back the Client Cert CA that is appropriate for the SNI requested, then some more modifications are required for the TLSConfig
:
tlsConfig := &tls.Config{
GetConfigForClient: func(ci *tls.ClientHelloInfo) (*tls.Config, error) {
if ci.ServerName == "tee.collaborator1.com" {
return &tls.Config{
MinVersion: tls.VersionTLS13,
ClientAuth: tls.RequireAndVerifyClientCert,
// set client1 root pool
ClientCAs: client1_root_pool,
GetCertificate: func(ci *tls.ClientHelloInfo) (*tls.Certificate, error) {
// set server1_ cert
return &server1_cert, nil
},
}, nil
}
if ci.ServerName == "tee.collaborator2.com" {
return &tls.Config{
MinVersion: tls.VersionTLS13,
ClientAuth: tls.RequireAndVerifyClientCert,
// set client1 root pool
ClientCAs: client2_root_pool,
GetCertificate: func(ci *tls.ClientHelloInfo) (*tls.Certificate, error) {
// set server2 cert
return &server2_cert, nil
},
}, nil
}
return nil, fmt.Errorf("SNI not recognized %s", ci.ServerName)
},
}