QUIC certificate question

I'm working on two Swift applications which are using QUIC in Network.framework for communication, one serve as the listener (server) and the other serve as the client so that they can exchange data, both the server and the client app are running under the same LAN, the problem I met is that when client try to connect to the server, the connection will fail due to boring SSL, couple questions:

  1. Since both the server app and client app are running under the same LAN, do they need TLS certificate?
  2. If it does, will self-signed certificate P12 work? I might distribute the app in App Store or in signed/notarized dmg or pkg to our users.
  3. If I need a public certificate and self signed wouldn't work, since they are just pair of apps w/o fixed dns domain etc, Is there any public certificate only for standalone application, not for the fixed web domain?
Answered by DTS Engineer in 827619022
Written by stang2021 in 775724021
1. Since both the server app and client app are running under the same LAN, do they need TLS certificate?

Yes. While it’s theoretically possible to implement QUIC directly over TCP, Apple’s QUIC implementation always runs over TLS. Moreover, this is TLS 1.3, which doesn’t support TLS-PSK. You have to use TLS in its standard PKI mode, which means digital identities and certificates.

Written by stang2021 in 775724021
2. If it does, will self-signed certificate P12 work?

Yes, but you’ll have to override TLS server trust evaluation on the client in order to trust the server’s certificate.

Having said that, embedded a single digital identity in your app is hardly ideal security-wise. It’d be relatively easy for someone to extract that and then impersonate your app. There are better options available to you.

If you’d like to explore these better options, I’m happy to do that later. However, for the moment, I recommend that you get things up and limping with a fixed digital identity.

Written by stang2021 in 775724021
3. Is there any public certificate only for standalone application, not for the fixed web domain?

No.


Regarding the code in your second post, you’re definitely on the right track. Your server-side code looks fine. On the client side, I recommend that you do this:

// I normally avoid using the global concurrent queue but in this
// case, for the reasons I outline in “Avoid Dispatch Global
// Concurrent Queues”.
//
// <https://developer.apple.com/forums/thread/711736>
//
// In this case I can’t use `.main` because the main thread is
// running my UI.  And it seems a bit silly to write a bunch of
// extract code to bounce back to the queue on which I’m running all
// the networking because I’m not actually doing anything on this
// queue other than call the completion handler.

sec_protocol_options_set_verify_block(self, { _, _, completionHandler in
    completionHandler(true)
}, .global())

That just disables server trust evaluation completely. Once you get that working, you can add code to do trust evaluation properly.

Once you get to that stage, make sure you invoke sec_trust_copy_ref. This lets you convert a sec_trust_t, in the second argument to your verify block, to a SecTrust object, which is what you need for the rest of your code.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I loaded the self-signed certificate in the server and applied the server validation at the client side

client:
func createQUICClientParameters() -> NWParameters {
            let params = NWParameters.quic(alpn: ["srdp"])
            
            guard let certUrl = Bundle.main.url(forResource: "sersrdp", withExtension: "der"),
                  let certData = try? Data(contentsOf: certUrl),
                  let certificate = SecCertificateCreateWithData(nil, certData as CFData) else {
                fatalError("Unable to load server.crt from bundle")
            }
            
            // Allow self-signed certificates for testing
            let tlsOptions = NWProtocolTLS.Options()
            
            sec_protocol_options_set_verify_block(
                tlsOptions.securityProtocolOptions,
                { (metadata, sec_trust, completion) in
                    // We'll compare the server's leaf certificate with our pinned certificate.

                    var trustRef: SecTrust?
                    let status = SecTrustCreateWithCertificates(sec_trust, nil, &trustRef)
                    guard status == errSecSuccess, let trust = trustRef else {
                        completion(false)
                        return
                    }

                    // Retrieve the leaf certificate (server's certificate at index 0)
                    guard let certificateChain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
                          let serverCertificate = certificateChain.first else {
                        completion(false)
                        return
                    }

                    // Compare it to our pinned certificate
                    if CFEqual(serverCertificate, certificate) {
                        // Certificates match, trust the connection
                        completion(true)
                    } else {
                        // Mismatch -> fail the connection
                        completion(false)
                    }
                },
                DispatchQueue.global()
            )
            
            
            params.defaultProtocolStack.applicationProtocols.insert(tlsOptions, at: 0)
            return params
        }
