How to get iPhone to trust Caddy root crt?

1. The problem I’m having:

Here’s my scenario. I have docker running frankenphp and mysql as two containers on a docker compose network on Mac OS X Sequoia. That runs an API. I have an Xcode iPhone app which I’m deploying to a local device via USB. I’m running dnsmasq on the MacBook Pro in order to allow the iPhone to connect to the FQDN which Caddy is serving. This works but safari complains about the certificate and so does the iPhone app. I installed the Caddy root.crt to the iPhone and it’s listed under Certificate Trust Settings (see attached image)

However, the URL is still not trusted.

2. Error messages and/or full log output:

There’s nothing in the logs because the iPhone app can’t connect. But what Xcode logs provide are

nw_application_id_create_self NECP_CLIENT_ACTION_GET_SIGNED_CLIENT_ID [80: Authentication error]
Failed to resolve host network app I’d

Task <8F1D2FB2-2CE6-4CC6-B47A-1D52D4AD0F1F>.<1> finished with error [-1009] Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline." UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x301498ea0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_NSURLErrorNWPathKey=satisfied (Path is satisfied), interface: en0[802.11], ipv4, ipv6, dns, uses wifi, _kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <8F1D2FB2-2CE6-4CC6-B47A-1D52D4AD0F1F>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <8F1D2FB2-2CE6-4CC6-B47A-1D52D4AD0F1F>.<1>"
), NSLocalizedDescription=The Internet connection appears to be offline., NSErrorFailingURLStringKey=https://app.sportch.localhost/api/v1/getInformationImportant, NSErrorFailingURLKey=https://app.sportch.localhost/api/v1/getInformationImportant, _kCFStreamErrorDomainKey=1}
Connection 2: default TLS Trust evaluation failed(-9807)
Connection 2: TLS Trust encountered error 3:-9807
Connection 2: encountered error(3:-9807)
Task <7A92A31F-E39A-46F9-A06D-6B5A7752777C>.<2> HTTP load failed, 0/0 bytes (error code: -1202 [3:-9807])
Task <7A92A31F-E39A-46F9-A06D-6B5A7752777C>.<2> finished with error [-1202] Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “app.sportch.localhost” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, NSErrorPeerCertificateChainKey=(
    "<cert(0x103868400) s: app.sportch.localhost i: Caddy Local Authority - ECC Intermediate>",
    "<cert(0x103868a00) s: Caddy Local Authority - ECC Intermediate i: Caddy Local Authority - 2024 ECC Root>"
), NSErrorClientCertificateStateKey=0, NSErrorFailingURLKey=https://app.sportch.localhost/api/v1/login, NSErrorFailingURLStringKey=https://app.sportch.localhost/api/v1/login, NSUnderlyingError=0x301496e50 {Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x302b01860>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9807, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9807, kCFStreamPropertySSLPeerCertificates=(
    "<cert(0x103868400) s: app.sportch.localhost i: Caddy Local Authority - ECC Intermediate>",
    "<cert(0x103868a00) s: Caddy Local Authority - ECC Intermediate i: Caddy Local Authority - 2024 ECC Root>"
)}}, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <7A92A31F-E39A-46F9-A06D-6B5A7752777C>.<2>"
), _kCFStreamErrorCodeKey=-9807, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <7A92A31F-E39A-46F9-A06D-6B5A7752777C>.<2>, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x302b01860>, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be “app.sportch.localhost” which could put your confidential information at risk.}

3. Caddy version:

I’m sorry but I don’t know how to do this on frankenphp :frowning:
Is it this?

# /usr/local/bin/frankenphp -v
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

I ran docker-compose up.

a. System environment:

Docker

b. Command:

I have this in my Dockerfile

# Configure supervisord to run both frankenphp and Postfix
RUN mkdir -p /etc/supervisor/conf.d && \
    echo "[supervisord]" > /etc/supervisord.conf && \
    echo "nodaemon=true" >> /etc/supervisord.conf && \
    echo "[program:postfix]" >> /etc/supervisord.conf && \
    echo "command=/usr/sbin/postfix start-fg" >> /etc/supervisord.conf && \
    echo "[program:frankenphp]" >> /etc/supervisord.conf && \
    echo "command=/usr/local/bin/frankenphp run --config /etc/frankenphp/Caddyfile" >> /etc/supervisord.conf

c. Service/unit/compose file:

networks:
  web-network:
