Implementing HTTPS Two-Way Authentication in Android Using Delphi XE10.x
This guide shows how to bundle .pfx client certs into app's AndroidKeyStore for mTLS to self-signed servers.
Join the DZone community and get the full member experience.
Join For FreeI have an HTTPS cloud server. After a mobile app sends a request, it receives the content returned by the server. The server stores a self-made CA and a server certificate.
const https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync("./myserver.key"),
cert: fs.readFileSync('./myserver.crt'),
ca: fs.readFileSync('./MyCARoot.crt'),
requestCert: true,
rejectUnauthorized:true
};
This configuration requires the client certificate to be installed on the mobile device.

However, I don't want all users to install client certificates, as doing so would expose their private keys. Is it possible to package the *.pfx certificate and then load it via code? Yes, it is possible. Here are the steps:
1. Locate the System .Net.HttpClient.Android.pas file under \Embarcadero\Studio\21.0\source\rtl\netCopy it to your project folder and then rename it.
2. Move all class definitions under 'implementation' to 'interface'.
TAndroidHTTPRequest = class;
TAliasCallback = class(TJavaLocal, JKeyChainAliasCallback)
protected
[Weak] FRequest: TAndroidHTTPRequest;
public
procedure alias(alias: JString); cdecl;
constructor Create(const ARequest: TAndroidHTTPRequest);
end;
TJHostnameVerifier = class(TJavaLocal, JHostnameVerifier)
public
function verify(hostname: JString; session: JSSLSession): Boolean; cdecl;
end;
3. Locate TAndroidHTTPClient = class(THTTPClient) and add two private members.
private
FMyTrustManagerFactory : JTrustManagerFactory;
FMyKeyManagerFactory: JKeyManagerFactory;
Add two more procedures.
procedure TAndroidHTTPClient.SetTrustManagerFactory(const ATmf: JTrustManagerFactory);
begin
FMyTrustManagerFactory := ATmf;
end;
procedure TAndroidHTTPClient.setKeyManagerFactory(const AKMF: JKeyManagerFactory);
begin
FMyKeyManagerFactory := AKMF;
end;
Similarly, find TAndroidHTTPRequest = class(THTTPRequest) and add these two private members.
4. Locate procedure TAndroidHTTPRequest.DoPrepare, modify it, and replace the original processing method.
// TrustManager
//LJTrustManagers := FMyTrustManagerFactory.getTrustManagers;
LJOldTrustManager := TJX509TrustManager.Wrap(FMyTrustManagerFactory.getTrustManagers[0]); // Get Current Trust Manager.
FJTrustManager := TX509TrustManager.Create(LJOldTrustManager, Self);
LJTrustManagers := TJavaObjectArray<JTrustManager>.Create(1);
LJTrustManagers.Items[0] := TJTrustManager.Wrap(FJTrustManager);
LJCerts := FJTrustManager.getAcceptedIssuers;
FJTrustManager.checkClientTrusted(LJCerts, StringToJString('RSA'));
FJTrustManager.checkServerTrusted(LJCerts, StringToJString('RSA'));
// KeyManager
LJOldKeyManager := TJX509KeyManager.Wrap(FMyKeyManagerFactory.getKeyManagers[0]); // Get Current Key Manager.
FJKeyManager := TX509KeyManager.Create(LJOldKeyManager, Self);
LJKeyManagers := TJavaObjectArray<JKeyManager>.Create(1);
LJKeyManagers.Items[0] := TJKeyManager.Wrap(FJKeyManager);
5. Locate function TAndroidHTTPClient.DoGetHTTPRequestInstance and pass the two previously defined private members to Request.
function TAndroidHTTPClient.DoGetHTTPRequestInstance(const AClient: THTTPClient; const ARequestMethod: string;
const AURI: TURI): IHTTPRequest;
begin
Result := TAndroidHTTPRequest.Create(TAndroidHTTPClient(AClient), ARequestMethod, AURI);
//Pass two factory instances to the created HttpRequest
(Result as TAndroidHTTPRequest).setTrustManagerFactory(FMyTrustManagerFactory);
(Result as TAndroidHTTPRequest).setKeyManagerFactory(FMyKeyManagerFactory);
end;
6. Find procedure TX509TrustManager.checkServerTrusted, and modify:
// Checking if it's an authoritative CA will fail for self-made certificates.
//FJOrigOldTrustManager.checkServerTrusted(chain, authType);
if not isServerTrusted(FRequest.FServerCertificate) then
raise ECertificateException.Create('Invalid server certificate!');
Comment out the native checkServerTrusted(chain, authType); function and use the custom function isServerTrusted instead.
The replacement function is quite simple; it checks the serial numbers of the CA certificate and the server certificate.
function TX509TrustManager.isServerTrusted(const ADCert: TCertificate):boolean;
begin
//only check SN
if (UpperCase(ADCert.SerialNum) = '1AFE100A09D8E894') or
(UpperCase(ADCert.SerialNum) = '40F2768CE4B83190') then
Result := True
else
Result := False;
end;
7. Let's see how these two factory instances are initialized.
fname := System.IOUtils.TPath.GetDocumentsPath + PathDelim + 'myclient.pfx';
F := TFileStream.Create(fname, fmOpenRead);
try
R := X509Cert.LoadFromStreamPFX(F, 'XF@dM1n');
if R = 0 then
begin
ms := TMemoryStream.Create;
if X509Cert.PrivateKeyExists then
begin
X509Cert.SaveKeyToStreamPEM(ms, 'password');
fname := System.IOUtils.TPath.GetSharedDocumentsPath + PathDelim + 'mykey.pem';
ms.SaveToFile(fname);
KeyManager.ImportFromFile(fname, 3, 'RSA', '', '', 2);
vKey := KeyManager.Key.Key;
vSize := Length(vKey);
end;
end
else
raise ECertificateException.Create('Failed to load certificate, PFX error ' + IntToHex(R, 4));
finally
F.Free;
ms.Free;
end;
SetLength(vBytes, X509Cert.CertificateSize);
Move(X509Cert.CertificateBinary^, vBytes[0], X509Cert.CertificateSize);
LJArray := TJavaArray<Byte>.Create(Length(vBytes));
Move(vBytes[0], LJArray.Data^, Length(vBytes));
LJStream := TJByteArrayInputStream.JavaClass.init(LJArray);
LJArray.Free;
LJClientCert := TJCertificateFactory.JavaClass.getInstance(StringToJString('X.509')).generateCertificate(LJStream);
LJCertChain := TJavaObjectArray<JCertificate>.create(1);
LJCertChain.Items[0] := LJClientCert;
LJArray := TJavaArray<Byte>.Create(vSize);
move(vKey[0], LJArray.Data^, vSize);
LJkeySpec := TJPKCS8EncodedKeySpec.JavaClass.Init(LJArray);
LJKeyFactory := TJKeyFactory.JavaClass.getInstance(StringToJString('RSA'));
LJKey := TJRSAPrivateKey.Wrap(LJkeyFactory.generatePrivate(TJKeySpec.Wrap(LJkeySpec)));
// Instantiate keystore
LJAlgorithm := TJKeyManagerFactory.JavaClass.getDefaultAlgorithm;
s := JStringToString(LJAlgorithm);
kmf := TJKeyManagerFactory.JavaClass.getInstance(LJAlgorithm);
// Obtain secret store
key_store_type := TJKeyStore.JavaClass.getDefaultType;
s := JStringToString(key_store_type);
LJKS_PK := TJKeyStore.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
//This is the only way; the source code shows that it does not accept non-empty parameters.
LJKS_PK.load(nil, nil);
LJKS_PK.setKeyEntry(StringToJString('mykey'), TJKey.Wrap(LJKey), nil, LJCertChain);
kmf.init(LJKS_PK, StringToJString('XF@dM1n').toCharArray);
//Certificate Management Factory
LJKS_Cert := TJKeyStore.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
LJKS_Cert.load(nil, nil);
//key_Store.setCertificateEntry(StringToJString('ca'), ca);
LJKS_Cert.setCertificateEntry(StringToJString('LJClientCert'), LJClientCert);
LJAlgorithm := TJTrustManagerFactory.JavaClass.getDefaultAlgorithm;
s := JStringToString(LJAlgorithm); //#BKS
tmf := TJTrustManagerFactory.JavaClass.getInstance(LJAlgorithm);
tmf.init(LJKS_Cert);
//set custom two factorys
FClient.SetTrustManagerFactory(tmf);
FClient.setKeyManagerFactory(kmf);
This uses two commercial controls from SecureBlackBox: KeyManager: TsbxCryptoKeyManager; and X509Cert: TElX509Certificate;. This cross-platform control is expensive, but very convenient.
You can also use the free Bouncy Castle, but that requires extensive modifications to the exported JNI, which is time-consuming and involves exporting some JNI code not provided by BigD.

Opinions expressed by DZone contributors are their own.
Comments