摘要:数字签名SIGN保证控件在下载时候的安全性。如果你的代码已经经过数字签名,即使用户IE的安全设置很高也能下载,安装并登记。但是在页面上初始化,或者用脚本运行这个控件,为了保证安全性,还需要进行MARK。
数字签名SIGN 曹晓峰 摘要:数字签名保证控件的安全性。数字签名使用证书。证书一般有个人证书和授信公司证书。个人证书是对个人的信任,由个人承担责任,控件每次下载时需要进行确认。公司证书是由第三方公司发布的,保证控件的安全性,公司证书需要付费。Windows授信的证书公司有VeriSign,SecureSign等等。由这些公司证书签名的控件在下载的时候不需要确认。 一.工具 工具包括以下几个软件: makecert.exe 制作cer格式的证书,即X.509证书,同时可以创建私钥(防止抵赖) cert2spc.exe 将cer格式证书转换成spc格式证书,即PKCS #7证书 signcode.exe 将证书签署到ocx上去 chktrust.exe 检查签署证书后的ocx是否正确 certmgr.exe,是管理证书用的,可以从这里面导出root.cer来,不过没有私钥用不上。 二.步骤 下面是具体的步骤: 1、创建一个自己的证书文件: makecert /sv "Record.PVK" /n "CN=SinoWave" dream.cer 这里,Record.PVK表示新创建的私人密钥保存文件名 SinoWave是你想显示的公司名 dream.cer是你创建最后的证书文件名 这些根据你自己的要求填写,最后得到Record.PVK和dream.cer两个文件。其中,运行过程中需要输入私人密钥的保护密码(sw),一定要输入一致,不要出错。 2、转换cer格式为spc格式(可以省略),得到dream.spc文件。 cert2spc dream.cer dream.spc 3、用VS6工具中的 cabarc生成internet分发的CAB包, cabarc.exe N DataTransfer.cab DataTransfer.ocx 4、同时制作分发代码(.htm,其中包含使IE可以自动下载安装包的代码)。 现在得到了2个文件DataTransfer.CAB和DataTransfer.htm。 .htm中包含类似如下的代码: <OBJECT ID=" DataTransfer " CLASSID="CLSID: CA466D54-0684-49D2-B0C3-DD7E09EA76D3" CODEBASE="http://192.9.200.8/DataTransfer.CAB#version=1,0,0,0"></OBJECT> 注意:一定要写上"http:// 192.9.200.8/",真正发行时最好使用url。 5、给CAB文件签名 运行signcode,命令行的我没有试验通过,我是通过界面实现的。signcode运行后会出现数字签名向导,首先选择DataTransfer.CAB,下一步后会出现签名选项,一种是典型,一种是自定义。选择自定义,这样才能从文件选择证书,选择前面制作的dream.spc,再下一步是选择私钥文件,选择Record.PVK,输入私人密钥的保护密码,选择散列算法,一般用md5就可以了,下一步是选择其他证书,直接下一步,填写一下这个控件的声明,用户用ie浏览的时候,会弹出证书说明,再下一步是加盖时间戳,例如http://timestamp.sheca.com/timestamp 6、用chktrust检查是否正确 chktrust -v DataTransfer.CAB 7、将签名后的DataTransfer.CAB和DataTransfer.htm复制到IIS的某个目录下。并在IE中打开DataTransfer.htm文件进行测试。
ActiveX控件的安全初始化和脚本操作MARK 曹晓峰 简介 很多微软的ActiveX控件(本地/远程)都需要使用持久性数据进行初始化,而且它们大多数都是可以通过脚本进行操作的 (支持一个方法,事件和属性的集合提供脚本语言操作)。初始化(使用持久性数据)和脚本操作都需要一个确定的安全性机制保证其安全性不被违背。 初始化安全性 当一个控件初始化时,可以从一个 IPersist* 接口获得数据 (来自一个本地/远端的URL)提供初始化状态。这是一个潜在的安全隐患,因为数据可能来自一个不可信的数据源。不提供安全性保证的控件将无视数据源的安全性。 有两种方法可以检测控件的初始化安全性。第一种使用组件分组管理器(Component Categories Manager)创建一个正确的入口到系统注册表。IE检测注册表之后才调用你的控件决定是否这些入口存在。第二种方法实现一个名称为IObjectSafe的接口到你的控件。如果IE发现你的控件支持IObjectSafety,它调用 IObjectSafety::SetInterfaceSafetyOptions 方法然后才载入你的控件。 注意,第一种方法是基于组件的安全性,也就是如果设置了注册表,那么整个组件的所有的接口都被标注为安全的;而第二种方法(实现IObjectSafety)是基于接口的,也就是它的作用范围是接口,必须一个接口一个接口地标注。第二种方法的运行性能要优于第一种。在保证安全的情况下,两种方法同时使用更好。 脚本操作安全性 代码签字可以保证用户其代码的可信度。但是运行一个 ActiveX 控件可以被脚本访问将带来几个新的安全性问题。即使控件被认为是可靠的,如果使用不可信脚本代码访问也不能保证其安全性。比如,微软的 Word 被认为是一个安全的程序,但是一个宏可以使用自动化模型的脚本删除用户计算机上的文件,载入宏病毒或者蠕虫病毒。 有两种方法提供你的控件的脚本操作安全性保证。第一种是使用组件分组管理器 (Component Categories Manager) ——在组件导入以后在注册表上面创建正确的入口。IE在脚本操作之前检查注册表确认安全性。第二种是实现一个 IObjectSafety 接口到你的控件。如果IE发现你的控件支持 IObjectSafety,就在导入控件之前调用 IObjectSafety::SetInterfaceSafetyOptions 方法来确保安全性脚本操作。 注意,第一种方法是基于组件的安全性,也就是如果设置了注册表,那么整个组件的所有的接口都被标注为安全的;而第二种方法是基于接口的,也就是它的作用范围是接口,必须一个接口一个接口地标注。第二种方法的运行性能要优于第一种。在保证安全的情况下,两种方法同时使用更好。 方法一:IObjectSafety方法: MFC:实现接口IObjectSafety ATL:继承接口IObjectSafetyImpl<CPolyCtl, INTERFACESAFE_FOR_UNTRUSTED_CALLER| INTERFACESAFE_FOR_UNTRUSTED_DATA > 具体方法: 一. MFC 1. 在控件类的头文件里 (1)加入文件#include <objsafe.h> (2)在类的定义里,声明接口映射表,并加入接口定义(以嵌套类的形式) DECLARE_INTERFACE_MAP() BEGIN_INTERFACE_PART(ObjSafe, IObjectSafety) STDMETHOD_(HRESULT, GetInterfaceSafetyOptions) ( /* [in] */ REFIID riid, /* [out] */ DWORD __RPC_FAR *pdwSupportedOptions, /* [out] */ DWORD __RPC_FAR *pdwEnabledOptions ); STDMETHOD_(HRESULT, SetInterfaceSafetyOptions) ( /* [in] */ REFIID riid, /* [in] */ DWORD dwOptionSetMask, /* [in] */ DWORD dwEnabledOptions ); END_INTERFACE_PART(ObjSafe); 2. 在控件类的CPP文件里 (1)建立接口映射表 BEGIN_INTERFACE_MAP( CMyCtrl, COleControl ) INTERFACE_PART(CMyCtrl, IID_IObjectSafety, ObjSafe) END_INTERFACE_MAP() (2)加入接口类的实现 ULONG FAR EXPORT CMyCtrl::XObjSafe::AddRef() { METHOD_PROLOGUE(CMyCtrl, ObjSafe) return pThis->ExternalAddRef(); } ULONG FAR EXPORT CMyCtrl::XObjSafe::Release() { METHOD_PROLOGUE(CMyCtrl, ObjSafe) return pThis->ExternalRelease(); } HRESULT FAR EXPORT CMyCtrl::XObjSafe::QueryInterface( REFIID iid, void FAR* FAR* ppvObj) { METHOD_PROLOGUE(CMyCtrl, ObjSafe) return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj); } const DWORD dwSupportedBits = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA; const DWORD dwNotSupportedBits = ~ dwSupportedBits; / // CStopLiteCtrl::XObjSafe::GetInterfaceSafetyOptions // Allows container to query what interfaces are safe for what. We're // optimizing significantly by ignoring which interface the caller is // asking for. HRESULT STDMETHODCALLTYPE CMyCtrl::XObjSafe::GetInterfaceSafetyOptions( /* [in] */ REFIID riid, /* [out] */ DWORD __RPC_FAR *pdwSupportedOptions, /* [out] */ DWORD __RPC_FAR *pdwEnabledOptions) { METHOD_PROLOGUE(CMyCtrl, ObjSafe) HRESULT retval = ResultFromScode(S_OK); // does interface exist? IUnknown FAR* punkInterface; retval = pThis->ExternalQueryInterface(&riid, (void * *)&punkInterface); if (retval != E_NOINTERFACE) { // interface exists punkInterface->Release(); // release it--just checking! } // we support both kinds of safety and have always both set, // regardless of interface *pdwSupportedOptions = *pdwEnabledOptions = dwSupportedBits; return retval; // E_NOINTERFACE if QI failed } / // CStopLiteCtrl::XObjSafe::SetInterfaceSafetyOptions // Since we're always safe, this is a no-brainer--but we do check to make // sure the interface requested exists and that the options we're asked to // set exist and are set on (we don't support unsafe mode). HRESULT STDMETHODCALLTYPE CMyCtrl::XObjSafe::SetInterfaceSafetyOptions( /* [in] */ REFIID riid, /* [in] */ DWORD dwOptionSetMask, /* [in] */ DWORD dwEnabledOptions) { METHOD_PROLOGUE(CMyCtrl, ObjSafe) // does interface exist? IUnknown FAR* punkInterface; pThis->ExternalQueryInterface(&riid, (void * *)&punkInterface); if (punkInterface) { // interface exists punkInterface->Release(); // release it--just checking! } else { // interface doesn't exist return ResultFromScode(E_NOINTERFACE); } // can't set bits we don't support if (dwOptionSetMask & dwNotSupportedBits) { return ResultFromScode(E_FAIL); } // can't set bits we do support to zero dwEnabledOptions &= dwSupportedBits; // (we already know there are no extra bits in mask ) if ((dwOptionSetMask & dwEnabledOptions) != dwOptionSetMask) { return ResultFromScode(E_FAIL); } // don't need to change anything since we're always safe return ResultFromScode(S_OK); } 二. ATL 在控件类加一个继承接口即可 public IObjectSafetyImpl<CAtlCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA> 方法二:使用组件分组管理器 前面提到,IE通过检测注册表决定一个控件是否是可以安全性初始化和脚本操作的。IE通过调用 ICatInformation::IsClassOfCategories 方法决定是否控件支持给出的安全性分组。 如果一个控件使用组件分组管理器将自己注册为安全的,该控件的注册表入口就包含一个实现的分组关键字,该关键字含有一个或者两个子键。一个子键设置控件支持安全性初始化,另一个设置支持安全性脚本操作。安全性初始化子键依赖于 CATID_SafeForInitializing; 安全性脚本操作子键依赖于 CATID_SafeForScripting。(其他组件分组子键定义在 Comcat.h 文件,而安全性初始化和脚本操作子键定义在 Objsafe.h 文件。) 下列演示显示了一个注册表入口(Tabular Data Control),该ActiveX控件同IE绑定支持创建数据驱动的网页。因为控件是可以安全性脚本操作和初始化的,注册表中将其标记为安全性脚本操作 (7DD95801-9882-11CF-9FA9-00AA006C42C4) 和安全性初始化 (7DD95802-9882-11CF-9FA9-00AA006C42C4)。 注意,这两个GUID值是固定的。 将一个控件注册为安全的 系统注册表含有一个组件分组键来罗列每一个安装在系统中的组件的功能性分组。下面演示了一个组件分组键。假设 CATID_SafeForScripting (7DD95801-9882-11CF-9FA9-00AA006C42C4) 和 CATID_SafeForInitializing (7DD95802-9882-11CF-9FA9-00AA006C42C4) 在列表之中。 要创建一个组件分组的子键,你的控件必须包含以下步骤: 1.创建一个组件分组管理器(Component Categories Manager)实例来接收 ICatRegister 接口的地址。 2.设置正确的 CATEGORYINFO 结构分量。 3.调用 ICatRegister::RegisterCategories 方法,将初始化的 CATEGORYINFO 结构变量传递给这个方法。 下面的例子演示如何使用这些步骤来完成一个名称为 CreateComponentCategory全局函数,生成组件分组。 #include "comcat.h" HRESULT CreateComponentCategory(CATID catid,WCHAR* catDescription) { ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; //创建一个组件管理器实例(进程内) hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (FAILED(hr)) return hr; // 确信 HKCR\Component Categories\{..catid...} 键已经被注册 CATEGORYINFO catinfo; catinfo.catid = catid; catinfo.lcid = 0x0409 ; // 英语 // 确信提供的描述在127个字符以内 int len = wcslen(catDescription); if (len>127) len = 127; wcsncpy(catinfo.szDescription,catDescription,len); // 确信描述使用'\0'结束 catinfo.szDescription[len] = '\0'; hr = pcr->RegisterCategories(1,&catinfo); pcr->Release(); return hr; } 当一个子键被创建到需要的分组,控件应该注册到该分组,需要以下步骤: 1.创建一个组件分组管理器实例接收 ICatRegister 接口地址。 2.调用 ICatRegister::RegisterClassImplCategories 方法,将控件的 CLSID 和需要的 category ID 作为参数传递给函数。 下面的例子演示如何将一个名称为 RegisterCLSIDInCategory 加入实例控件。 #include "comcat.h" HRESULT RegisterCLSIDInCategory(REFCLSID clsid,CATID catid) { // 注册你的组件分组信息 ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (SUCCEEDED(hr)) { // 注册已实现的类到分组 CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->RegisterClassImplCategories(clsid,1,rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } 一个控件应该在调用 DLLRegisterServer 函数是注册安全性初始化和脚本操作。(DLLRegisterServer 由组件对象模型 [COM] 调用创建注册表入口) 在实例组件中 DLLRegisterServer 函数调用了 CreateComponentCategory 和 RegisterCLSIDInCategory 函数 (它们保证控件的安全性初始化和脚本操作)。下面的就是 DLLRegisterServer 的实现。 STDAPI DllRegisterServer(void) { HRESULT hr; // return for safety functions AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(),_tlid)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE)) return ResultFromScode(SELFREG_E_CLASS); RegisterTwo(CLSID_SafeItem); return NOERROR; } HRESULT RegisterTwo(REFCLSID clsid) { // 注册控件是安全性初始化的 / HRESULT hr; hr = CreateComponentCategory(CATID_SafeForInitializing, L"Controls safely initializable from persistent data!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(clsid, CATID_SafeForInitializing); if (FAILED(hr)) return hr; // 注册控件是安全性脚本操作的 hr = CreateComponentCategory(CATID_SafeForScripting, L"Controls safely scriptable!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(clsid, CATID_SafeForScripting); if (FAILED(hr)) return hr; return S_OK; } 作为一个创建所有安全性分组入口到注册表的控件,它也应该负责卸载所有的分组信息。COM 调用控件的 DLLUnRegisterServer 函数删除相应的注册表入口然后卸载该控件。 要卸载一个安全性初始化和脚本操作控件,控件应该完成以下任务: 1 创建一个组件分类管理器实例接收 ICatRegister 接口地址。 2 调用 ICatRegister::UnRegisterClassImplCategories 方法,将控件的 CLSID 和必要的 category ID 作为参数传递 下面的例子演示如何完成一个 UnRegisterCLSIDInCategory 。 HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid,CATID catid) { ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (SUCCEEDED(hr)) { // 从分组卸载组件 CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->UnRegisterClassImplCategories(clsid,1,rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } 我们前面提过,一个控件负责删除安全性初始化和脚本操作入口,下面演示如何完成这两个步骤: STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE)) return ResultFromScode(SELFREG_E_CLASS); UnRegisterTwo(CLSID_SafeItem); return NOERROR; } HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid,CATID catid) { ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (SUCCEEDED(hr)) { // 从分组卸载组件 CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } 附件, CXFMARK.h 以上函数形成一个文件, #include "comcat.h" const GUID CATID_SafeForInitializing = {0x7DD95802,0x9882,0x11CF,{0x9F,0xA9,0x00,0xAA,0x00,0x6C,0x42,0xC4}}; const GUID CATID_SafeForScripting = {0x7DD95801,0x9882,0x11CF,{0x9F,0xA9,0x00,0xAA,0x00,0x6C,0x42,0xC4}}; HRESULT CreateComponentCategory(CATID catid,WCHAR* catDescription) { ICatRegister* pcr = NULL ; HRESULT hr = S_OK; //创建一个组件管理器实例(进程内) hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (FAILED(hr)) return hr; // 确信 HKCR\Component Categories\{..catid...} 键已经被注册 CATEGORYINFO catinfo; catinfo.catid = catid; catinfo.lcid = 0x0409 ; // 英语 // 确信提供的描述在127个字符以内 int len = wcslen(catDescription); if (len>127) len = 127; wcsncpy(catinfo.szDescription,catDescription,len); // 确信描述使用'\0'结束 catinfo.szDescription[len] = '\0'; hr = pcr->RegisterCategories(1,&catinfo); pcr->Release(); return hr; } HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid) { // 注册你的组件分组信息 ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (SUCCEEDED(hr)) { // 注册已实现的类到分组 CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->RegisterClassImplCategories(clsid,1,rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid,CATID catid) { ICatRegister* pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr); if (SUCCEEDED(hr)) { // 从分组卸载组件 CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } //下面来两个函数分别在DllRegisterServer和DllUnregisterServer中使用,参数均为控件的GUID。 HRESULT RegisterTwo(REFCLSID clsid)// 注意clsid就是控件的GUID { // 注册控件是安全性初始化的 / HRESULT hr; hr = CreateComponentCategory(CATID_SafeForInitializing, L"Controls safely initializable from persistent data!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(clsid, CATID_SafeForInitializing); if (FAILED(hr)) return hr; // 注册控件是安全性脚本操作的 hr = CreateComponentCategory(CATID_SafeForScripting, L"Controls safely scriptable!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(clsid, CATID_SafeForScripting); if (FAILED(hr)) return hr; return S_OK; } HRESULT UnRegisterTwo(REFCLSID clsid) // 注意clsid就是控件的GUID { // 删除注册表入口 HRESULT hr; hr = UnRegisterCLSIDInCategory(clsid, CATID_SafeForInitializing); if (FAILED(hr)) return hr; //REFCLSID hr = UnRegisterCLSIDInCategory(clsid, CATID_SafeForScripting); if (FAILED(hr)) return hr; return S_OK; }