services:
  mysql:
    image: mysql/mysql-server:latest-aarch64
    restart: on-failure
    ports:
      - "23306:3306"
    environment:
      MYSQL_ROOT_HOST: "%"
      MYSQL_ROOT_USER: root
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: aasportch10700
      MYSQL_USER: my_user
      MYSQL_PASSWORD: my_password
      MYSQL_SQL_MODE: "NO_ENGINE_SUBSTITUTION"

    volumes:
      - $PWD/db/data:/var/lib/mysql
      - $PWD/db/config:/etc/mysql/conf.d
    networks:
      - web-network
    extra_hosts:
      host.docker.internal: host-gateway


  frankenphp:
    build:
      context: .
      dockerfile: php/caddy/frankenphp/Dockerfile
    ports:
      - "80:80"
      - "443:443"
      - "2019:2019"
    volumes:
      - ./caddy/frankenphp/data:/data
      - ./caddy/frankenphp/config:/config
      - ./caddy/frankenphp/php.ini:/etc/php.d/dev.ini
      - ./caddy/frankenphp/Caddyfile:/etc/frankenphp/Caddyfile
      - ./caddy/frankenphp/logs:/var/logs/caddy
      - ./caddy/certificates:/root/.local/share/caddy/certificates
      - ./caddy/pki:/root/.local/share/caddy/pki
      - ./app:/var/www/html
    networks:
      - web-network
    extra_hosts:
      host.docker.internal: host-gateway

d. My complete Caddy config:

{
    # Enable FrankenPHP
    frankenphp

    # Logging
    log {
        output stderr
        level DEBUG
    }

    debug
}

