ScreenshotNeo

BlogHow-to

How to Use Certificate Pinning with Self-Signed SSL Certificates in Native Apps

Configure narrow trust for self-signed certificates, add safe certificate pinning, and handle Android and Apple platform differences without disabling TLS checks.

By the ScreenshotNeo team4 October 202610 min read

A self-signed certificate is not trusted by a phone’s normal TLS trust store just because the server presents it. Configure the app to trust the intended certificate or private certificate authority (CA), then add pinning if your threat model calls for restricting the accepted key or certificate identity. Keep normal certificate and hostname checks enabled; never fix this by accepting every certificate.

The implementation depends on the platform and networking stack. For Android framework-managed traffic, use a host-scoped Network Security Configuration. For Apple platforms using URLSession, preserve App Transport Security (ATS) and normal trust evaluation, add the intended self-signed certificate as a trust anchor, and make pin checks an additional restriction.

1. Separate trust configuration from pinning

Trust configuration establishes which certificate authority or certificate can anchor a valid chain. Pinning narrows which identity is acceptable, commonly by requiring a specific public key. Neither is a substitute for validating the certificate’s integrity, validity period, hostname, and chain.

Approach What it does Lifecycle tradeoff
Trust a self-signed leaf Allows that specific certificate to act as a trust anchor. A renewed or replaced leaf may require an app update.
Trust a private CA Allows certificates issued by a narrowly scoped private CA to build a trusted chain. Can simplify server certificate renewal, but protect the CA key and keep its scope narrow.
Pin a public key Rejects otherwise trusted chains that do not contain an explicitly allowed key. Key rotation must be coordinated with deployed app versions; keep a backup pin.

For many deployments, a private CA anchor plus a pin set gives a practical balance: the CA handles certificate issuance while pins restrict which server keys the app accepts. The right choice depends on whether you can safely distribute and rotate CA or server keys.

2. Inventory the app’s network stacks and certificates

  1. List every host the app contacts, including API, upload, authentication, and media hosts. Do not apply trust to all domains if only one endpoint needs it.
  2. Identify each transport: Android framework networking, URLSession, WebView, or a third-party HTTP client. Platform configuration may not cover every library or embedded browser.
  3. Choose whether to trust a self-signed leaf or a private CA. Obtain the intended certificate from a trusted deployment channel; do not blindly trust a certificate copied from an unverified connection.
  4. If pinning, compute the SHA-256 hash of the certificate’s SubjectPublicKeyInfo (SPKI), not a hash of the whole certificate file. Prepare a backup key and a rotation plan before releasing the app.
  5. Test the intended host with the intended certificate and test a wrong key or certificate as a negative case. Repeat with a release build.

3. Android: configure Network Security Configuration

For Android networking stacks that honor the platform configuration, place a PEM or DER certificate in the app’s raw resources, for example app/src/main/res/raw/my_ca.pem. Create app/src/main/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="false">api.example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca" />
        </trust-anchors>
        <pin-set expiration="2030-01-01">
            <pin digest="SHA-256">BASE64_CURRENT_SPKI_SHA256</pin>
            <pin digest="SHA-256">BASE64_BACKUP_SPKI_SHA256</pin>
        </pin-set>
    </domain-config>
</network-security-config>

Replace the sample hostname, expiration date, and pin values. Each pin is the Base64-encoded SHA-256 digest of an allowed certificate public key’s SPKI. The chain presented for the host must contain a key matching at least one configured pin. Keep a backup pin for a key you can deploy if the primary key is lost or rotated.

Wire the resource into the application manifest:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... >
    ...
</application>

