上周,另一部门需要支援解决数字签名问题。但因为之前也没做过,现学现卖。此方面可参考的中文资料较少,特作分享,方便查阅。
有关数字签名的概念、原理,这里就不做介绍了,请自行google或百度。
利用证书对文件进行签名,从证书来源看,可分为两种:1、软证书:就是将*.pfx文件导入到系统中,这意味着,只要登录到PC中的用户,均可以使用该证书;2、硬证书:通常将证书存放到uKey中(smart card),这样的好处是,只有拥有usb key的人才有权限使用该证书。
USB Key通常支持CryptToAPI——除非特殊安全需要,只公布使用自己的接口,不支持微软接口。由于使用CryptToAPI,使用起来较繁琐,微软提供了CAPICOM组件,方便开发。
不论是硬证书或软证书,只要支持CryptToAPI接口,那么CAPICOM均可使用。为此本次内容以CAPICOM,作为数字签名功能的基础。
动手之前,首先要熟悉数字签名的过程。通过分析,主要是两部分:数字签名(身份标识及防篡改)和数字信封;其实按业务流程,签名之前还有签章的过程(也就是通常的盖章);过程大致如下:
发送方
1、验证证书是否准备好?(若是硬证书,usbkey是否已插入;判断证书是否有效);
2、对文件进行签名;
3、对文件进行数字信封(公钥加密);
4、可选:填入CSP(加密服务提供商,通常是在USB Key当中)信息
接收方:
1、获取文件,读取CSP信息;
2、依据CSP信息,获取相关证书并验证;
3、利用证书进行数字解封;
4、签名验证,确认身份及文件的完整性(是否被篡改);
依据以上分析,程序可这样设计,由于USB Key可能支持CAPICOM,也可能不支持,所以,后续可能会有相应由多种方法去执行签名。可提取接口,来解除这样的依赖。
接口定义如下:
IDigitalIntf = interface(IUNKNOWN) ['{78657307-FD4A-452F-91FF-956379A7F654}'] //验证设备 function VerifyUserAvailable: Boolean; //签名与数字信封加密 function Pack(const sInPath: string; const sOutPath: string; bOverride: Boolean): Boolean; //数字信封解密与签名验证 function Unpack(const sInPath: string; const sOutPath: string; bCreateDirectory: Boolean): Boolean; //获取数字指纹 function GetThumbPrint: string; //获取证书信息 function GetCertficateInfo(var ACertInfo: TStampInfo): Boolean; end;CAPICOM实现类,构造如下:
TDigital_CAPICOM = class(TInterfacedObject, IDigitalIntf) private FProviderName, FStoreName: string; function GetStoreByName(AStoreName: string): TStore; protected FStoreList: TStringList; ICert: ICertificate; ICert2: ICertificate2; FPublicKey: string;//公钥 FPKLength: Integer;//算法长度 FAlgType: string; // 算法类型 {----------------------方法定义-----------------------} //证书库操作 function OpenStore(AStoreName: string): TStore; procedure CloseStore; //获取证书接口 procedure GetCertificate; //执行文件签名 function SignedFile(const AFileName: string; EncodeType: CAPICOM_ENCODING_TYPE): Boolean; //验证文件签名 function VerifySign(const AFileName: string): Boolean; //附加签名信息 function AppendSignedContent(const AFileName, ASignedContent: string): Boolean; //分解签名信息 function ExtractSignedContent(const AFileName: string): string; {-----------------------------------------------------} {---------------------属性定义------------------------} //CSP提供商 property ProviderName : string read FProviderName; //证书存放位置 property StoreName : string read FStoreName; {-----------------------------------------------------} public function VerifyUserAvailable: Boolean; function Pack(const sInPath: string; const sOutPath: string; bOverride: Boolean): Boolean; function Unpack(const sInPath: string; const sOutPath: string; bCreateDirectory: Boolean): Boolean; function GetThumbPrint: string; function GetCertficateInfo(var ACertInfo: TStampInfo): Boolean; constructor Create(const StoreName, ProviderName: string); virtual; destructor Destroy; override; end;其实现代码去除了关键信息:
function TDigital_CAPICOM.AppendSignedContent(const AFileName, ASignedContent: string): Boolean; var msSrc, ms1: TMemoryStream; iLen: Integer; sSignedData, sLength: string; BDA: TByteDynArray; begin if not FileExists(AFileName) then raise Exception.Create('文件"' + AFileName + '"不存在'); //拼接签名信息 sLength := IntToStr(Length(ASignedContent)); sLength := FillChars(sLength, HashString_Length); sSignedData := HYMSignature + sLength + ASignedContent; BDA:= String2Byte(sSignedData); iLen := Length(sSignedData); msSrc := TMemoryStream.Create; ms1 := TMemoryStream.Create; try msSrc.LoadFromFile(AFileName); ms1.Write(BDA[0], iLen); //写入文件头信息 ms1.Write(msSrc.Memory^, msSrc.Size); //把文件内容附加上 ms1.SaveToFile(AFileName); finally ms1.Free; msSrc.Free; end; Result := True; end; procedure TDigital_CAPICOM.CloseStore; var vStore: TStore; iCnt: Integer; begin try for iCnt := 0 to FStoreList.Count - 1 do begin vStore := TStore(FStoreList.Objects[iCnt]); vStore.Disconnect; end; except raise Exception.Create('关闭密钥库失败!'); end; end; constructor TDigital_CAPICOM.Create(const StoreName, ProviderName: string); begin CoInitialize(nil); FProviderName:= ProviderName; FStoreName := StoreName; FStoreList:= TStringlist.create; GetCertificate; end; destructor TDigital_CAPICOM.Destroy; begin FStoreList.Free; ICert := nil; ICert2:= nil; CoUninitialize; inherited; end; function TDigital_CAPICOM.ExtractSignedContent( const AFileName: string): string; var fs: TFileStream; iHeadLen, iContentLen, iPos: Integer; sContentLength: string; ms: TMemoryStream; BDA_Head, BDA_Cont: TByteDynArray; begin Result := ''; if not FileExists(AFileName) then raise Exception.Create('文件"' + AFileName + '"不存在'); iHeadLen := Length(HYMSignature) + HashString_Length; SetLength(BDA_Head, iHeadLen); ms:= TMemoryStream.Create; ms.LoadFromFile(AFileName); fs := TFileStream.Create(AFileName, fmCreate); try ms.Position:= 0; ms.Read(BDA_Head[0], iHeadLen); sContentLength := Byte2String(BDA_Head); //含有长度信息 iPos := Pos(HYMSignature, sContentLength); if iPos > 0 then begin //取得长度 iContentLen := StrToInt(Copy(sContentLength, Length(HYMSignature) + 1, MaxInt)); SetLength(BDA_Cont, iContentLen); ms.Read(BDA_Cont[0], iContentLen); Result := Byte2String(BDA_Cont); //该位置之后的内容为真正需要的 fs.CopyFrom(ms, ms.Size - ms.Position); //读取文件内容去除文件头部分 fs.Position := 0; end finally ms.Free; fs.Free; end; end; function TDigital_CAPICOM.GetCertficateInfo( var ACertInfo: TStampInfo): Boolean; var iCnt: Integer; begin Result := True; if ICert <> nil then begin ACertInfo.PKAlg := FAlgType; ACertInfo.PKLength := FPKLength; for iCnt := 0 to Length(FPublicKey) - 1 do begin ACertInfo.PKContent[iCnt] := FPublicKey[iCnt + 1]; end; ACertInfo.EndDate:= ICert.ValidToDate; ACertInfo.DispachTime:= ICert.ValidFromDate; end else result:= False; end; procedure TDigital_CAPICOM.GetCertificate; var vStore: TStore; iCnt: Integer; IBaseIntf: IInterface; ICert2Dsp: ICertificate2Disp; begin if ICert2 = nil then begin vStore := OpenStore(FStoreName); for iCnt := 1 to vStore.Certificates.Count do begin IBaseIntf := vStore.Certificates.Item[iCnt]; try if IBaseIntf.QueryInterface(ICertificate2Disp, ICert2Dsp) = 0 then begin //确认硬件是否连接 if ICert2Dsp.HasPrivateKey then begin //确认是否为指定CSP提供商 if ((FProviderName = CSPProvider_ePass) and ((ICert2Dsp.PrivateKey.ProviderName = CSPProvider_ePass_1K) or (ICert2Dsp.PrivateKey.ProviderName = CSPProvider_ePass_3K))) or (ICert2Dsp.PrivateKey.ProviderName = FProviderName) then begin IBaseIntf.QueryInterface(IID_ICertificate2, ICert2); IBaseIntf.QueryInterface(IID_ICertificate, ICert); FPublicKey:= ICert2Dsp.publickey.EncodedKey.Format(True); FPKLength:= ICert2Dsp.publickey.Length; FAlgType:= ICert2Dsp.publickey.Algorithm.FriendlyName; end; end; end; except //某些不支持CAPICOM的,会出现异常 ICert2 := nil; end; end; end; end; function TDigital_CAPICOM.GetStoreByName(AStoreName: string): TStore; var i: integer; begin i := FStoreList.IndexOf(AStoreName); if i >= 0 then result := FStoreList.Objects[i] as Tstore else result := nil; end; function TDigital_CAPICOM.GetThumbPrint: string; begin Result := ''; if ICert <> nil then Result := ICert.Thumbprint; end; function TDigital_CAPICOM.OpenStore(AStoreName: string): TStore; var vStore: TStore; begin vStore := self.GetStoreByName(AStoreName); if vStore = nil then try vStore := TStore.Create(nil); //默认为从CurrenUser读取, 后续可能会是CAPICOM_SMART_CARD_USER_STORE 智能卡 vStore.Open(CAPICOM_CURRENT_USER_STORE, AStoreName, CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED or CAPICOM_STORE_OPEN_INCLUDE_ARCHIVED or CAPICOM_STORE_OPEN_EXISTING_ONLY); self.FStoreList.AddObject(AStoreName, vStore); except on E:exception do raise exception.Create('无法打开密钥库!'+E.Message); end; Result := vStore; end; function TDigital_CAPICOM.Pack(const sInPath, sOutPath: string; bOverride: Boolean): Boolean; var EnvelopedData: IEnvelopedData; BUFFER: WideString; FileStm: TFileStream; iP, oP: string; begin ip:= StringReplace(sInPath, '\\', '\', [rfReplaceAll]); op:= StringReplace(sOutPath, '\\', '\', [rfReplaceAll]); Result := True; EnvelopedData := CoEnvelopedData.Create; //指定采用的CSP算法类型 EnvelopedData.Algorithm.Name := Algorithm; //指定加密长度 EnvelopedData.Algorithm.KeyLength := EnLength; try //获取证书接口 GetCertificate; //目前sInPath是一个文件夹,先压缩,再解密 Files2ZipArchive(ip, op, RZipPassWd); //执行签名 SignedFile(op, CAPICOM_ENCODE_BASE64); //获取要加密的内容 FileStm := TFileStream.Create(sOutPath, fmOpenRead); try Pointer(Buffer):= SysAllocStringByteLen (nil, FileStm.Size); FileStm.ReadBuffer(Pointer(Buffer)^, FileStm.Size); EnvelopedData.Content:= Buffer; finally FileStm.Free; end; //基于64位编码加密 EnvelopedData.Recipients.Add(ICert2); Buffer:= EnvelopedData.Encrypt(CAPICOM_ENCODE_BASE64); //输出加密内容 FileStm := TFileStream.Create(sOutPath, fmCreate); try FileStm.WriteBuffer(Pointer(Buffer)^, SysStringByteLen(PWideChar(Buffer))); finally FileStm.Free; end; except Result := False; end; end; function TDigital_CAPICOM.SignedFile(const AFileName: string; EncodeType: CAPICOM_ENCODING_TYPE): Boolean; var Signer: ISigner2; SignedData: ISignedData; HashString: string; SignedContent: WideString; begin Result := True; try GetCertificate; //获取文件哈希值 HashString:= GetFileHash(AFileName); //构建 签名者 Signer := CoSigner.Create; Signer.Certificate := ICert2; //构建 数据签名对象 SignedData := CoSignedData.Create; //执行签名 SignedData.Content:= HashString; SignedContent := SignedData.Sign(Signer, False, EncodeType); //附加签名信息 AppendSignedContent(AFileName, SignedContent); except Result := False; end; end; function TDigital_CAPICOM.Unpack(const sInPath, sOutPath: string; bCreateDirectory: Boolean): Boolean; var EnvelopedData: IEnvelopedData; BUFFER: WideString; FileStm: TFileStream; vDecryptFileName: string; begin Result := True; EnvelopedData := CoEnvelopedData.Create; //指定采用的CSP算法类型 EnvelopedData.Algorithm.Name := Algorithm; //指定加密长度 EnvelopedData.Algorithm.KeyLength := EnLength; try //获取数字证书接口 GetCertificate; //关联证书以解密 EnvelopedData.Recipients.Add(ICert2); //获取加密内容 FileStm := TFileStream.Create(sInPath, fmOpenRead ); try Pointer(Buffer):= SysAllocStringByteLen (nil, FileStm.Size); FileStm.ReadBuffer(Pointer(Buffer)^, FileStm.Size); finally FileStm.Free; end; //解密 EnvelopedData.Decrypt(Buffer); Buffer:= EnvelopedData.Content; //输出解密内容 vDecryptFileName:= sOutPath + ExtractFileName(sInPath); FileStm := TFileStream.Create(vDecryptFileName, fmCreate); try FileStm.WriteBuffer(Pointer(Buffer)^, SysStringByteLen(PWideChar(Buffer))); finally FileStm.Free; end; //验证签名 VerifySign(vDecryptFileName); //因为有压缩,再解压 ZipArchive2Files ZipArchive2Files(vDecryptFileName, sOutPath, RZipPassWd); DeleteFile(PAnsiChar(vDecryptFileName)); except Result := False; end; end; function TDigital_CAPICOM.VerifySign(const AFileName: string): Boolean; var SignedData: ISignedData; HashString: WideString; ASignedContent: string; begin Result := True; try GetCertificate; //先获取签名信息,因为会做信息分离,还原出加上签名前的数据 ASignedContent:= ExtractSignedContent(AFileName); //获取文件哈希值 HashString:= GetFileHash(AFileName); //构建 数据签名对象 SignedData := CoSignedData.Create; SignedData.Content := HashString; //执行检查 SignedData.Verify(ASignedContent, False, CAPICOM_VERIFY_SIGNATURE_ONLY); except Result := False; Raise Exception.Create('数字签名校验失败!'); end; end; function TDigital_CAPICOM.VerifyUserAvailable: Boolean; begin Result := False; if (ICert2 <> nil) and ICert2.HasPrivateKey then Result:= True; end;另外,还需要一个管理类,目的是解除依赖,这里就不说明了。
功能的实现,通过google,不论你了解或不了解,都可以得到较多信息,帮助实现。更多的还是在于怎么去设计?怎么让后续的开发人员更容易维护?
这里面有个与证书接口相关的问题,比如在GetCertificate,里面有判断PrivateKey,必须使用Disp接口,直接用ICertificate,会出现地址错误。具体原因,还待查证。有谁知道的,还请你指点指点。谢谢!