Various ways to secure network traffic for mobile apps

👍
· Dec 23, 2024 · 6 mins read
Various ways to secure network traffic for mobile apps

In today’s mobile-first world, securing communication between the mobile app and backend servers is no longer optional — it’s essential.

While HTTPS protects data in transit, it doesn’t protect against:

  • Fake apps using API
  • Man-in-the-middle attacks with rogue certificates
  • Replay or tampering of signed requests
  • Leaked tokens from compromised apps

This post will explore various other ways to secure mobile app-to-server communication using modern, layered techniques. We’ll take iOS app as an example but the principle applies to Android apps as well.

Enforce HTTPS with App Transport Security (ATS)

What it is: iOS blocks non-HTTPS traffic by default.

<key>NSAppTransportSecurity</key>  
<dict>  
  <key>NSAllowsArbitraryLoads</key>  
  <false/>  
</dict>

Why it matters: Prevents unencrypted communication — the baseline security.

Use when: Always. Every app should enforce HTTPS.

Certificate Pinning

Configure using the NSPinnedDomains key in the app’s Info.plist . More information can be found from Apple.

NSPinnedDomains : Dictionary {  
    <domain-name-string> : Dictionary {  
        NSIncludesSubdomains : Boolean  
        NSPinnedCAIdentities : Array  
        NSPinnedLeafIdentities : Array  
    }  
}

b) Code-based (fallback)

Implement the pinning logic in the code like below.

func urlSession(\_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,   
                   completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {  
          
        // Ensure it's a server trust challenge  
        let protectionSpace = challenge.protectionSpace  
        guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,  
            protectionSpace.host.contains("your.domainname.com"),  
            let serverTrust = protectionSpace.serverTrust else {  
            completionHandler(.performDefaultHandling, nil)  
            return  
        }  
          
        // Get the server's certificate  
        if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {  
            let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data  
              
            // Compare with our pinned certificate,   
            // pinnedCertificateData can be stored in bundle or get from other secure way  
            if serverCertificateData == pinnedCertificateData {  
                let credential = URLCredential(trust: serverTrust)  
                completionHandler(.useCredential, credential)  
                return  
            }  
        }  
          
        // Certificate doesn't match  
        completionHandler(.cancelAuthenticationChallenge, nil)  
    }

Why it matters: Prevents MITM attacks using rogue certificates
Use when: Need to ensure server authenticity in critical data flows.

Client Certificate Authentication (mTLS)

What it is: Mutual TLS allows the server to authenticate the client app using a certificate.

The challenge of this approach is how to distribute the client certificate to valid mobile client apps, which should be done in a secure channel via push or a secure onboarding process etc.

 func urlSession(\_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,  
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {  
        // Handle server trust (verify server's certificate)  
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {  
            if let serverTrust = challenge.protectionSpace.serverTrust {  
                let isTrusted = SecTrustEvaluateWithError(serverTrust, nil)  
                if isTrusted {  
                    completionHandler(.useCredential, URLCredential(trust: serverTrust))  
                } else {  
                    print("Server trust evaluation failed")  
                    completionHandler(.cancelAuthenticationChallenge, nil)  
                }  
            } else {  
                completionHandler(.cancelAuthenticationChallenge, nil)  
            }  
        }  
        // Handle client certificate authentication  
        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {  
            // Create a credential with the client identity  
            let credential = retrieveClientCertificate()  
            completionHandler(.useCredential, credential)  
        } else {  
            // Fallback to default handling for other challenges  
            completionHandler(.performDefaultHandling, nil)  
        }  
    }

Why it matters: Provides strong client identity at the transport layer.
Use when: Need secure app-level identity (e.g., enterprise or internal apps).

Request Signing

What it is: A technique where each API request is cryptographically signed by the client, ensuring the request hasn’t been altered and originates from a trusted source. Digitally sign each request to ensure it hasn’t been tampered with. This operates at the application layer to allow the server and client to verify each other. The common algorithms for the signing is:

  • HMAC-SHA256: Uses a shared secret for fast, symmetric signing.
  • RSA/ECDSA: Uses a private key for asymmetric signing, allowing public verification.

The challenge of this approach is the Key Management: Securely distributing and rotating shared secrets or public-private keys is critical.

let message = "POST\\n/api/order\\n\\(timestamp)\\n\\(body)"  
let key = SymmetricKey(data: Data("shared-secret".utf8))  
let hmac = HMAC<SHA256>.authenticationCode(for: Data(message.utf8), using: key)  
let signature = Data(hmac).base64EncodedString()

Why it matters: Ensures integrity and authenticity of requests.
Use when: Verifying the source and payload of sensitive API requests.

Key Agreement

What it is: a cryptographic process where two parties independently derive a shared secret key over an insecure channel. A common algorithm is Elliptic Curve Diffie-Hellman (ECDH).

import CryptoKit  
import Foundation  
  
func performECDHKeyAgreement(clientPrivateKey: P256.KeyAgreement.PrivateKey,   
                            serverPublicKey: P256.KeyAgreement.PublicKey) throws -> SymmetricKey {  
    // Perform ECDH to compute the shared secret  
    let sharedSecret = try clientPrivateKey.sharedSecretFromKeyAgreement(with: serverPublicKey)  
      
    // Derive a symmetric key using HKDF  
    let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(  
        using: SHA256.self,  
        salt: Data(),  
        sharedInfo: Data(),  
        outputByteCount: 32 // 256-bit key for AES-256 or HMAC  
    )  
      
    return symmetricKey  
}

Why it matters: Creates session-specific symmetric keys without transmission.
Use when: Need to encrypt payloads or manage secure sessions dynamically.

Payload Encryption (AES-GCM)

What it is: Encrypts sensitive JSON or binary data at the application level using symmetric cryptography such as AES in GCM mode. This protects data from being visible to intermediate systems or logs, even if HTTPS is in use.

let jsonData = try JSONEncoder().encode(data)  
let sealedBox = try AES.GCM.seal(jsonData, using: symmetricKey)  
let encryptedPayload = sealedBox.combined!

Why it matters: Protects content even beyond TLS.
Use when: Handling personally identifiable or sensitive data.

App Attest (iOS 14+)

What it is: A security service provided by Apple that allows the server to confirm that incoming API requests were made by a valid, unmodified instance of the app. It works by securely generating cryptographic keys on device and proving app authenticity.

let keyId = try await AppAttestService.shared.generateKey()  
let attestation = try await AppAttestService.shared.attestKey(keyId, clientDataHash: challengeHash)

Why it matters: Blocks requests from fake, modified, or jailbroken apps.
Use when: Need to verify app integrity before issuing sensitive credentials.

Replay Protection

What it is: Prevents reuse of signed requests with nonces, timestamps, or one-time tokens.

{  
  "timestamp": 1712403100,  
  "nonce": "A1B2C3D4",  
  "signature": "..."  
}

Why it matters: Prevents replay attacks via captured requests.
Use when: Performing one-time actions like payments or token exchange.

Secure Remote Password (SRP)

What it is: SRP (Secure Remote Password) is a password-authenticated key exchange protocol. It allows a client and server to authenticate each other using a password without ever transmitting the password, even over HTTPS. The result is a mutually derived session key.

There are some open sourced Swift libraries on GitHub like https://github.com/adam-fowler/swift-srp and https://github.com/flockoffiles/SwiftySRP. And it’s included in OpenSSL too.

Why it matters: SRP protects credentials from being intercepted or replayed, even if TLS is compromised. It enables zero-knowledge password verification and secure session bootstrapping without symmetric secrets.

Use when: You want to enable secure login based on user credentials without transmitting or storing raw passwords.

Conclusion

Securing mobile app-to-server communication is not about a single tool or tactic — it’s about layered resilience. Each mechanism mentioned here addresses a different threat vector: transport security, request integrity, identity validation, or tamper prevention. Use them that align with the app’s sensitivity, user base, and regulatory demands. Above all, treat mobile security as an evolving discipline. Start with strong defaults, and iterate toward defense in depth.

Comments

Sharing is caring!