摘要:本文主要介紹如何在Delphi中使用SSL協議.一共分為七個部分:(1)SSL協議是什么?(2)Delphi中如何使用SSL協議?(3)SSL客戶端編程實例.(4)SSL服務端編程實例.(5)SSL證書編程實例.(6)中間人欺騙實例.(7)其它.本文作者同時有一個用SSL協議編寫的作品叫SSLPROXY,感興趣的讀者可以從作者主頁http://www.138soft.org下載.
一:SSL協議是什么?
SSL是一種加密傳輸協議.引用網上一段話:SSL 是Secure socket Layer英文縮寫,它的中文意思是安全套接層協議,指使用公鑰和私鑰技術組合的安全網絡通訊協議。SSL協議是網景公司(Netscape)推出的基於 WEB應用的安全協議,SSL協議指定了一種在應用程序協議(如Http、Telenet、NMTP和FTP等)和TCP/IP協議之間提供數據安全性分層的機制,它為TCP/IP連接提供數據加密、服務器認證、消息完整性以及可選的客戶機認證,主要用於提高應用程序之間數據的安全性,對傳送的數據進行加密和隱藏,確保數據在傳送中不被改變,即確保數據的完整性。
二:Delphi中如何使用SSL協議?
要使用SSL協議,有幾種方法,一種是根據SSL協議文檔自己實現,另外一種是調用第三方的開發庫.現在一般的開發者(無論是Delphi還是VC)都是使用開源的OpenSSL庫.本文的介紹都是基於OpenSSL的.
Delphi中的Indy組件本身就是支持OpenSSL的.例如,對於IdTCPClient,只需要拖一個IdSSLIOHandlerSocket控件到窗口,然后將IdTCPClient的IOHandler指向它即可.不過IdSSLOpenSSLHeaders.pas的聲明比較老,是基於OpenSSL 0.9.4的,而0.9.4的DLL文件現在比較少見了.現在一般使用OpenSSL 0.9.7i.(可以打開IdSSLOpenSSLHeaders.pas,搜索版本,老版本是這樣的:
OPENSSL_OPENSSL_VERSION_NUMBER = $00904100;
OPENSSL_OPENSSL_VERSION_TEXT = 'OpenSSL 0.9.4 09 Aug 1999'; {Do not localize}
0.9.4和0.9.7i的區別是0.9.4的部分函數位於ssleay32.dll中,而0.9.7i則拋棄了此DLL.網上有很多修改過的基於0.9.7i版本的IdSSLOpenSSLHeaders.pas,本文的演示代碼主要是基於0.9.7i.
三:SSL客戶端編程實例
使用OpenSSL之前,得先裝載SSL庫.直接調用Load函數即可完成:
if not uIdSSLOpenSSLHeaders.Load then //裝載ssl庫
begin
Application.MessageBox('裝載ssl動態庫失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
然后裝載錯誤字符庫和加密算法:
IdSslLoadErrorStrings;
IdSslAddSslAlgorithms; //load所有的SSL算法
然后,初始化SSL使用的協議版本和SSL上下文:
methClient := IdSslMethodClientV23;//SSL協議有多個版本,包括SSLv2,SSLv23,SSLv3和TLSv1我們這里使用v23.
if methClient = nil then
begin
Application.MessageBox('建立SSL Client所用的method失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
ctxClient := IdSslCtxNew(methClient); //初始化上下文情景.
if ctxClient = nil then
begin
Application.MessageBox('創建客戶端SSL_CTX失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
上下文情景初始化完畢后,就可以進行socket通信了.首先創建一個正常的socket,連接到目的地址,然后申請一個SSL會話的環境:
sslClient := IdSslNew(ctxClient);
if sslClient = nil then
begin
closesocket(ClientSocket);
Application.MessageBox('申請SSL會話的環境失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
然后綁定套接字:
IdSslSetFd(sslClient, ClientSocket);
IdSslConnect(sslClient);
成功后就可以調用IdSslRead和IdSslWrite來讀寫數據了.
下面我們給出一個完整的例子,用於連接安全焦點的論壇首頁.此程序用到一個Memo控件和一個按鈕控件.注意:此代碼沒有做過多的錯誤處理,讀者可以自行添加:
procedure TForm1.Button1Click(Sender: TObject);
const
//Url: string = 'https://www.xfocus.net/bbs/index.php?lang=cn';
Host: string = 'www.xfocus.net';
Port: Word = 443;
var
Wsa: TWSAData;
ctxClient: PSSL_CTX; //SSL上下文
methClient: PSSL_METHOD;
sRemoteIP: string;
ClientSocket: TSocket;
sAddr: TSockAddr;
sslClient: PSSL;
pServer_Cert: PX509;
pStr: Pchar;
buf: array[0..4095] of Char;
nRet: integer;
strSend: string;
begin
Button1.Enabled := False;
if WSAStartup($101, Wsa) <> 0 then //初始化Wsock32.dll,MakeWord(2,2),
begin
Application.MessageBox('初始化Winsock動態庫失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
if not uIdSSLOpenSSLHeaders.Load then //裝載ssl庫
begin
Application.MessageBox('裝載ssl動態庫失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
IdSslLoadErrorStrings;
IdSslAddSslAlgorithms; //load所有的SSL算法
methClient := IdSslMethodClientV23;
if methClient = nil then
begin
Application.MessageBox('建立SSL Client所用的method失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
ctxClient := IdSslCtxNew(methClient); //初始化上下文情景
if ctxClient = nil then
begin
Application.MessageBox('創建客戶端SSL_CTX失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
sRemoteIP := GetIPAddressByHost(Host);
if sRemoteIP = '' then
begin
Application.MessageBox('獲取遠程IP地址失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
ClientSocket := Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ClientSocket = INVALID_SOCKET then
begin
Application.MessageBox('創建socket失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
FillChar(sAddr, Sizeof(TSockAddr), #0);
sAddr.sin_family := AF_INET;
sAddr.sin_addr.S_addr := inet_addr(Pchar(sRemoteIP));
sAddr.sin_port := htons(Port);
if connect(ClientSocket, sAddr, sizeof(TSockAddr)) = SOCKET_ERROR then
begin
Application.MessageBox('連接遠程服務器失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
sslClient := IdSslNew(ctxClient); //申請SSL會話的環境,參數就是前面我們申請的 SSL通訊方式。返回當前的SSL 連接環境的指針。
if sslClient = nil then
begin
closesocket(ClientSocket);
Application.MessageBox('申請SSL會話的環境失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
IdSslSetFd(sslClient, ClientSocket); //綁定讀寫套接字
if IdSslConnect(sslClient) = -1 then
begin
IdSslFree(sslClient);
closesocket(ClientSocket);
Application.MessageBox('綁定讀寫套接字失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
Memo1.Lines.Add(Format('SSL連接使用的算法為:%s', [IdSSLCipherGetName(IdSSLGetCurrentCipher(sslClient))]));
pServer_Cert := IdSslGetPeerCertificate(sslClient);
if (pServer_Cert <> nil) then
begin
Memo1.Lines.Add('服務端證書:');
pStr := IdSslX509NameOneline(IdSslX509GetSubjectName(pServer_Cert), nil, 0);
if pStr <> nil then Memo1.Lines.Add(Format('主題: %s', [pStr]));
pStr := IdSslX509NameOneline(IdSslX509GetIssuerName(pServer_Cert), nil, 0);
if pStr <> nil then Memo1.Lines.Add(Format('發行者: %s', [pStr]));
end
else
begin
Memo1.Lines.Add('客戶端沒有證書.');
end;
Memo1.Lines.Add('');
strSend := 'GET /bbs/index.php?lang=cn HTTP/1.1'#$D#$A +
'Accept: */*'#$D#$A +
'Accept-Language: zh-cn'#$D#$A +
'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)'#$D#$A +
'Host: ' + Host + #$D#$A +
'Connection: Keep-Alive'#$D#$A#$D#$A;
if IdSslWrite(sslClient, @strSend[1], Length(strSend)) <= 0 then
begin
IdSslFree(sslClient);
closesocket(ClientSocket);
Application.MessageBox('發送失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
FillChar(buf, sizeof(buf), #0);
nRet := IdSslRead(sslClient, buf, sizeof(buf));
while nRet > 0 do
begin
Memo1.Lines.Add(StrPas(Buf));
FillChar(buf, sizeof(buf), #0);
Application.ProcessMessages;
nRet := IdSslRead(sslClient, buf, sizeof(buf));
end;
IdSslFree(sslClient);
closesocket(ClientSocket);
if ctxClient <> nil then IdSslCtxFree(ctxClient);
WSACleanup; //結束對WSocket32.dll調用
Button1.Enabled := True;
end;
四:SSL服務端編程實例
服務端沒有什么好講的,跟客戶端不同的地方,在於初始化時需要加載證書,然后Accept后需要再用IdSslAccept關聯.關於如何用Delphi生成證書文件,詳見下一講.下面直接貼代碼:
var
Form1: TForm1;
g_Wsa: TWSAData;
g_ServerSocketMain: TSocket = INVALID_SOCKET;
g_ctxServer: PSSL_CTX = nil; //SSL上下文
g_methServer: PSSL_METHOD = nil;
g_DebugCritSec: TRTLCriticalSection;
g_hAcceptThread: THandle = 0;
g_Start: BOOL = False;
implementation
{$R *.dfm}
function AcceptThread(lp: Pointer): DWORD; stdcall;
var
nAddrLen: integer;
sdClient: TSocket;
sAddrs: TSockAddr;
nErrorCode: integer;
sslServer: PSSL;
pClient_Cert: PX509;
pStr: Pchar;
nRet: integer;
buf: array[0..4095] of Char;
strBody, strSend: string;
begin
Result := 0;
while g_Start do
begin
nAddrLen := sizeof(TSockAddr);
sdClient := accept(g_ServerSocketMain, @sAddrs, @nAddrLen);
if (sdClient = INVALID_SOCKET) then
begin
nErrorCode := WSAGetLastError;
EnterCriticalSection(g_DebugCritSec);
if g_Start then Form1.Memo1.Lines.Add(Format('發生錯誤.錯誤代碼:%d', [nErrorCode]));
LeaveCriticalSection(g_DebugCritSec);
end
else
begin
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add(Format('新連接.客戶端IP:%s', [inet_ntoa(sAddrs.sin_addr)]));
LeaveCriticalSection(g_DebugCritSec);
sslServer := IdSslNew(g_ctxServer); //申請SSL會話的環境,參數就是前面我們申請的 SSL通訊方式,返回當前的SSL 連接環境的指針.
if sslServer = nil then
begin
closesocket(sdClient);
Continue;
end;
IdSslSetFd(sslServer, sdClient); //綁定讀寫套接字
nRet := IdSslAccept(sslServer);
&nb"7p0�B if (nRet = -1) then<br "2="" ="" begin<br=""> closesocket(sdClient);
IdSslShutdown(sslServer);
IdSslFree(sslServer);
Continue;
end;
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add(Format('SSL連接使用的算法為:%s', [IdSSLCipherGetName(IdSSLGetCurrentCipher(sslServer))]));
LeaveCriticalSection(g_DebugCritSec);
pClient_Cert := IdSslGetPeerCertificate(sslServer);
if (pClient_Cert <> nil) then
begin
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add('客戶端證書:');
LeaveCriticalSection(g_DebugCritSec);
pStr := IdSslX509NameOneline(IdSslX509GetSubjectName(pClient_Cert), nil, 0);
if pStr = nil then Exit;
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add(Format('主題: %s', [pStr]));
LeaveCriticalSection(g_DebugCritSec);
IdSslFree(pStr);
pStr := IdSslX509NameOneline(IdSslX509GetIssuerName(pClient_Cert), nil, 0);
if pStr = nil then Exit;
IdSslFree(pStr);
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add(Format('發行者: %s', [pStr]));
LeaveCriticalSection(g_DebugCritSec);
IdSslFree(pStr);
IdSslFree(pClient_Cert);
end
else
begin
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add('客戶端沒有證書.');
LeaveCriticalSection(g_DebugCritSec);
end;
FillChar(buf, sizeof(buf), #0);
nRet := IdSslRead(sslServer, buf, sizeof(buf));
if nRet <= 0 then
begin
closesocket(sdClient);
sdClient := INVALID_SOCKET;
IdSslShutdown(sslServer);
IdSslFree(sslServer);
sslServer := nil;
end
else
begin
EnterCriticalSection(g_DebugCritSec);
Form1.Memo1.Lines.Add('客戶端發送請求:'#$D#$A + StrPas(buf));
LeaveCriticalSection(g_DebugCritSec);
end;
strBody := 'Your IP is:' + inet_ntoa(sAddrs.sin_addr);
strSend := 'HTTP/1.1 200 OK'#$D#$A +
'Content-Length: ' + IntToStr(Length(strBody)) + #$D#$A +
'Content-Type: text/html'#$D#$A#$D#$A + strBody;
IdSslWrite(sslServer, @strSend[1], Length(strSend));
closesocket(sdClient);
IdSslShutdown(sslServer);
IdSslFree(sslServer);
end;
end;
g_hAcceptThread := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
sAddr: TSockAddr;
dwThreadID: DWORD;
begin
Button1.Enabled := False;
//Create a socket
g_ServerSocketMain := Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if g_ServerSocketMain = INVALID_SOCKET then
begin
MessageBox(0, '創建Socket失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Application.Terminate;
Exit;
end;
//Bind Port
FillChar(sAddr, Sizeof(TSockAddr), #0);
sAddr.sin_family := AF_INET;
sAddr.sin_port := htons(StrToIntDef(Trim(Edit1.Text), 443));
sAddr.sin_addr.S_addr := INADDR_ANY;
if Bind(g_ServerSocketMain, sAddr, Sizeof(TSockAddr)) = SOCKET_ERROR then
begin
MessageBox(0, '綁定端口失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Application.Terminate;
Exit;
end;
//listen
if listen(g_ServerSocketMain, SOMAXCONN) = SOCKET_ERROR then
begin
MessageBox(0, '監聽端口失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Application.Terminate;
Exit;
end;
g_Start := TRUE;
g_hAcceptThread := CreateThread(nil, 0, @AcceptThread, nil, 0, dwThreadId);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
g_Start := FALSE;
if g_ServerSocketMain <> INVALID_SOCKET then
begin
closesocket(g_ServerSocketMain);
g_ServerSocketMain := INVALID_SOCKET;
end;
if (g_hAcceptThread <> 0) then
begin
WaitForSingleObject(g_hAcceptThread, INFINITE);
CloseHandle(g_hAcceptThread);
g_hAcceptThread := 0;
end;
end;
initialization
if WSAStartup($101, g_Wsa) <> 0 then //初始化Wsock32.dll,2.2版本可以使用MakeWord(2,2),
begin
MessageBox(0, '初始化Winsock動態庫失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
if not Load then //裝載ssl庫失敗
begin
MessageBox(0, '裝載ssl動態庫失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
IdSslLoadErrorStrings;
IdSslAddSslAlgorithms; //load所有的SSL算法.
//========================== 初始化SSL Server ================================
g_methServer := IdSslMethodV23; //建立SSL所用的method.
if g_methServer = nil then
begin
MessageBox(0, '建立SSL Server所用的method失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
g_ctxServer := IdSslCtxNew(g_methServer); //初始化上下文情景.
if g_ctxServer = nil then //創建SSL_CTX失敗
begin
MessageBox(0, '創建服務端SSL_CTX失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
if IdSslCtxUseCertificateFile(g_ctxServer, Pchar(ExtractFilePath(GetModuleName(HInstance)) + 'UserCert.pem'), OPENSSL_SSL_FILETYPE_PEM) <= 0 then //加載證書失敗
begin
MessageBox(0, '加載證書失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
if IdSslCtxUsePrivateKeyFile(g_ctxServer, Pchar(ExtractFilePath(GetModuleName(HInstance)) + 'UserKey.pem'), OPENSSL_SSL_FILETYPE_PEM) <= 0 then //加載私鑰失敗
begin
MessageBox(0, '加載私鑰失敗!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
if not IdSslCtxCheckPrivateKeyFile(g_ctxServer) = 0 then //密鑰證書不匹配
begin
MessageBox(0, '密鑰和證書不匹配!', '錯誤', MB_ICONEXCLAMATION + MB_TOPMOST);
Halt;
end;
InitializeCriticalSection(g_DebugCritSec);
finalization
if g_ctxServer <> nil then IdSslCtxFree(g_ctxServer);
DeleteCriticalSection(g_DebugCritSec);
WSACleanup; //結束對WSocket32.dll調用
end.
完整代碼下載地址:http://www.138soft.com/download/ssldemo_server.rar
實際上,稍微用心的人結合第一講的客戶端例子,已經可以寫出中間人欺騙程序了.
完整代碼下載地址:http://www.138soft.com/download/ssldemo_client.rar
http://www.cnblogs.com/qiubole/archive/2007/12/24/1012077.html