Android’s Network Security Configuration supports custom anchors for self-signed or privately issued certificates, domain scoping, and separate pin sets. If you trust a self-signed leaf directly, use that certificate as the resource anchor. If you use a private CA, bundle the CA certificate instead. Avoid broad base configuration unless every domain genuinely needs the same trust policy. See the [Android Network Security Configuration documentation](https://developer.android.com/privacy-and-security/security-config) and its [pinning guidance](https://developer.android.google.cn/privacy-and-security/security-config?hl=en).

Android debug and release behavior

You can add test certificates under <debug-overrides> for debuggable builds. Android documents that pinning is not performed for chains using a debug-overrides trust anchor. That means a successful debug connection does not prove production pinning works. Test a release build with both the expected key and a deliberately wrong key. Keep debug-only anchors out of production trust.

Pin expiration is an availability choice as well as a security choice: after expiration, pinning is disabled. Set a date only with an explicit renewal and app-update plan. Android defaults and localhost behavior can vary with target SDK and platform version, so check the documentation for the app’s actual targets rather than assuming one default applies everywhere.

4. Apple platforms: URLSession trust evaluation

URLSession performs server trust evaluation. Keep ATS enabled and require the normal trust checks to pass. Apple documents using a bundled self-signed certificate as an anchor with SecTrustSetAnchorCertificates. If you add pinning, treat it as an additional restriction after successful trust evaluation, not a reason to override a failed evaluation.

The following Swift delegate pattern shows the security order. Add the certificate file to the app bundle and replace the example hostname and pin data. The pin comparison here is intentionally represented by matchesConfiguredSPKI: extracting and hashing SPKI bytes depends on the certificate/key representation and must be implemented and reviewed for the app’s chosen certificate handling. Do not ship a placeholder implementation.

import Foundation
import Security

final class PinnedSessionDelegate: NSObject, URLSessionDelegate {
    private let expectedHost = "api.example.com"
    private let pinnedCertificate: SecCertificate

    init?(certificateURL: URL) {
        guard let data = try? Data(contentsOf: certificateURL),
              let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
            return nil
        }
        pinnedCertificate = certificate
    }

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              challenge.protectionSpace.host == expectedHost,
              let trust = challenge.protectionSpace.serverTrust else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Preserve the platform's TLS server policy and hostname validation.
        guard let policy = SecPolicyCreateSSL(true, expectedHost as CFString) as SecPolicy? else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        SecTrustSetPolicies(trust, policy)

        // Trust only the bundled self-signed certificate as an anchor.
        SecTrustSetAnchorCertificates(trust, [pinnedCertificate] as CFArray)
        SecTrustSetAnchorCertificatesOnly(trust, true)

        var error: CFError?
        guard SecTrustEvaluateWithError(trust, &error) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Optional extra key pinning belongs here. Compare a SHA-256 SPKI
        // digest against a current and backup pin before accepting the trust.
        guard matchesConfiguredSPKI(trust: trust) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        completionHandler(.useCredential, URLCredential(trust: trust))
    }

    private func matchesConfiguredSPKI(trust: SecTrust) -> Bool {
        // Implement SPKI extraction and constant-time comparison using the
        // app's chosen, reviewed certificate handling. Fail closed by default.
        return false
    }
}

Use the delegate with the session that makes the request:

let certificateURL = Bundle.main.url(forResource: "api-self-signed", withExtension: "cer")!
let delegate = PinnedSessionDelegate(certificateURL: certificateURL)!
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
let url = URL(string: "https://api.example.com/v1/status")!
let (data, response) = try await session.data(from: url)

