OpenSC的结构、扩展及调试环境搭建

发布时间:2010-2-12 11:36
分类名称:Private


这是我们在向OpenSC添加ePass3000支持的时候,阅读代码,调试和提交补丁的一点体会,与大家共享。

OpenSC的代码是C的,虽然它里面很多地方体现了面向对象的思想,但是使用rose的类图和时序图表现其逻辑结构和行为仍然有一定困难。所以文中图表 无法做的非常精确,例如C中没有继承,而OpenSC中使用union表示有共有属性的结构,我在图里让这些结构继承自void.又例如在时序图中,可能 函数参数中有好几个对象指针我们无法判断是谁主导,就随便挑了一个。

OpenSC的整体架构

(这个图从网上找的,不知其版权)

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

OpenSC几个重要数据结构及 其关系

(这个图是咱们自己画的,简化了一些UML符号,有一些无关紧要的属性没有画,同时不敢保证对代码理解100%正确)

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

我们可以从数据结构图中看出,OpenSC大体上可以分成3层,他们分别以sc_card_t,sc_pkcs15_card_t和sc_profile 为关键数据结构。

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace


运行时特征

所有的操作流程都需要维持以下不变性:

初始化及清理

sc_card_t

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

sc_pkcs15_card_t

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

sc_profile

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

格式化

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

生成密钥

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

导入密钥

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

删除密钥

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

数据签名

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

数据解密

OpenSC的结构、扩展及调试环境搭建 - Dsliu - Dspace

机制

进程间一致性

数据

数据的一致性由应用自己来保证:

  1. 可以锁定卡来防止其它进程对卡片的访问。
  2. 可以在进行一项事务时,先bind,使缓存重新载入。
  3. 可以使用pc文件系统进行缓存。

COS状态

所谓COS状态对于我们卡片来说分两种:

  1. 当前处在哪个目录。在处理某个事务时,用户要先锁定卡,在卡锁定期间,其他进程 不能对卡进行访问。锁定后,OpenSC会缓存当前路径(由各个厂商的card driver实现),路径缓存在卡解锁后失效。有效的减少了选择文件的次数。
  2. 当前安全状态机的值。安全状态不缓存,但缓存PIN。每次进行需要权限的操作 时,如果是会话中的第一次,会先去验证PIN,成功后将其缓存,要求应用保证这一点。之后对于会改变卡片数据的操作(如写文件等),先从缓存里取出PIN 去卡上验证,然后再进行其它操作。对卡片无影响的操作(如解密和签名)则先进行操作,如果卡片汇报权限不满足,则在从缓存中取PIN验证。

PIN缓存

有两种方法可以让应用更新PIN缓存:

  1. 使用OpenSC的缓存。提供sc_keycache_put_XXX函数让应 用主动设置PIN缓存数据。
  2. 使用应用的缓存。让用户注册回调函数,使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的话,就容易了,将光标移动到要找的名字上,

编译

OpenSC采用autotools对项目进行管理,编译时采用以下步骤:

在源代码trunk目录运行:

$./bootstrap

生成configure脚本。

运行:

$CFLAGS="-g -O0" ./configure --enable-openssl --enable-pcsc

生成Makefile。

运行:

$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. 启动后:

直接打回车重复上一条命令。

补丁

如何生成源代码补丁

因为不是所有的开源项目都用统一的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管理的,这种方法都可以用。

提交流程

  1. 将生成的diff文件以附件的形式发送到opensc的dev邮件列表中,这时 我们发的补丁文件可以被所有的opensc的开发人员以及一些对opensc研发感兴趣的用户看到。
  2. 所有感兴趣的邮件列表用户会阅读我们的源代码补丁,提出修改意见或要求你解释某 一段程序为什么要这样写(改)。
  3. 我们收集所有人的意见,需要解释的地方做出解释,需要修改的地方对我们的代码进 行修改,并把改后的代码再做一个补丁提交。
  4. 重复1-3,直到所有人的意见都被处理
  5. 邮件列表里有代码库提交权限的opensc开发人员会使用我们上面介绍的应用补 丁的方法,把补丁打到他从svn上update下来的代码中,并提交.
  6. 提交后svn服务器会自动向opensc的commit邮件列表的所有用户发一 封邮件,说明是谁提交了新代码,作了什么修改.这样我们的代码就进入了官方的代码库.

