grpc / grpc-swift

The Swift language implementation of gRPC.
Apache License 2.0
2.01k stars 413 forks source link

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

Open tarac99 opened 4 months ago

tarac99 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 : &{jdoe@foo.io 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 commented 4 months ago

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 commented 4 months ago

@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 commented 4 months ago

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 commented 4 months ago

@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 commented 4 months ago

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 commented 4 months ago

You can do this using NIOSSLCustomPrivateKey.