This is an implementation pattern, not a universal drop-in pinning library. The supplied certificate must be the intended trust anchor, host validation must remain enabled, and the pin routine must inspect the evaluated chain and compare the intended key representation. A URLSession delegate does not automatically govern WebViews or third-party networking stacks. Consult Apple’s [network security guidance](https://developer.apple.com/documentation/security/preventing-insecure-network-connections?changes=l_5), [trust configuration API](https://developer.apple.com/documentation/security/configuring-a-trust?changes=_2&language=objc), and [SecTrustEvaluateWithError](https://developer.apple.com/documentation/security/sectrustevaluatewitherror%28_%3A_%3A%29?changes=_5) documentation.

5. Do not accept all certificates

A trust-all callback commonly returns success for any certificate challenge or installs a permissive trust manager. That removes server identity validation and makes an encrypted connection vulnerable to interception. The safe flow is:

  1. Require a TLS server certificate challenge for the expected hostname.
  2. Apply the platform hostname and certificate policy.
  3. Evaluate the chain against the narrowly configured anchor.
  4. If pinning is enabled, require an allowed public key or certificate identity.
  5. Cancel the connection on any error or mismatch.

OWASP cautions that custom pin validation can introduce serious vulnerabilities when implemented incorrectly. Prefer declarative platform controls when the stack supports them, and audit custom code closely. See the [OWASP Pinning Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Pinning_Cheat_Sheet.html).

6. Rotation, rollout, and recovery

  • Keep a backup pin: include a second key whose private key is controlled and can be deployed if the primary changes.
  • Overlap keys during rotation: ship clients that accept old and new keys before moving the server to the new key, where rollout timing permits.
  • Test rollback: verify that the server can return to a key still accepted by deployed clients.
  • Plan for lost app updates: pinning can strand clients that cannot receive a new pin set. Choose an expiration only after weighing the security and availability consequences.
  • Protect signing keys: certificate pinning does not protect a compromised server private key or private CA key.

7. Troubleshooting

Symptom Likely cause Fix
Certificate chain error on Android The certificate is absent, encoded in an unsupported form, or not configured as an anchor for the requested domain. Check the raw resource, manifest reference, hostname spelling, and domain-config scope. Confirm the app is using a stack that honors Network Security Configuration.
Connection works in debug but fails in release A debug-only trust anchor is present, or debug and release builds use different configuration or endpoints. Inspect the merged release manifest and resources. Test the release artifact against the production chain and wrong-key case.
Pin mismatch despite trusting the certificate The configured value is a whole-certificate fingerprint instead of the SHA-256 SPKI digest, or the server presents a different key. Recompute the SPKI digest for the intended key and inspect the actual served chain. Include the planned backup pin.
iOS still reports an untrusted server The anchor was not added to the evaluated trust, the hostname policy is wrong, or validity/chain checks fail. Check the bundled certificate, expected host, server chain, and SecTrust evaluation result. Do not convert the failure to success.
URLSession works but another request path fails A WebView or third-party transport uses a separate trust implementation. Check that stack’s official TLS and pinning behavior; configure it explicitly and test it independently.
All clients fail after key rotation The server switched keys before deployed apps accepted the new pin. Restore an accepted key if possible, then release an app with overlapping pins before retrying rotation.
Localhost behaves differently from production Platform and target SDK defaults differ; newer Android versions have localhost-specific behavior. Use explicit configuration and verify on the exact Android versions and target SDKs in scope.

8. Performance, reliability, and cost

Certificate and pin checks occur during connection setup and are generally small compared with network latency and page or API work; this guide makes no benchmark claim. Reuse HTTP sessions and connections as appropriate for the chosen client. The main operational cost is maintaining certificate issuance, key rotation, and compatible app releases. A pinning outage can be more expensive than the computation itself, so rehearse rotation and rollback.

Pinning does not improve server availability, and it can reduce client availability when keys change unexpectedly. Use a private CA or leaf trust anchor only for hosts that need it, monitor certificate expiry through your normal operations, and maintain a recovery route that does not depend on a rejected key.

9. FAQ

Can I pin a self-signed certificate directly?

Yes. Configure it as a trust anchor, then optionally restrict the accepted identity with a pin. A leaf certificate replacement will require updating clients unless you have planned an overlapping key or CA strategy.

Should I pin the certificate or its public key?

Android’s documented pin configuration uses SHA-256 SPKI hashes. Public-key pins can survive certificate renewal when the same key is retained; they still require careful key rotation. Choose the same identity type consistently across platforms and deployment tooling.

Does pinning replace normal TLS validation?

No. Keep hostname, validity, and chain evaluation. Pinning is an additional restriction on an otherwise acceptable connection.

Does the Android XML protect every HTTP client in my app?

Do not assume so. Verify the specific networking library and any WebView or native transport separately.

Or skip the browser setup

For website screenshots used in debugging, documentation, or visual checks, ScreenshotNeo provides a one-request screenshot API and an MCP server. It is a different tool from certificate pinning: use it when the task is capturing a web page rather than securing your app’s TLS connection. The API accepts a URL and returns an image or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.