app.sportch.localhost {
    # Enable compression (optional)
    encode zstd br gzip

    root * /var/www/html/SportchLegacyApp/

    # Security: Hide specific paths
    @hide path /.env *.sql *.template /gtscripts/* *.txt *.md /random /support vapi/ var/ /support /system.* /lib
    respond @hide 404

    # Execute PHP files
    php_server

    # Rewrite subfolder requests for screenscrapers
    rewrite /ladder/* /ladder/index.php
    rewrite /manager/* /manager/index.php

    # Serve media files
    handle /media/* {
        root * /var/www/html
        file_server
    }
}

5. Links to relevant resources:

Could it be related to this?

I notice that the certificate issued is valid for 10 years, which is probably too long for Apple to trust it if that article is correct.

I tried adding lifetime setting:

app.sportch.localhost {
    tls internal {
        lifetime 69120000
    }

But that caused frankenphp to fail with errors (whatever I put inside those curly braces causes errors):

frankenphp-1  | 2024-11-12 21:05:01,168 INFO spawned: 'frankenphp' with pid 7
frankenphp-1  | 2024-11-12 21:05:01,170 INFO spawned: 'postfix' with pid 8
frankenphp-1  | 2024-11-12 21:05:01,223 WARN exited: frankenphp (exit status 1; not expected)
frankenphp-1  | 2024-11-12 21:05:02,340 INFO spawned: 'frankenphp' with pid 93
frankenphp-1  | 2024-11-12 21:05:02,340 INFO success: postfix entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
frankenphp-1  | 2024-11-12 21:05:02,389 WARN exited: frankenphp (exit status 1; not expected)
frankenphp-1  | 2024-11-12 21:05:04,401 INFO spawned: 'frankenphp' with pid 107
frankenphp-1  | 2024-11-12 21:05:04,454 WARN exited: frankenphp (exit status 1; not expected)
frankenphp-1  | 2024-11-12 21:05:07,466 INFO spawned: 'frankenphp' with pid 121
frankenphp-1  | 2024-11-12 21:05:07,516 WARN exited: frankenphp (exit status 1; not expected)
frankenphp-1  | 2024-11-12 21:05:08,518 INFO gave up: frankenphp entered FATAL state, too many start retries too quickly

According to ChatGPT, caddy is not serving the root cert? Here’s some more logs:

% openssl s_client -connect app.sportch.localhost:443 -showcerts

Connecting to 192.168.68.102
CONNECTED(00000005)
depth=1 CN=Caddy Local Authority - ECC Intermediate
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0
verify return:1
---
Certificate chain
 0 s:
   i:CN=Caddy Local Authority - ECC Intermediate
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Nov 12 18:07:41 2024 GMT; NotAfter: Nov 13 06:07:41 2024 GMT
-----BEGIN CERTIFICATE-----
REDACTED
-----END CERTIFICATE-----
 1 s:CN=Caddy Local Authority - ECC Intermediate
   i:CN=Caddy Local Authority - 2024 ECC Root
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Nov 12 18:07:40 2024 GMT; NotAfter: Nov 19 18:07:40 2024 GMT
-----BEGIN CERTIFICATE-----
REDACTED
-----END CERTIFICATE-----
---
Server certificate
subject=
issuer=CN=Caddy Local Authority - ECC Intermediate
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1281 bytes and written 396 bytes
Verification error: unable to get local issuer certificate
---
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
Protocol: TLSv1.3
Server public key is 256 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 20 (unable to get local issuer certificate)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_128_GCM_SHA256
    Session-ID: 50E3F14965E235B2CC1050BC2E5B084C7F23AF2FE8783148685F010A8D6A72C6
    Session-ID-ctx:
    Resumption PSK: 3D1A483D9AF7D820AE2B20DC2EFA9E7216933903ADB83A740DFDC2150BB7FEB0
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 604800 (seconds)
    TLS session ticket:
    0000 - 6f 97 a5 b0 42 fa c6 a0-f0 e6 f1 e7 fa 1b 73 b0   o...B.........s.
    0010 - 07 5c e5 41 29 7d 17 1b-5e 32 2f fa c8 0d b2 22   .\.A)}..^2/...."
    0020 - 8f f9 40 c3 22 01 2c ec-2d 2c df 70 97 eb 71 ff   ..@.".,.-,.p..q.
    0030 - 25 b8 85 fc f6 22 b7 ad-07 b5 92 59 62 19 09 0b   %....".....Yb...
    0040 - 2b 91 fd d9 01 46 11 5b-12 a5 e2 cc 4c 93 68 f1   +....F.[....L.h.
    0050 - da af 55 a8 e1 92 cb 15-8c 35 33 7a 34 31 2a e4   ..U......53z41*.
    0060 - 31 bd 81 7d 31 4b 61 e5-41                        1..}1Ka.A

    Start Time: 1731447816
    Timeout   : 7200 (sec)
    Verify return code: 20 (unable to get local issuer certificate)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK

Eh, 10 years is a common lifetime for root certs. I think that article is referring to leaf certs issued for the site.

Is that a correct copy+paste? :face_with_monocle: I’m not sure I undrestand this error.

Are you sure you installed the correct root from the right server instance?

Correct, serving roots is a waste of bandwidth, as serving it won’t do any good if it’s not already trusted. (And if it is, serving it is not needed.)

1 Like

Failed to resolve host network app ID (sorry my Mac rewrites ID as I’d… drives me nuts).

It’s an error from Xcode when it’s monitoring the iPhone app.

I am sure that I’ve copied the correct certificate yes. You can see the map to the certificates folder in the docker compose yml. And that’s where I took it from. I installed the root cert into the Mac’s keychain (from where it apparently cannot be deleted – I tried) and I can trust the root on Safari but if I click the lock icon to see certificates, Safari says that the Intermediate Cert is not trusted.

When I try this from the Mac: openssl s_client -connect app.sportch.localhost:443 it also complains: Verify return code: 20 (unable to get local issuer certificate)

On the iPhone, as shown in the earlier image, I’ve installed that root cert and trusted it, but the iPhone still claims as you can see from the error:

Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “app.sportch.localhost” which could put your confidential information at risk."

That screenshot shows that the intermediate certificate expire(s/d) on Nov. 13 at 6:07 GMT. That is in the past, was it at the time of the screenshot?

Otherwise, I’m not sure then why the cert isn’t trusted. Does any other platform (root store) have the same problem if you install the same root into it? I’m not really well versed with Mac PKI tooling.

1 Like

Well, no I posted it yesterday, while it was still valid. I haven’t had other issues with non-self assigned root certificates – Mac OS X’s keychain works very well generally speaking.

I have also had success with self-assigned certificates I’ve generated myself in the past.

What do you mean by “other platform”? Do you mean something not-Apple? I don’t know as I haven’t tried. I do have an Android device I could have a go on, although I’m not currently in Android mode. It may have to wait.

I updated to the latest iOS version… and that did the trick. The certificate just started working.

1 Like

Yeah, that was one thing I was thinking of for troubleshooting.

Interesting! Well, thanks for posting your solution!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.