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

NIOSSL: How to include a client certificate in the gRPC request context to server ? #1872

Open
tarac99 opened this issue Apr 29, 2024 · 8 comments
Labels

Comments

@tarac99
Copy link

tarac99 commented Apr 29, 2024

What are you trying to achieve?

I want to include a client certificate in the gRPC context in the request.
My server(in Golang) looks for this certificate along with some custom metadata:

// AuthenticateFromContext extracts the caller's identity from the gRPC request context.
func AuthenticateFromContext(ctx context.Context) *Identity {
	cert := mTLSFromContext(ctx)
	if cert == nil {
		return nil
	}

	return &Identity{
		certificate: cert,
	}
}

Note: This works fine with a Linux Golang based client gRPC connection.

What have you tried so far?

I included the client certificate in the TLSConfiguration but the context in the server end doesn't have any certificate:

let certificateChain = NIOSSLCertificateSource.certificate(clientCert)
let trustRoots = NIOSSLTrustRoots.certificates(trustCA) // Root CA for server
let tlsConfig = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(certificateChain: [certificateChain], trustRoots: trustRoots, certificateVerification: .fullVerification)
let channel = try GRPCChannelPool.with(
                target: .host(server, port: port),
                transportSecurity: .tls(tlsConfig),
                eventLoopGroup: eventLoopGroup
            )
self.grpcClient = MyActivationServiceNIOClient(channel: channel)

What am I missing ?

@glbrntt
Copy link
Collaborator

glbrntt commented Apr 30, 2024

The first thing that comes to mind is whether the go client is setting any metadata that the server is reading. Can you check what metadata the server receives when queried by the go client?

@tarac99
Copy link
Author

tarac99 commented Apr 30, 2024

I added a JWT to the header and that server is able to see.

let headers: HPACKHeaders = ["x-id-token": jwt]
let callOptions = CallOptions(customMetadata: headers, timeLimit: .timeout(.seconds(20)))
...
self.grpcClient = MyActivationServiceNIOClient(channel: channel, defaultCallOptions: callOptions)

When I printed the Identity object in server side, I see :
&{[email protected] https://afoo.okta.com/oauth2/asulk9kg0WgF61JKS5d6 map[some-access-group1:{} some-access-group2:{} some-access-group3:{} some-access-group4:{}] <nil> []}
The nil at the end is the certificate type. With Go client I get the cert pointer there and it is not nil.
Does that mean I have to send the x509 client cert as a custom metadata / HPACKHeaders ? Not sure how to do that ?

@glbrntt
Copy link
Collaborator

glbrntt commented May 1, 2024

With Go client I get the cert pointer there and it is not nil. Does that mean I have to send the x509 client cert as a custom metadata / HPACKHeaders ? Not sure how to do that ?

I'm not sure to be honest, I don't know how the Go server decides whether extract the certificate. If you can figure that bit out then hopefully we can understand what the Swift client isn't doing.

@tarac99
Copy link
Author

tarac99 commented May 1, 2024

@glbrntt First of all thanks for looking into it. I looked into the code bit more to see how the Go server looks for certs in the request context.

import (
        "context"
        "google.golang.org/grpc/credentials"
	"google.golang.org/grpc/peer"
)

func mTLSFromContext(ctx context.Context) *Certificate {
	p, ok := peer.FromContext(ctx)
	if !ok {
		return nil
	}

	tlsInfo, _ := p.AuthInfo.(credentials.TLSInfo)
	certs := tlsInfo.State.PeerCertificates
	if len(certs) == 0 {
		return nil
	}

	return parseCertificate(certs[0])
}

Basically it uses https://pkg.go.dev/google.golang.org/grpc/peer#FromContext to extract the Peer object which contains the creds. Each cert is of *x509.Certificate type. So one thing is clear - the server is not looking the certificate from the request's metadata (which makes sense as it is for some light weight headers). Is there anything we can do from Swift client to add to this request context ? or is this model incompatible with grpc-swift client currently ?

@Lukasa
Copy link
Collaborator

Lukasa commented May 1, 2024

The code in OP looks wrong. Specifically, this call:

let tlsConfig = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(certificateChain: [certificateChain], trustRoots: trustRoots, certificateVerification: .fullVerification)

You aren't providing a private key here. This cannot successfully present a cert to the server, so presumably you aren't.

@tarac99
Copy link
Author

tarac99 commented May 1, 2024

@Lukasa Maybe thats the issue. The reason I dont have the private key set is that the client certificate is hardware-backed i.e the private key is in Secure Enclave and not available on disk. I can see in Mac logs, the CryptoTokenKit extension running on the Mac provides the private key ref for signing when the cert is accessed (for example mTLS in browser). I was hoping it can still send the certificate(with the public key) from the Keychain since it can find it with SecItemCopyMatching().

@tarac99
Copy link
Author

tarac99 commented May 1, 2024

It looks like we can’t convert a SecKey (private key ref from Secure Enclave) to a NIOSSLPrivateKey. SecKey represents a key ref in a secure context where the actual key material is not meant to be exported or exposed, providing added security while NIOSSLPrivateKey requires key material in a format like PEM or DER.

What do you suggest ? In future will hardware-backed client certs be supported ?

@Lukasa
Copy link
Collaborator

Lukasa commented May 2, 2024

You can do this using NIOSSLCustomPrivateKey.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants