While working on certificate pinning for our mobile apps, we've come across this issue making http calls. We see that there are differences on what certificates get returned for each mobile platform. The names of the CNs in the certificate chain are the same, but the encoded SHA256 certificates are different. Hoping to shed some light as to why.
We wrote some sample code to analyze the certificates that were getting returned to our mobile platforms. We were expecting the 256 fingerprints to be the same, but we are instead only getting one or two common values.
Note: These are different from what we see in the browsers as well (chrome, safari).
Here is the result of some sample code written to return the peer certificate chain (in SHA256 base64 encoded format) returned from the various platforms:
Android:
- 2xC1bw6iB/pA4QmHVuJXllJCgjNTJxtEFISGMNlpLlg=: CN=www.google.com
- zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=: CN=GTS CA 1C3,O=Google Trust Services LLC,C=US
- hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=: CN=GTS Root R1,O=Google Trust Services LLC,C=US
iOS:
- HX8NrTiisCa9yA43DdmJT+iuiEakuZ9VcKDGKaSCD7E= CN=www.google.com
- zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w= CN=GTS CA 1C3
- EumkTYs+nSg5q/mGi38Fjyg/I7lBU59PhayJy7/fx5k= CN=GTS Root R1
Android sample code to return the peer certificate chain :
void analyzeCerts()
{
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BASIC); // Change this to enable logging.
OkHttpClient.Builder httpClient = new OkHttpClient.Builder()
.connectTimeout(DateTimeConstants.SECONDS_PER_MINUTE, TimeUnit.SECONDS)
.readTimeout(DateTimeConstants.SECONDS_PER_MINUTE, TimeUnit.SECONDS);
httpClient.addInterceptor(logging);
String url = "https://www.google.com";
String pinUrl = "www.google.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(pinUrl, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient.Builder clientWithPinner = httpClient.certificatePinner(certificatePinner);
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(clientWithPinner.build())
.build();
SimpleInterface si = retrofit.create(SimpleInterface.class);
Call<ResponseBody> call = si.getPublicConsentsByFlow("abcd", "efg");
call.enqueue(new Callback<ResponseBody>()
{
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
{
Log.i(CERT_APP, "In onResponse" + response.toString());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t)
{
Log.i(CERT_APP, "In onFailure " + t);
}
});
}
iOS sample code
import Foundation
import CryptoKit
class ServiceRequest: NSObject, URLSessionDelegate {
private let rsa2048Asn1Header: [UInt8] = [
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
]
enum ServiceRequestMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
var session: URLSession!
override init() {
super.init()
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
self.certPinTestRequest()
}
func certPinTestRequest(){
let request = NSMutableURLRequest(url: URL(string: "https://google.com")!)
request.httpMethod = ServiceRequestMethod.get.rawValue
request.timeoutInterval = 20.0
session.dataTask(with: request as URLRequest) { (data, response, error) -> Void in
}.resume()
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 {
for index in 0..<SecTrustGetCertificateCount(trust) {
// Get the public key data for the certificate at the current index of the loop.
if let certificate = SecTrustGetCertificateAtIndex(trust, index),
let publicKey = SecCertificateCopyPublicKey(certificate),
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil){
let keyHash = hash(data: (publicKeyData as NSData) as Data)
let certSummary = SecCertificateCopySubjectSummary(certificate) ?? NSString(string: "nil")
print("sha256/\(keyHash) CN=\(certSummary)")
}
}
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
private func hash(data: Data) -> String {
var keyWithHeader = Data(rsa2048Asn1Header)
keyWithHeader.append(data)
if #available(iOS 13.0, *) {
return Data(SHA256.hash(data: keyWithHeader)).base64EncodedString()
} else {
// Fallback on earlier versions
}
return ""
}
}