Server:
func loadIdentity() -> SecIdentity? {
            if let p12URL = Bundle.main.url(forResource: "sersrdp", withExtension: "p12"),
               let p12Data = try? Data(contentsOf: p12URL) {
                let password = "NotEvenClose"
                let options: [String: Any] = [
                    kSecImportExportPassphrase as String: password
                ]

                var items: CFArray?
                let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &items)
                guard status == errSecSuccess,
                      let itemsArray = items as? [[String: Any]],
                      let identityRef = itemsArray.first?[kSecImportItemIdentity as String] else {
                    fatalError("Failed to import identity from p12: \(status)")
                }
                
                guard CFGetTypeID(identityRef as CFTypeRef) == SecIdentityGetTypeID() else {
                    fatalError("The imported object is not a SecIdentity.")
                }

                // 2. Now that we've confirmed the type, force-cast is safe:
                let identity = identityRef as! SecIdentity
                return identity
            }
            return nil
        }
      ....
      let tlsOptions = NWProtocolTLS.Options()
            
            guard let identity = loadIdentity() else {
                fatalError("Could not load TLS identity!")
            }
            
            if let secIdentity = sec_identity_create(identity) {
                sec_protocol_options_set_local_identity(tlsOptions.securityProtocolOptions, secIdentity)
            } else {
                fatalError(" Failed to create sec_identity_t from SecIdentity")
            }
            
            params.defaultProtocolStack.applicationProtocols.insert(tlsOptions, at: 0)

when the client tries to connect, still hit error:

03-03 16:00:13.591 QuicServer.swift:L99 Accepted new connection on port 8780 from 192.168.68.53:63785

boringssl_context_handle_fatal_alert(2170) [C2:1][0x124605e40] write alert, level: fatal, description: internal error

boringssl_context_error_print(2160) [C2:1][0x124605e40] Error: 4905268928:error:100000ae:SSL

routines:OPENSSL_internal:NO_CERTIFICATE_SET:/AppleInternal/Library/BuildRoots/cf117d38-cf63-11ef-b315-aabfac210453/Library/Caches/com.apple.xbs/Sources/boringssl/ssl/tls13_server.cc:286:

Accepted Answer
Written by stang2021 in 775724021
1. Since both the server app and client app are running under the same LAN, do they need TLS certificate?

Yes. While it’s theoretically possible to implement QUIC directly over TCP, Apple’s QUIC implementation always runs over TLS. Moreover, this is TLS 1.3, which doesn’t support TLS-PSK. You have to use TLS in its standard PKI mode, which means digital identities and certificates.

Written by stang2021 in 775724021
2. If it does, will self-signed certificate P12 work?

Yes, but you’ll have to override TLS server trust evaluation on the client in order to trust the server’s certificate.

Having said that, embedded a single digital identity in your app is hardly ideal security-wise. It’d be relatively easy for someone to extract that and then impersonate your app. There are better options available to you.

If you’d like to explore these better options, I’m happy to do that later. However, for the moment, I recommend that you get things up and limping with a fixed digital identity.

Written by stang2021 in 775724021
3. Is there any public certificate only for standalone application, not for the fixed web domain?

No.


Regarding the code in your second post, you’re definitely on the right track. Your server-side code looks fine. On the client side, I recommend that you do this:

// I normally avoid using the global concurrent queue but in this
// case, for the reasons I outline in “Avoid Dispatch Global
// Concurrent Queues”.
//
// <https://developer.apple.com/forums/thread/711736>
//
// In this case I can’t use `.main` because the main thread is
// running my UI.  And it seems a bit silly to write a bunch of
// extract code to bounce back to the queue on which I’m running all
// the networking because I’m not actually doing anything on this
// queue other than call the completion handler.

sec_protocol_options_set_verify_block(self, { _, _, completionHandler in
    completionHandler(true)
}, .global())

That just disables server trust evaluation completely. Once you get that working, you can add code to do trust evaluation properly.

