OpenSC的结构、扩展及调试环境搭建
发布时间:2010-2-12 11:36
分类名称:Private
这是我们在向OpenSC添加ePass3000支持的时候,阅读代码,调试和提交补丁的一点体会,与大家共享。
OpenSC的代码是C的,虽然它里面很多地方体现了面向对象的思想,但是使用rose的类图和时序图表现其逻辑结构和行为仍然有一定困难。所以文中图表 无法做的非常精确,例如C中没有继承,而OpenSC中使用union表示有共有属性的结构,我在图里让这些结构继承自void.又例如在时序图中,可能 函数参数中有好几个对象指针我们无法判断是谁主导,就随便挑了一个。
(这个图从网上找的,不知其版权)
![OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace](pic/img241.ph.126.net_Ykpbaf-vnWSXag9D8g4beg==_2202260217785319281.png)
(这个图是咱们自己画的,简化了一些UML符号,有一些无关紧要的属性没有画,同时不敢保证对代码理解100%正确)
我们可以从数据结构图中看出,OpenSC大体上可以分成3层,他们分别以sc_card_t,sc_pkcs15_card_t和sc_profile 为关键数据结构。
所有的操作流程都需要维持以下不变性:
scconf_context以树形结构记录了OpenSC的配置.
读卡器驱动(sc_reader_dirver)实现读卡器通讯协议,如 pcsc,OpenCT或CT-
API. 卡片驱动(sc_card_driver)实现卡片的APDU层协议,相当于我 们NG里的TSP,并知道它支持哪些卡片(ATR),以及这些ATR各自支持的算法.
sc_context_t维护了所有OpenSC知道的读卡器驱动列表,并维护 了所有它所知道的卡片驱动.
当创建sc_card_t类型变量时,sc_context_t里的各个读卡器 驱动监控到卡片插入,并根据ATR载入它对应的卡片驱动.
sc_pkcs15_card维护了所有p15对象.
sc_profile维护了卡片的配置信息,如各个DF和EF多大,它们的权 限,它们的fid。sc_profile也维护了PIN的缓存。
数据的一致性由应用自己来保证:
可以锁定卡来防止其它进程对卡片的访问。
可以在进行一项事务时,先bind,使缓存重新载入。
可以使用pc文件系统进行缓存。
所谓COS状态对于我们卡片来说分两种:
当前处在哪个目录。在处理某个事务时,用户要先锁定卡,在卡锁定期间,其他进程 不能对卡进行访问。锁定后,OpenSC会缓存当前路径(由各个厂商的card driver实现),路径缓存在卡解锁后失效。有效的减少了选择文件的次数。
当前安全状态机的值。安全状态不缓存,但缓存PIN。每次进行需要权限的操作 时,如果是会话中的第一次,会先去验证PIN,成功后将其缓存,要求应用保证这一点。之后对于会改变卡片数据的操作(如写文件等),先从缓存里取出PIN 去卡上验证,然后再进行其它操作。对卡片无影响的操作(如解密和签名)则先进行操作,如果卡片汇报权限不满足,则在从缓存中取PIN验证。
有两种方法可以让应用更新PIN缓存:
使用OpenSC的缓存。提供sc_keycache_put_XXX函数让应 用主动设置PIN缓存数据。
使用应用的缓存。让用户注册回调函数,使OpenSC需要缓存的PIN时能调用 回调函数,取得PIN.
OpenSC的权限分成几种,其中最重要的是read和update,对应于读和更新(注意不是写,OpenSC里的写和更新是有区别的)。 每个权限用ACL来控制,ACL也分成几种类别,分别是never,always和chv。
使用grep在源文件中查找:
$grep what_you_want directory -rn --include="*.h" --include="*.c"
其中:
-r 如果是目录则递归查找。
-n 找到后打印行号,否则默认只打印涉及到的行内容。
--include=something 在文件名符合something的文件中查找。
如果你习惯使用emacs并安装了cscope的话,就容易了,将光标移动到要找的名字上,
按Ctl-c s g找到定义,相当于vc里的F12。
按Ctl-c s c找到调用当前函数的调用者。
按Ctl-c s s找到所有涉及到名字的源代码处,与grep相同。
按Ctl-c s i打开#include语句中的包含文件。
OpenSC采用autotools对项目进行管理,编译时采用以下步骤:
在源代码trunk目录运行:
$./bootstrap
生成configure脚本。
运行:
$CFLAGS="-g -O0" ./configure --enable-openssl --enable-pcsc
生成Makefile。
因为我们要调试所以要加上-g选项,使二进制代码中带上调试信息.
使用-O0(欧零)选项禁止编译器进行代码优化.
我们代码里用openssl计算了MAC和加密apdu,所以要把 openssl支持加上.
我们的卡通过pcsc-lite进行访问,要把pcsc支持加上.
运行:
$make
编译生成二进制文件。生成的文件在源代码各自目录的.libs目录下,我们不用运行make install,而是直接在这些目录下运行。
因为系统中已经安装了OpenSC在/usr/lib下,所以你需要使应用程序优先使用你编译的OpenSC动态库,覆盖系统库是一个办法,更好的办法是 设置环境变量,让应用程序优先使用咱们自己的动态库,这个环境变量叫做LD_LIBRARY_PATH:
$export LD_LIBRARY_PATH=/home/dave/devel/work/opensc/trunk/src/libopensc/.libs/:\
/home/dave/devel/work/opensc/trunk/src/pkcs15init/.libs/:\
/home/dave/devel/work/opensc/trunk/src/pkcs11/.libs/:\
$LD_LIBRARY_PATH
注意一定要有export关键字,他会让你设置的新环境变量传递给当前shell的子进程。
OpenSC从环境变量中读取OpenSC的配置文件路径,所以我们要设置自己的确保运行时OpenSC能找到:
$export OPENSC_CONF=/etc/opensc/opensc.conf
可以把上面的几个语句放到一个Shell脚本如opensc_debug_env.sh中,要调试时运行一下。
注意:运行时不能使用
$./opensc_debug_env.sh
或者
$sh ./opensc_debug_env.sh
或
$bash ./opensc_debug_env.sh
否则的话当前命令控制台会启动一个新的进程去执行脚本,而不是在当前进程中执行shell语句,而之后我们要调试的程序是当前进程的子进程,要把当前进程 export的环境变量传递给要调试的进程如firefox,所以要使用
$source ./opensc_debug_env.sh
或
$. ./opensc_debug_env.sh
让shell语句在当前进程中执行。
使用gdb excuteble_file来调试,调试firefox申请证书时也可以使用firefox -g. 启动后:
set args 设置命令行参数
show args 显示命令行参数
r 从头开始运行被调试程序,相当于vc里的F5.
next 下一步相当于vc里的F10,可以缩写为n
step 相当于vc里的F11,可以缩写为s
continue 继续执行,可以缩写为c
finish 执行到函数结尾
until 执行到循环结尾,可以缩写为u
break 在当前行设置断点,可以缩写为b
break filename:linenum, 可以缩写为 b filename:linenum
break linenum在当前源文件的linenum行设置断点,可以缩写为 b linenum
break functionname在函数functionname入口处设置断点,可以缩写为 b functionname
delete break breaknum删除断点号为breaknum的断点,可以缩写为 d b breaknum
delete break删除所有断点,可以缩写为 d b
backtrace查看函数调用栈,可以缩写为bt
list列出源文件。
print var显示名字为var的变量值,简写p var
p /x var十六进制显示
p *var@n 显示以地址var开始的n个值
p var=something 设置变量var的值为something
直接打回车重复上一条命令。
因为不是所有的开源项目都用统一的cvs或svn,我们在这里介绍一种通用的生成源代码补丁和打补丁的方法。 首先将原始源代码放入一个目录中,例如~/opensc里,将修改过的代码放入同一级目录中,例如~ /opensc_with_ePass3000_support中。 在~/目录下运行:
$diff -urN -x .svn opensc opensc_with_ePass3000_support > ePass3000_support.diff
其中:
之后会在当前目录~/下生成一个名为ePass3000_support.diff的diff文件,里面的内容类似于:
diff -urN -x .svn -x cscope.out opensc/trunk/src/libopensc/cardctl.h opensc_with_ePass3000_support/trunk/src/libopensc/cardctl.h
--- opensc/trunk/src/libopensc/cardctl.h 2008-08-15 10:04:06.000000000 +0800
+++ opensc_with_ePass3000_support/trunk/src/libopensc/cardctl.h 2008-08-15 09:54:41.000000000 +0800
@@ -156,7 +156,17 @@
SC_CARDCTL_RUTOKEN_GOST_ENCIPHER,
SC_CARDCTL_RUTOKEN_GOST_DECIPHER,
SC_CARDCTL_RUTOKEN_FORMAT_INIT,
- SC_CARDCTL_RUTOKEN_FORMAT_END
+ SC_CARDCTL_RUTOKEN_FORMAT_END,
+
+ /*
+ * EnterSafe specific calls
+ */
+ SC_CARDCTL_ENTERSAFE_BASE = _CTL_PREFIX('E', 'S', 'F'),
+ SC_CARDCTL_ENTERSAFE_CREATE_FILE,
+ SC_CARDCTL_ENTERSAFE_CREATE_END,
+ SC_CARDCTL_ENTERSAFE_WRITE_KEY,
+ SC_CARDCTL_ENTERSAFE_GENERATE_KEY,
+ SC_CARDCTL_ENTERSAFE_PREINSTALL_KEYS,
};
其中:
以”---“开头的是原始文件的路径及时间戳。
以”+++“开头的是修改后文件的路径及时间戳。
以”@@“开头的是原始文件和修改后的文件有差异地方的位置。
以”-“开头的行是原始文件的内容,在修改后的文件中要把它删掉。
以”+“开头的行是修改后文件中新加的内容。
其它开头的行不起实质作用,可以让代码审查员了解修改处的上下文。
$cd opensc
$patch -p1 < ePass3000_support.diff
其中:
首先要进入修改前的代码路径,我们这里是~/opensc, 然后用带-p1参数的patch命令应用补丁,为什么要加参数p1呢? 我们注意到在diff文件里有这么一行:
--- opensc/trunk/src/libopensc/cardctl.h 2008-08-15 10:04:06.000000000 +0800
+++ opensc_with_ePass3000_support/trunk/src/libopensc/cardctl.h 2008-08-15 09:54:41.000000000 +0800
我们现在在目录opensc里,所以需要把补丁里的路径中第一个斜杠前面的内容切掉(p1切一个,p2切2个,pN切N个斜杠),使修改前和修改后的文件 路径都变成trunk/src/libopensc/cardctl.h,这样patch程序就知道从当前路径开始修改哪个文件了。
这是一种通用的生成补丁和打补丁的方式,不管项目的源代码是用svn还是cvs甚至ftp管理的,这种方法都可以用。
将生成的diff文件以附件的形式发送到opensc的dev邮件列表中,这时 我们发的补丁文件可以被所有的opensc的开发人员以及一些对opensc研发感兴趣的用户看到。
所有感兴趣的邮件列表用户会阅读我们的源代码补丁,提出修改意见或要求你解释某 一段程序为什么要这样写(改)。
我们收集所有人的意见,需要解释的地方做出解释,需要修改的地方对我们的代码进 行修改,并把改后的代码再做一个补丁提交。
重复1-3,直到所有人的意见都被处理
邮件列表里有代码库提交权限的opensc开发人员会使用我们上面介绍的应用补 丁的方法,把补丁打到他从svn上update下来的代码中,并提交.
提交后svn服务器会自动向opensc的commit邮件列表的所有用户发一 封邮件,说明是谁提交了新代码,作了什么修改.这样我们的代码就进入了官方的代码库.
这时,有兴趣的用户或linux发布版(如debian或红帽)开发者会从svn里check下新代码,编译,然后放到自己的linux发布版中.用户安 装这些linux发布版时,我们打过补丁的opensc自动变为可用.
我们分析OpenSC的数据结构可以发现,OpenSC的代码结构是分层次的, 我们的中间件也是分了层次的。说明对软件结构的纵向划分(层次)和横向划分(模块)是行之有效的降低软件复杂度的方法。
虽然高内聚/低耦合是划分的一般依据,但开发人员对OpenSC的层次及导出接 口形式做了一个有意思的决策,它的接口都是强类型的。而一般C语言写的程序是弱类型的,以对象的handle作为对其它模块数据的引用。使用强类型暴露了 模块内部的具体内容,增加了划分之间的耦合度,给日后的扩展增加了难度,也对人们理解代码造成困难(我们可以看到OpenSC的数据结构图中的连线纵横交 错,我在读OpenSC代码时读的很是头疼),但这种方式程序运行效率较高。而使用弱类型隔离性好,容易扩展,也容易理解。但它减弱了编译器对代码正确性 检查的作用(任何类型的对象handle都是一种类型如unsigned long,如果传错了编译器检查不出来),需要对handle和内部数据作映射,所以效率较低。写代码时,小心选择接口形式是值得的,例如可以考虑部分接 口强类型,部分接口弱类型,在效律和可读性之间寻找一个平衡点。我觉得这是OpenSC做的不大好的地方。
OpenSC采用了树形配置文件,它的配置文件非常全面,几乎包含了任何可自定 义常量(如各个DF/EF的大小,权限和FID),我们的中间件中这些常量一般定义在头文件里,作定制时需要重新编译。OpenSC写了大量代码对配置文 件作分析,我们如果采用树形配置文件时完全可以使用XML格式,有很多不错的XML库可用。如果不想被非内部人员阅读或修改的话,可以对配置文件加密。
OpenSC的log功能完备而又使用简单,可以在配置文件中指定log输出级 别。我们的NG产品的log机制也很不错,但我们有的新产品对这方面却没怎么讲究。建议部门或整个公司对这些通用功能的机制如log和错误处理方法,做好 培训工作。
OpenSC使用C语言对C++的多态(虛函数)做了模拟。以APDU级驱动为 例,它以函数列表形式提供。OpenSC内置实现了ISO7816标准的卡驱动,卡片驱动开发者实现卡片对应独特的函数,与7816相同的函数指针置空。 当载入卡片驱动时,先将7816的function list拷贝过来,然后把卡片自己的function list中不为空的函数指针覆盖到拷贝过来的function list中。这样达成所有卡片驱动从7816继承过来的效果。
一个实现小细节:我们知道给卡片发命令有两个返回值,1个是发送函数的返回值, 表示指令是否成功发给卡片。另一个是cos返回值,表示COS执行指令的结果。在我们代码里有两种处理方法:1.在transmit内部函数中将cos返 回值消化(NG),将其转变成函数返回值,这样外部调用时不知道具体哪一步出了问题。另一种是返回两个返回值,其中一个使用指针返回 (Minidriver),这样会让看代码的人犯糊涂,怎么既有return又有*ret。OpenSC的sc_apdu_t结构中不止有 cla,ins,p1,p2,lc,data和le,它还有两个成员sw1,sw2对应于cos的返回值。这样apdu结构里的东西都是关于cos的,而 transmit辅助函数的返回值就是代表发送成功没有,看起来很清晰。我怎么没想到。。。
OpenSC的有些机制在本模块实现困难的时候大胆的交给客户程序去做(例如数 据一致性),因为是开源的,所以当客户程序感觉实现起来比在OpenSC里还困难时,应用程序开发者就会反过来修改OpenSC代码,将此机制做成补丁提 交给官方。这样就使库和客户程序的复杂度达到平衡,清晰而又简单。而我们的中间件是商业程序,客户的水平又参差不齐,所以我们的库会把能提供的机制尽量全 部提供,虽然难度增加了,但是给用户造成好的客户体验。
你来写
你来写
你来写