Certificate Pinning in XData (and Sparkle)

Dear Wagner,

shortly, i need a way to implement certificate pinning in my XData client and Server applications.
For a better understanding i made a copy paste from my ChatGPT discussion about this:

We need stronger protection against MITM attacks in XData client apps, especially on untrusted networks (public Wi-Fi, proxies, compromised devices). Today, XData (via Sparkle) performs standard TLS chain validation, but there isn’t an official, simple, cross-platform way to enable certificate pinning (e.g., SPKI/sha256 or certificate thumbprint).

Primary Goal
Eliminate the need to install or manage any certificates on end-user machines. The application should ship with built-in pins and validate the server at runtime—no per-device certificate deployment, no OS-store changes, and no custom CA installation.

What we’re requesting
Native certificate pinning support in TXDataClient (and ideally SparkleHttpClient) that validates the server using configurable pins before accepting the TLS connection.

Acceptance criteria

  1. Supported pin types
  • SPKI (Subject Public Key Info) hash (sha256) — recommended.
  • Certificate thumbprint (sha1/sha256) for compatibility.
  1. Multiple pins + backup pins
    • Allow a list of hashes; connection succeeds if any match.
  2. Operating modes
  • Strict (fail connection on mismatch).
  • ReportOnly (log/telemetry without blocking; useful for rollout).
  1. Clear, easy API
  • Configurable at TXDataClient and SparkleHttpClient levels.
  • Event for pinning failures (for logging, telemetry, retry/fallback).
  1. Cross-platform
  • Windows (SChannel/OpenSSL), macOS/iOS, Android/Linux (OpenSSL).
  1. Backwards compatibility
  • Pinning off by default so existing projects remain unaffected.
  1. Documentation
  • Short guide on calculating SPKI hashes, planning key rotations, and best practices.

Proposed API (Delphi — XData client)

uses XData.Client, Sparkle.Http.Engine, Sparkle.Security;

var
Client: TXDataClient;
begin
Client := TXDataClient.Create;
// Set base URL, etc.

// 1) Pinning – SPKI sha256, multiple pins (active + backup)
Client.Security.Pins.Add(
TCertificatePin.SPkiSha256('u2FsdGVkX1+...base64...')
);
Client.Security.Pins.Add(
TCertificatePin.SPkiSha256('Q29tcGFuaW9u...base64...')
);

// 2) Optional: certificate thumbprint as a fallback
Client.Security.Pins.Add(
TCertificatePin.CertSha256('AB CD EF ...')
);

// 3) Mode
Client.Security.PinningMode := TPinningMode.Strict; // or ReportOnly

// 4) Failure handler
Client.Security.OnPinningFailure :=
procedure(const Info: TPinningFailureInfo; var Allow: Boolean)
begin
// log/telemetry; Allow stays False in Strict
end;

// ...XData calls
end;
Proposed API (SparkleHttpClient — optional / recommended)

delphi
Copy code
var
Http: ISparkleHttpClient;
begin
Http := TSparkleHttpClient.Create;

Http.Security.Pins := [
TCertificatePin.SPkiSha256('...'),
TCertificatePin.CertSha256('...')
];
Http.Security.PinningMode := TPinningMode.Strict;
Http.Security.OnPinningFailure := ...
end;
Security considerations

  • Prefer SPKI sha256 pinning (instead of full certificate hash) to allow certificate renewals using the same key.
  • Support multiple pins (active + backup) to enable seamless rollout/rotation without downtime.
  • Avoid “TOFU” (Trust On First Use) persistence by default; if offered, keep it as an advanced/opt-in feature.
  • Provide clear logging on failure reasons (chain OK but pin mismatch, etc.).

Benefits

  • No certificates to install on client machines—apps carry the pins and validate at runtime.
  • Stronger defense against MITM beyond standard CA validation.
  • Simple, correct, cross-platform implementation for XData/Sparkle users—no custom per-platform code.

Impact / Compatibility

  • Off by default ⇒ zero breaking changes.
  • A dedicated Security/Pins API keeps current TLS validation behavior intact.

Nice-to-have

  • A build-time utility or documented snippet to compute SPKI sha256 from .cer/PEM files.
  • Examples for certificate/key rotation (active + backup pins) and platform-specific notes.

Best regards,
Radu Capota

Also from ChatGPT:

For Android:

  1. Get SHA-256 public-key hashes
echo | openssl s_client -servername api.example.com -connect api.example.com:443 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | base64

Save current + backup hash for each host.

  1. Create res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.example.com</domain>
    <pin-set expiration="2026-12-31">
      <pin digest="SHA-256">CURRENT_BASE64_HASH==</pin>
      <pin digest="SHA-256">BACKUP_BASE64_HASH==</pin>
    </pin-set>
  </domain-config>

  <!-- repeat domain-config for other hosts -->
</network-security-config>
  1. Reference it in AndroidManifest.xml
<application android:networkSecurityConfig="@xml/network_security_config" ...>
  1. Build & sign APK (you know how).
    Install on device.
  2. Test
  • Positive: app → real backend → must succeed.
  • Negative: intercept proxy / changed DNS with different cert → must fail.
  1. Maintenance
  • Before rotating backend certs: add new hash to XML (keep old hash), release update.
  • After clients update, remove old hash in a later release.

Same for iOS:

iOS certificate pinning without source-code edits

1. Use an App Transport Security (ATS) configuration

ATS is defined in the app’s Info.plist.
You can pin certificates (or public keys) by embedding the required hashes in the ATS domain exceptions.

Example snippet to insert or update in Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSPinnedDomains</key>
  <dict>
    <key>api.example.com</key>
    <dict>
      <key>NSIncludeSubdomains</key>
      <true/>
      <key>NSPinnedLeafIdentities</key>
      <array>
        <data>
        MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuZQh...==
        </data>
      </array>
      <key>NSPinnedCAIdentities</key>
      <array/>
    </dict>
  </dict>
</dict>
  • The <data> entry is the Base64 DER encoding of the certificate (not the SHA-256 hash).
    You can extract it with:
openssl x509 -in cert.pem -outform der | base64
  • You can pin multiple certificates by adding multiple <data> entries.
  • Add separate <key> blocks for each domain you want to pin.

2. Rebuild the app

After editing Info.plist, rebuild and sign normally — no Swift/Obj-C code change required.

3. Test

  • Requests to the pinned domains with the correct certificate → succeed.
  • Requests with a different or MITM cert → fail (ATS will block the connection).

4. Maintain

When certificates rotate:

  • Add the new certificate DER data as an additional <data> entry before rotation.
  • After rollout and rotation, remove the old one.