Once you get to that stage, make sure you invoke sec_trust_copy_ref. This lets you convert a sec_trust_t, in the second argument to your verify block, to a SecTrust object, which is what you need for the rest of your code.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn for the answer, the server started the listener w/o problem and the self-signed p12 was loaded correctly, in the client side, I updated to give green light to everything from the server side according to what you suggested:

let params = NWParameters.quic(alpn: ["srdp"])
let tlsOptions = NWProtocolTLS.Options()
            sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completionHandler in
                completionHandler(true)
            }, .global())
            /*
            sec_protocol_options_set_verify_block(
            ...
            */

                  params.defaultProtocolStack.applicationProtocols.insert(tlsOptions, at: 0)

When the client tries to connect the server, before the server sends back anything, the server hit the failed state with dns error, I suspect if the client side greenlight update could help the problem @ the server side:

override func handleConnection(_ connection: NWConnection) {
        connection.stateUpdateHandler = { state in
            switch state {
            case .ready:
                logMsg("Server: Connection is ready on port \(self.getPort())")
                self.receivePacket(connection)
            case .failed(let error):
                logMsg("Server: Connection failed on port \(self.getPort()) with error: \(error)") // error: (Network.NWError) dns
            case .cancelled:
                logMsg("Server: Connection cancelled on port \(self.getPort())")
            default:
                break
            }
        }

        connection.start(queue: DispatchQueue.global())
    }

I suspect if it is the problem in the way I created the self-signed certificate, I created it via

$ openssl genrsa -out sersrdp.key 2048
$ openssl req -new -key sersrdp.key -out sersrdp.csr -subj "/CN=localhost"
$ openssl x509 -req -days 365 -in sersrdp.csr -signkey sersrdp.key -out sersrdp.crt
$ openssl pkcs12 -export -inkey sersrdp.key -in sersrdp.crt -out sersrdp.p12 -name "sersrdp"

Is there any problem of above? I also tried 4096 and it didn't work either.

Great appreciate your help .

Written by stang2021 in 827663022
I suspect if it is the problem in the way I created the self-signed certificate

That seems unlikely, given that you’ve completely disabled server trust evaluation on the client.

If you’re concerned about a TLS issue, one easy way to eliminate that from the equation is to use the same credentials for a TLS-over-TCP server. If that works, you know your credentials are good.

Is the client connecting with a .hostPort(…) endpoint? Or with a .service(…) endpoint? If it’s the latter, try testing with the former. That removes Bonjour from the equation.

You set an ALPN, which also complicates things. Try removing that.

Written by stang2021 in 827663022
the server hit the failed state with dns error

A DNS error? Weird.

What error exactly?

ps While it’s almost certain not the cause of your curent issue, this:

connection.start(queue: DispatchQueue.global())