这时,有兴趣的用户或linux发布版(如debian或红帽)开发者会从svn里check下新代码,编译,然后放到自己的linux发布版中.用户安 装这些linux发布版时,我们打过补丁的opensc自动变为可用.

我们可以从OpenSC代码 里借鉴到什么

  1. 我们分析OpenSC的数据结构可以发现,OpenSC的代码结构是分层次的, 我们的中间件也是分了层次的。说明对软件结构的纵向划分(层次)和横向划分(模块)是行之有效的降低软件复杂度的方法。
  2. 虽然高内聚/低耦合是划分的一般依据,但开发人员对OpenSC的层次及导出接 口形式做了一个有意思的决策,它的接口都是强类型的。而一般C语言写的程序是弱类型的,以对象的handle作为对其它模块数据的引用。使用强类型暴露了 模块内部的具体内容,增加了划分之间的耦合度,给日后的扩展增加了难度,也对人们理解代码造成困难(我们可以看到OpenSC的数据结构图中的连线纵横交 错,我在读OpenSC代码时读的很是头疼),但这种方式程序运行效率较高。而使用弱类型隔离性好,容易扩展,也容易理解。但它减弱了编译器对代码正确性 检查的作用(任何类型的对象handle都是一种类型如unsigned long,如果传错了编译器检查不出来),需要对handle和内部数据作映射,所以效率较低。写代码时,小心选择接口形式是值得的,例如可以考虑部分接 口强类型,部分接口弱类型,在效律和可读性之间寻找一个平衡点。我觉得这是OpenSC做的不大好的地方。
  3. OpenSC采用了树形配置文件,它的配置文件非常全面,几乎包含了任何可自定 义常量(如各个DF/EF的大小,权限和FID),我们的中间件中这些常量一般定义在头文件里,作定制时需要重新编译。OpenSC写了大量代码对配置文 件作分析,我们如果采用树形配置文件时完全可以使用XML格式,有很多不错的XML库可用。如果不想被非内部人员阅读或修改的话,可以对配置文件加密。
  4. OpenSC的log功能完备而又使用简单,可以在配置文件中指定log输出级 别。我们的NG产品的log机制也很不错,但我们有的新产品对这方面却没怎么讲究。建议部门或整个公司对这些通用功能的机制如log和错误处理方法,做好 培训工作。
  5. OpenSC使用C语言对C++的多态(虛函数)做了模拟。以APDU级驱动为 例,它以函数列表形式提供。OpenSC内置实现了ISO7816标准的卡驱动,卡片驱动开发者实现卡片对应独特的函数,与7816相同的函数指针置空。 当载入卡片驱动时,先将7816的function list拷贝过来,然后把卡片自己的function list中不为空的函数指针覆盖到拷贝过来的function list中。这样达成所有卡片驱动从7816继承过来的效果。
  6. 一个实现小细节:我们知道给卡片发命令有两个返回值,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辅助函数的返回值就是代表发送成功没有,看起来很清晰。我怎么没想到。。。
  7. OpenSC的有些机制在本模块实现困难的时候大胆的交给客户程序去做(例如数 据一致性),因为是开源的,所以当客户程序感觉实现起来比在OpenSC里还困难时,应用程序开发者就会反过来修改OpenSC代码,将此机制做成补丁提 交给官方。这样就使库和客户程序的复杂度达到平衡,清晰而又简单。而我们的中间件是商业程序,客户的水平又参差不齐,所以我们的库会把能提供的机制尽量全 部提供,虽然难度增加了,但是给用户造成好的客户体验。
  8. 你来写
  9. 你来写
  10. 你来写