Delphi XE10.x实现Android下Https双向认证

这篇博客介绍了如何在Android应用中避免用户安装客户端证书,而是通过代码加载.pfx证书进行HTTPS请求。文章详细阐述了修改系统源码,创建自定义的TrustManager和KeyManager,以及加载和验证证书的步骤,涉及到SecureBlackBox控件的使用,但同时也提到了可以使用BouncyCastle替代。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我有一个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。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值