is a really bad idea. I recommend against using the global concurrent queues in general — see Avoid Dispatch Global Concurrent Queues — but it’s particularly problematic when you do that with Network framework. In fact, I was recently discussing this with another developer on this thread. So, switch to a serial queue. And for bring up efforts like this, I generally just use the main queue (-:

That is, btw, the reason why the snippet I posted earlier had a big comment explaining why I decided to use a global concurrent queue in that specific case.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you Quinn, I tried remove alpn out(leave as empty []), the QUIC server listener will fail to start, and I asked ChatGPT, DeepSeek, they said we cannot remove alpn(Not sure if they were right). About the DNS error I mentioned, actually it is the same error as before, but the failed(let error)'s error says DNS error (the same NO_CERTIFICATE_SET in the dump)

3-06 08:22:05.461  QuicServer.swift:L172 Server is ready and listening on port 8780
03-06 08:23:15.894  QuicServer.swift:L181 Accepted new connection on port 8780 from 192.168.68.53:52471
boringssl_context_handle_fatal_alert(2170) [C2:1][0x151e086e0] write alert, level: fatal, description: internal error
boringssl_context_error_print(2160) [C2:1][0x151e086e0] Error: 5668644992:error:100000ae:SSL routines:OPENSSL_internal:NO_CERTIFICATE_SET:/AppleInternal/Library/BuildRoots/cf117d38-cf63-11ef-b315-aabfac210453/Library/Caches/com.apple.xbs/Sources/boringssl/ssl/tls13_server.cc:286:
boringssl_session_handshake_incomplete(241) [C2:1][0x151e086e0] SSL library error
boringssl_session_handshake_error_print(44) [C2:1][0x151e086e0] Error: 5668644992:error:100000ae:SSL routines:OPENSSL_internal:NO_CERTIFICATE_SET:/AppleInternal/Library/BuildRoots/cf117d38-cf63-11ef-b315-aabfac210453/Library/Caches/com.apple.xbs/Sources/boringssl/ssl/tls13_server.cc:286:
nw_endpoint_flow_failed_with_error [C2 192.168.68.53:52471 in_progress socket-flow (satisfied (Path is satisfied), viable, interface: en0, scoped, ipv4, dns)] already failing, returning

I dig into it yesterday, come to my conclusions:

  • I'm sure the client is working if I try to connect to other QUIC server not with macOS (by enabling HTTP3 of my nginx server)
  • The server self-signed certificate (both p12 and key + csr) shall be correct.
  • The problem is here:
    var securityProtocolOptions: sec_protocol_options_t { get }
    
    NWProtocolQUIC.Options.securityProtocolOptions is readonly, even the TLS options loaded the certificate successfully, we cannot set to QUIC options, I suspect network.framework doesn't support QUIC server yet.
  • Is there any working QUIC server sample code written with network.framework?

Not sure if I made progress or not, I updated the parameters like this:

let options = NWProtocolQUIC.Options(alpn: ["srdp"])
options.direction = .bidirectional
            
if let identity = createIdentity() {
       logMsg("identity is \(isValidIdentity(identity) ? "valid" : "invalid")")
       setLocalIdentityBridge(options.securityProtocolOptions, identity)
 }
 let securityProtocolOptions: sec_protocol_options_t = options.securityProtocolOptions
            sec_protocol_options_set_verify_block(securityProtocolOptions,
                                                  { (_: sec_protocol_metadata_t,
                                                     _: sec_trust_t,
                                                     complete: @escaping sec_protocol_verify_complete_t) in
                complete(true)
            }, dataQueue)
            
 return NWParameters(quic: options)

Right now when the client connecting, the server won't hit .failed with blaming of missing certificate, instead it hits .preparing and then the server app crashed at SecIdentityCopyCertificate, but identify was validated after loaded from P12.

On the ALPN front, yeah, that’s right, it is required. I forgot that I had set up a default ALPN in my test project.

Make sure you apply the same ALPN at both ends.

Written by stang2021 in 827997022
I suspect network.framework doesn't support QUIC server yet.

That’s not right. Network framework does support QUIC server. The issue is that you’re not setting up the security options correctly.

Written by stang2021 in 828024022
Not sure if I made progress or not

And now you’re on the right track!

This is confusing, and it definitely threw me for a loop when I first encountered it:

  • With TCP, TLS is optional so you have to configure it as an application protocol.

  • With QUIC, TLS is mandatory, so you set it via the QUIC options.

There are multiple ways you can do this with the Network framework API [1], but here’s an example that shows the two approaches in parallel:

func quicParameters() -> NWParameters {
    let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"])
    let sec = quic.securityProtocolOptions
    // … configure `sec` here …
    return NWParameters(quic: quic)
}

func tlsOverTCPParameters() -> NWParameters {
    let tcp = NWProtocolTCP.Options()
    let tls = NWProtocolTLS.Options()
    let sec = tls.securityProtocolOptions
    // … configure `sec` here …
    return NWParameters(tls: tls, tcp: tcp)
}

As to why your current code is crashing, I’m not sure what’s going on there but I suspect it’s due to the way that you’ve set up the local identity. Here’s how I might do that:

func configureTLS(sec: sec_protocol_options_t, identity: SecIdentity, disableServerTrustEvaluation: Bool) {
    let secIdentity = sec_identity_create(identity)!
    sec_protocol_options_set_local_identity(sec, secIdentity)
    if disableServerTrustEvaluation {
        sec_protocol_options_set_verify_block(sec, { _, _, completionHandler in
            completionHandler(true)
        }, .global())
    }
}

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Indeed, I had to write this example from scratch because the test project I’m cribbing from uses a completely different approach (-:

QUIC certificate question
 
 
Q