我有一个https云服务器,在手机端的app发出请求后,获得服务端返回的内容,服务端存放了自制ca以及sever证书。
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
};
这样的配置,就需要手机端安装client证书
但我并不想让使用者都来安装client证书,这样就把私钥泄漏出去了,能不能把*.pfx证书打包,然后以代码去加载证书呢?可以的,下面是实现的步骤:
1、在\Embarcadero\Studio\21.0\source\rtl\net下找到System.Net.HttpClient.Android.pas文件 ,复制一份到你的工程文件夹下,然后重命名。
2、把implementation下的所有类定义,移动到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、找到TAndroidHTTPClient = class(THTTPClient),增加两个私有成员
private
FMyTrustManagerFactory : JTrustManagerFactory;
FMyKeyManagerFactory: JKeyManagerFactory;
再添加两个procedure
procedure TAndroidHTTPClient.SetTrustManagerFactory(const ATmf: JTrustManagerFactory);
begin
FMyTrustManagerFactory := ATmf;
end;
procedure TAndroidHTTPClient.setKeyManagerFactory(const AKMF: JKeyManagerFactory);
begin
FMyKeyManagerFactory := AKMF;
end;
同样的,找到TAndroidHTTPRequest = class(THTTPRequest),增加这两个私有成员
4、找到procedure TAndroidHTTPRequest.DoPrepare,做修改,替换原来的处理方式
// 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、找到function TAndroidHTTPClient.DoGetHTTPRequestInstance,把两个前面定义的私有成员传递给Request
function TAndroidHTTPClient.DoGetHTTPRequestInstance(const AClient: THTTPClient; const ARequestMethod: string;
const AURI: TURI): IHTTPRequest;
begin
Result := TAndroidHTTPRequest.Create(TAndroidHTTPClient(AClient), ARequestMethod, AURI);
//把两个工厂实例传递给创建的HttpRequest
(Result as TAndroidHTTPRequest).setTrustManagerFactory(FMyTrustManagerFactory);
(Result as TAndroidHTTPRequest).setKeyManagerFactory(FMyKeyManagerFactory);
end;
6、找到procedure TX509TrustManager.checkServerTrusted,做修改
// 检查是否是权威CA,对于自制证书,无法通过
//FJOrigOldTrustManager.checkServerTrusted(chain, authType);
if not isServerTrusted(FRequest.FServerCertificate) then
raise ECertificateException.Create('无效服务端证书!');
把原生的checkServerTrusted(chain, authType);注释掉,以自定义的函数isServerTrusted
替换,这个函数很简单,判断CA证书和Server证书的序列号
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、下面看看这两个工厂实例是如何初始化的
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)));
// 實例化密鑰庫
LJAlgorithm := TJKeyManagerFactory.JavaClass.getDefaultAlgorithm;
s := JStringToString(LJAlgorithm);
kmf := TJKeyManagerFactory.JavaClass.getInstance(LJAlgorithm);
// 獲得密鑰庫
key_store_type := TJKeyStore.JavaClass.getDefaultType;
s := JStringToString(key_store_type);
LJKS_PK := TJKeyStore.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
//只能是此方式,源码显示不接收非空参数
LJKS_PK.load(nil, nil);
LJKS_PK.setKeyEntry(StringToJString('mykey'), TJKey.Wrap(LJKey), nil, LJCertChain);
kmf.init(LJKS_PK, StringToJString('XF@dM1n').toCharArray);
//证书管理工厂
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);
//设置自己的两个工厂
FClient.SetTrustManagerFactory(tmf);
FClient.setKeyManagerFactory(kmf);
这里面用到SecureBlackBox的两个商业控件:KeyManager: TsbxCryptoKeyManager;
X509Cert: TElX509Certificate;这套控件是跨平台的,很贵,但很方便。也可以用免费的Bouncy Castle来替换,但要对导出的JNI做大量修改,很费神,还要导出一些大D没提供的JNI。