Indy 可以从内存中加载 SSL 证书吗?
Can Indy load SSL certificates from memory?
我有一个客户端应用程序需要使用证书将自己授权给服务器。通信使用基于 TCP 的 SSL(与 HTTP/HTTPS 无关)。
当我使用私钥文件并在 IOHandler 等上设置它时,我可以正常工作
我想避免将私钥文件放在客户端的磁盘上。相反,我想以某种方式通过内存向 Indy 提供私钥。
我一直没能找到办法做到这一点,所以现在我想知道这是否完全可行?
说明:
我的计划是将加密形式的私钥作为资源包含在 .exe
文件中,在内存中对其进行解密,然后通过流或缓冲区将其提供给 Indy。所以问题是,Indy 是否以某种方式支持它?
如果 Indy 代码将接受一个流,您可以轻松地从 TMemoryStream
加载任何您可以从文件加载的内容,但这并不能解决如何将证书加载到TMemoryStream
第一名。
归根结底,您的程序必须在某个时候从光盘或网络加载证书,然后才能将其保存在内存中。您可以将它作为资源嵌入到 EXE 本身并使用 TResourceStream 加载它,但这仍然算作它在光盘上,因为它是 EXE 的一部分。通过网络加载它意味着您在某处有一台服务器 提供您的私钥, 因此,出于显而易见的原因,这不是一个好主意。真的没有办法将您的私钥保存在某个地方的光盘上;只需确保您的服务器的配置方式使其无法将该文件作为下载提供。
目前,Indy 不公开从内存加载证书的功能。该功能请求有一个开放票证:
#150: Support loading OpenSSL certificate/key data from user-defined storage
但是,OpenSSL 确实支持它,并且在 Windows 上的 D2009+ 下,Indy 求助于该功能以使用 OpenSSL 不支持的 UTF-16 文件名加载证书文件(在 *Nix 系统上,不过它确实支持 UTF-8 文件名)。 Indy 将文件加载到内存中,并使用 OpenSSL 本身使用的相同解析函数。所以你要求的是可能,而不是直截了当。
这是一个例子。这些是 Indy 的 IndySSL_CTX_use_PrivateKey_file_PKCS12()
和(基于 UTF-16 的)IndySSL_CTX_use_PrivateKey_file()
包装函数,TIdSSLContext.LoadKey()
调用它们来加载 TIdSSLOptions.KeyFile
属性 中指定的私钥文件:
function TIdSSLContext.LoadKey: Boolean;
begin
if PosInStrArray(ExtractFileExt(KeyFile), ['.p12', '.pfx'], False) <> -1 then begin
Result := IndySSL_CTX_use_PrivateKey_file_PKCS12(fContext, KeyFile) > 0;
end else begin
Result := IndySSL_CTX_use_PrivateKey_file(fContext, KeyFile, SSL_FILETYPE_PEM) > 0;
end;
if Result then begin
Result := SSL_CTX_check_private_key(fContext) > 0;
end;
end;
function IndySSL_CTX_use_PrivateKey_file_PKCS12(ctx: PSSL_CTX; const AFileName: String): TIdC_INT;
var
LM: TMemoryStream;
B: PBIO;
LKey: PEVP_PKEY;
LCert: PX509;
P12: PPKCS12;
CertChain: PSTACK_OF_X509;
LPassword: array of TIdAnsiChar;
LPasswordPtr: PIdAnsiChar;
begin
Result := 0;
LM := nil;
try
LM := TMemoryStream.Create;
LM.LoadFromFile(AFileName);
except
// Surpress exception here since it's going to be called by the OpenSSL .DLL
// Follow the OpenSSL .DLL Error conventions.
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
LM.Free;
Exit;
end;
try
B := BIO_new_mem_buf(LM.Memory, LM.Size);
if not Assigned(B) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
Exit;
end;
try
SetLength(LPassword, MAX_SSL_PASSWORD_LENGTH+1);
LPassword[MAX_SSL_PASSWORD_LENGTH] := TIdAnsiChar(0);
LPasswordPtr := PIdAnsiChar(LPassword);
if Assigned(ctx^.default_passwd_callback) then begin
ctx^.default_passwd_callback(LPasswordPtr, MAX_SSL_PASSWORD_LENGTH, 0, ctx^.default_passwd_callback_userdata);
// TODO: check return value for failure
end else begin
// TODO: call PEM_def_callback(), like PEM_read_bio_X509() does
// when default_passwd_callback is nil
end;
P12 := d2i_PKCS12_bio(B, nil);
if not Assigned(P12) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_PKCS12_LIB);
Exit;
end;
try
CertChain := nil;
if PKCS12_parse(P12, LPasswordPtr, LKey, LCert, @CertChain) <> 1 then begin
SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_FILE, ERR_R_PKCS12_LIB);
Exit;
end;
try
Result := SSL_CTX_use_PrivateKey(ctx, LKey);
finally
sk_pop_free(CertChain, @X509_free);
X509_free(LCert);
EVP_PKEY_free(LKey);
end;
finally
PKCS12_free(P12);
end;
finally
BIO_free(B);
end;
finally
FreeAndNil(LM);
end;
end;
function IndySSL_CTX_use_PrivateKey_file(ctx: PSSL_CTX; const AFileName: String;
AType: Integer): TIdC_INT;
var
LM: TMemoryStream;
B: PBIO;
LKey: PEVP_PKEY;
j: TIdC_INT;
begin
Result := 0;
LM := nil;
try
LM := TMemoryStream.Create;
LM.LoadFromFile(AFileName);
except
// Surpress exception here since it's going to be called by the OpenSSL .DLL
// Follow the OpenSSL .DLL Error conventions.
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
LM.Free;
Exit;
end;
try
B := BIO_new_mem_buf(LM.Memory, LM.Size);
if not Assigned(B) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
Exit;
end;
try
case AType of
SSL_FILETYPE_PEM:
begin
j := ERR_R_PEM_LIB;
LKey := PEM_read_bio_PrivateKey(B, nil,
ctx^.default_passwd_callback,
ctx^.default_passwd_callback_userdata);
end;
SSL_FILETYPE_ASN1:
begin
j := ERR_R_ASN1_LIB;
LKey := d2i_PrivateKey_bio(B, nil);
end;
else
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, SSL_R_BAD_SSL_FILETYPE);
Exit;
end;
if not Assigned(LKey) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, j);
Exit;
end;
Result := SSL_CTX_use_PrivateKey(ctx, LKey);
EVP_PKEY_free(LKey);
finally
BIO_free(B);
end;
finally
FreeAndNil(LM);
end;
end;
如您所见,Indy 使用 BIO_new_mem_buf()
将证书文件加载到 BIO
,从中提取 EVP_PKEY
,并将其传递给 OpenSSL 的 SSL_CTX_use_PrivateKey()
函数将密钥加载到会话的 SSL_CTX
对象中(Indy 目前不支持 RSA/ASN1 私钥)。
因此,您必须在自己的代码中采用类似的逻辑。当然,您必须让您的代码在正确的时间被调用,为此您需要修改 Indy 的源代码以将您的代码添加到 TIdSSLContext.LoadKey()
,然后重新编译 Indy。至少在 Indy 在未来的版本中公开本机访问之前。
我有一个客户端应用程序需要使用证书将自己授权给服务器。通信使用基于 TCP 的 SSL(与 HTTP/HTTPS 无关)。
当我使用私钥文件并在 IOHandler 等上设置它时,我可以正常工作
我想避免将私钥文件放在客户端的磁盘上。相反,我想以某种方式通过内存向 Indy 提供私钥。
我一直没能找到办法做到这一点,所以现在我想知道这是否完全可行?
说明:
我的计划是将加密形式的私钥作为资源包含在 .exe
文件中,在内存中对其进行解密,然后通过流或缓冲区将其提供给 Indy。所以问题是,Indy 是否以某种方式支持它?
如果 Indy 代码将接受一个流,您可以轻松地从 TMemoryStream
加载任何您可以从文件加载的内容,但这并不能解决如何将证书加载到TMemoryStream
第一名。
归根结底,您的程序必须在某个时候从光盘或网络加载证书,然后才能将其保存在内存中。您可以将它作为资源嵌入到 EXE 本身并使用 TResourceStream 加载它,但这仍然算作它在光盘上,因为它是 EXE 的一部分。通过网络加载它意味着您在某处有一台服务器 提供您的私钥, 因此,出于显而易见的原因,这不是一个好主意。真的没有办法将您的私钥保存在某个地方的光盘上;只需确保您的服务器的配置方式使其无法将该文件作为下载提供。
目前,Indy 不公开从内存加载证书的功能。该功能请求有一个开放票证:
#150: Support loading OpenSSL certificate/key data from user-defined storage
但是,OpenSSL 确实支持它,并且在 Windows 上的 D2009+ 下,Indy 求助于该功能以使用 OpenSSL 不支持的 UTF-16 文件名加载证书文件(在 *Nix 系统上,不过它确实支持 UTF-8 文件名)。 Indy 将文件加载到内存中,并使用 OpenSSL 本身使用的相同解析函数。所以你要求的是可能,而不是直截了当。
这是一个例子。这些是 Indy 的 IndySSL_CTX_use_PrivateKey_file_PKCS12()
和(基于 UTF-16 的)IndySSL_CTX_use_PrivateKey_file()
包装函数,TIdSSLContext.LoadKey()
调用它们来加载 TIdSSLOptions.KeyFile
属性 中指定的私钥文件:
function TIdSSLContext.LoadKey: Boolean;
begin
if PosInStrArray(ExtractFileExt(KeyFile), ['.p12', '.pfx'], False) <> -1 then begin
Result := IndySSL_CTX_use_PrivateKey_file_PKCS12(fContext, KeyFile) > 0;
end else begin
Result := IndySSL_CTX_use_PrivateKey_file(fContext, KeyFile, SSL_FILETYPE_PEM) > 0;
end;
if Result then begin
Result := SSL_CTX_check_private_key(fContext) > 0;
end;
end;
function IndySSL_CTX_use_PrivateKey_file_PKCS12(ctx: PSSL_CTX; const AFileName: String): TIdC_INT;
var
LM: TMemoryStream;
B: PBIO;
LKey: PEVP_PKEY;
LCert: PX509;
P12: PPKCS12;
CertChain: PSTACK_OF_X509;
LPassword: array of TIdAnsiChar;
LPasswordPtr: PIdAnsiChar;
begin
Result := 0;
LM := nil;
try
LM := TMemoryStream.Create;
LM.LoadFromFile(AFileName);
except
// Surpress exception here since it's going to be called by the OpenSSL .DLL
// Follow the OpenSSL .DLL Error conventions.
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
LM.Free;
Exit;
end;
try
B := BIO_new_mem_buf(LM.Memory, LM.Size);
if not Assigned(B) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
Exit;
end;
try
SetLength(LPassword, MAX_SSL_PASSWORD_LENGTH+1);
LPassword[MAX_SSL_PASSWORD_LENGTH] := TIdAnsiChar(0);
LPasswordPtr := PIdAnsiChar(LPassword);
if Assigned(ctx^.default_passwd_callback) then begin
ctx^.default_passwd_callback(LPasswordPtr, MAX_SSL_PASSWORD_LENGTH, 0, ctx^.default_passwd_callback_userdata);
// TODO: check return value for failure
end else begin
// TODO: call PEM_def_callback(), like PEM_read_bio_X509() does
// when default_passwd_callback is nil
end;
P12 := d2i_PKCS12_bio(B, nil);
if not Assigned(P12) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_PKCS12_LIB);
Exit;
end;
try
CertChain := nil;
if PKCS12_parse(P12, LPasswordPtr, LKey, LCert, @CertChain) <> 1 then begin
SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_FILE, ERR_R_PKCS12_LIB);
Exit;
end;
try
Result := SSL_CTX_use_PrivateKey(ctx, LKey);
finally
sk_pop_free(CertChain, @X509_free);
X509_free(LCert);
EVP_PKEY_free(LKey);
end;
finally
PKCS12_free(P12);
end;
finally
BIO_free(B);
end;
finally
FreeAndNil(LM);
end;
end;
function IndySSL_CTX_use_PrivateKey_file(ctx: PSSL_CTX; const AFileName: String;
AType: Integer): TIdC_INT;
var
LM: TMemoryStream;
B: PBIO;
LKey: PEVP_PKEY;
j: TIdC_INT;
begin
Result := 0;
LM := nil;
try
LM := TMemoryStream.Create;
LM.LoadFromFile(AFileName);
except
// Surpress exception here since it's going to be called by the OpenSSL .DLL
// Follow the OpenSSL .DLL Error conventions.
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_SYS_LIB);
LM.Free;
Exit;
end;
try
B := BIO_new_mem_buf(LM.Memory, LM.Size);
if not Assigned(B) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB);
Exit;
end;
try
case AType of
SSL_FILETYPE_PEM:
begin
j := ERR_R_PEM_LIB;
LKey := PEM_read_bio_PrivateKey(B, nil,
ctx^.default_passwd_callback,
ctx^.default_passwd_callback_userdata);
end;
SSL_FILETYPE_ASN1:
begin
j := ERR_R_ASN1_LIB;
LKey := d2i_PrivateKey_bio(B, nil);
end;
else
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, SSL_R_BAD_SSL_FILETYPE);
Exit;
end;
if not Assigned(LKey) then begin
SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, j);
Exit;
end;
Result := SSL_CTX_use_PrivateKey(ctx, LKey);
EVP_PKEY_free(LKey);
finally
BIO_free(B);
end;
finally
FreeAndNil(LM);
end;
end;
如您所见,Indy 使用 BIO_new_mem_buf()
将证书文件加载到 BIO
,从中提取 EVP_PKEY
,并将其传递给 OpenSSL 的 SSL_CTX_use_PrivateKey()
函数将密钥加载到会话的 SSL_CTX
对象中(Indy 目前不支持 RSA/ASN1 私钥)。
因此,您必须在自己的代码中采用类似的逻辑。当然,您必须让您的代码在正确的时间被调用,为此您需要修改 Indy 的源代码以将您的代码添加到 TIdSSLContext.LoadKey()
,然后重新编译 Indy。至少在 Indy 在未来的版本中公开本机访问之前。