In general, we don’t prevent things unless there is a good reason for that. Put another way, we try to allow anything that doesn’t cause a security problem.
David Wooten,
During an e-mail exchange about context management
相对于TPM大量的功能来说,它的内存是非常受限的,这主要是为了降低成本。这就意味着对象,会话,以及中间序列必须在必要的时候换入换出TPM设备。这很大程度上和虚拟内存管理器将内存页在磁盘中换入换出一样。这两种情况中,应用程序都会认为它们能够访问比实际情况更多的对象和会话(TPM),或者更多的内存(虚拟内存的情况)。
如果一个系统上确定只有一个应用会向TPM发送命令,那这些交换操作可以由应用本身来完成。但是,当同时有多个应用或者进程在访问TPM时,就需要TSS软件栈的两个组件:TPM访问中介(TPM Access Broker)和资源管理器(Resource Manager)。
这一章描述了TAB和RM的高层次架构。然后介绍TPM用于支持对象,会话,序列交换的特性和命令。同时还有相关命令是怎样处理交换实体的细节和特殊情况。
TAB和RM首先是在第7章中作为TSS的软件层来描述的。这一节会提供一些TAB和RM内部细节。
TAB和RM将TPM应用、进程 与 仲裁多进程访问TPM和对象、会话、序列在必要时换入换出TPM的繁杂事务隔离开来。TAB和RM关系非常紧密,并且通常会被集成到相同的软件模块中。根据系统设计具体情况,它们可能是TPM设备驱动的最上面一层软件,或者被集成为一个独立守护进程,进程处于TSS SAPI之下以及TPM设备驱动之上。
TAB的职责非常简单:仲裁多进程同时访问TPM。最小的要求是,必须保证所有进程在命令数据流开始发送到TPM直到从TPM接收命令响应数据的时间段内,没有其他的进程与TPM通信。下面是在没有TAB的时候,一些多进程访问TPM冲突的例子:
- 进程A的命令数据开始被传送到TPM,在传送完成之前,进程B的命令数据开始发送。这样一来,发送到TPM的数据就是两个命令的混合,并且TPM很可能会返回一个错误代码。
- 进程A发送了命令,但是进程B读取了这个命令的响应数据。
- 进程A的命令数据已经被发送到TPM中,然后当A读取命令响应数据时,进程B开始发送命令数据。
- 进程A创建并加载一个只能它自己使用的秘钥。进程B保存了这个秘钥的上下文(使用TPM2_ContextSave命令),之后在某个时间加载这个秘钥的上下文(使用TPM2_ContextLoad命令)并使用开始使用这个秘钥。
一个TAB可以用以下两种方式实现:使用TPM锁或者不使用TPM锁。
在带锁的架构中,可以设计一种通过软件向TAB发送锁请求的机制。这个请求会指示TAB,在发送锁请求的进程完成命令之前不允许别的进程访问TPM。这个机制就保证了TPM在独立命令不被中断的情况下完成多个命令的处理。有趣的是,如果应用能够自己实现资源管理,这种架构将不需要RM。应用可以首先向TAB请求锁,然后在向TPM发送命令的同时管理TPM的上下文(这个管理包括在释放锁之前清理自己占用的资源,比如删除对象,序列,和会话)。这种架构的缺点(Achilles heel 艾吉利斯的脚后跟(来自希腊神话))是,这个应用可能在释放锁时失败,这将阻塞所有其他应用。当然可以通过设置超时来缓解这个问题,但是超时又会造成对一个应用独占TPM时间的限制(有些操作可能时间就是很长,这时候超时的设置就麻烦了)。结果就是,应用不得不处理一些关于TPM上下文的边边角角的复杂管理任务,因为设定超时之后,应用不能保证超时到来时TPM是出错还是正在执行命令,如果因为遇到执行很长时间的命令,中间因超时返回,应用可能需要重新发送命令等等。TCG TSS工作组刚开始想用这个方案,但是最终在微软的Paul Englang给出一些建设性建议之后否决了。
无锁架构相对来说更简单,它允许任何进程在不受其他进程访问TPM的情况下,独占地发送TPM命令和接收命令响应。这种机制减少了应用程序独占访问TPM的时间和进程发送命令、接收命令响应占用的时间。这个架构需要背后有RM的支撑,因为多个相互竞争的应用不可能相互管理它们的对象、会话和序列。比如:
- 一段时间内进程A恰好是唯一访问TPM的进程,在此期间它启动了三个会话并加载了三个秘钥。
- 进程B准备好与TPM通信并且想要创建一个会话和一个秘钥。如果TPM只有三个用于会话的内存槽位,和三个用于对象的内存槽位,进程B必须首先清除进程A至少一个会话和一个秘钥对象。
- 这就要求进程B能够管理进程A的状态——从进程隔离和软件复杂度的角度考虑,这不太可能。没有一个中心化的RM,应用程序必须管理所有的TPM上下文。这几乎不可能,而且这确定无疑地允许应用之间的TPM上下文混合在一起。
因此,这种情况下需要一个RM。
RM负责处理对象、会话、和序列换入换出TPM的细节,这个处理对应用来说是透明的。在高度嵌入式的环境中,单用户应用可能会选择自己处理这些人物,但是之前我们也讨论过,大多数系统是多用户的,并且需要RM的存在。作为类比,想象一下如果所有的PC应用程序必须自己将虚拟内存换入换出硬盘,而不是依赖操作系统来做这件事情。RM在处理TPM的访问时与上述例子中操作系统的角色类似。
在之前的学习中有如下显而易见的事实:如果一个命令要使用临时性的实体,这个实体必须首先加载到TPM内存中。这就意味着RM必须在命令发送到TPM之前解析命令数据流,并且做出合适的动作以确保命令使用的临时性对象都被加载到TPM中。这些对象包括授权区域引用的所有会话,所有命令handle区域对应的对象,会话,以及序列。
RM需要做的基本操作总结如下:
- 将所有发送和接收的对象和序列的handle虚拟化。因为需要用到的handle很可能比TPM内存中可以容纳的对象数量要多,因此它们必须被虚拟化(这个跟虚拟内存的机制及其优点(统一地址应用层的地址空间等)非常相似)。
- 维护用于跟踪对象和序列上下文的表。
- 维护一个已加载到TPM的对象和序列的虚拟到TPMhandle的映射。
- 对于要发送到TPM的命令:
- 所有命令在发送到TPM之前捕获它们的字节流。
- 检查授权区域或者handle区域中的所有handle。如果这些handle代表对象,序列,或者会话,确保这些handle已保存的上下文重新被加载到TPM中,这样命令才能成功执行。其中对于对象和序列,在命令被发送到TPM之前,还需要将命令字节流中的虚拟handle替换成加载上下文后返回的真实的handle。
注意:会话的handle不需要被虚拟化,因为会话的整个生命周期内它们都是固定的:也就是说,当会话被重新加载后,它的handle与之前相同。我们稍后再介绍原因。现在,只要明白会话与对象和序列的不同就足够了,对象和序列在重新加载后handle可能会发生变化,但是会话的handle不会变化。
- 对于从TPM接收到的命令响应:
- 在将命令响应返回到上层软件之前截获它们。
- 虚拟化命令响应中的对象或者序列的handle,并且将TPM返回的命令响应数据流中的真实handle替换成虚拟化的handle。
- RM还必须主动积极保证命令永远不会因为TPM的内存不够而失败,或者当需要上下文不能被加载到TPM时被动地解决(比如暂时将不用的上下文清除出TPM)。
- 有两种方式来实现上述的要求:
- 最简单的积极主动的方式是,RM上层软件每次完成TPM命令执行之后,RM都会保存所有的对象,会话,以及序列的上下文,然后将它们从TPM内存中移除。从根本上来讲,这个方法是相对更简单的一种,但是它需要向TPM发送更多的命令。比如说,当一个对象命令加载完成之后,即使下一个命令会立即使用它,RM也会先把它清除出TPM然后重新加载上下文。
- 第二种,被动的方式是在执行命令之前,先检查命令的handle以及会话区域,找出必须要加载到TPM中的对象,序列和会话。然后从TPM中清除足够多的已经加载的对象,序列以及会话,从而保证当先命令需要的对象,序列以及会话能够成功加载到TPM中。然后加载需要加载的内容。
注意:本章稍后我们将讨论由硬件触发的命令——_TPM_Hash_Start。这个命令会隐式地,清除一个对象或者会话,这对RM来说是透明的。这就对上述第二种RM的实现方式有特殊的要求。
- 当命令因为TPM内存不足返回错误代码而失败时,上述第二种RM会被动地做出响应。当RM收到这样的错误代码时,它必须清除(换出)对象,会话,或者序列,知道TPM有足够的空间加载当前命令需要的对象,会话和序列。然后重新执行失败的命令。
注意:以下是实践中得出的经验:这种被动应对的方式实现起来很难,并且调试更难。我强烈建议实现第一种方式。我曾经尝试过被动的实现方式,
但是结果并不理想,结果就是重新编码。(译注:不知道当前的TPM2-TABRM是用什么样的方式实现的?)
- RM必须小心恰当地处理复位事件时对象,序列和会话上下文的handle。
以上的介绍已经涵盖了RM的基本要求。其他需要处理的边界情况和一些神秘难懂的功能在“TSS TAB and Resource Manager Specification”中有详细介绍。
现在让我们了解一下支持RM的TPM特性吧。
因为TPM没存有限,所以对象,会话,以及序列需要动态地换入换出TPM。举例来说,微软的TPM模拟器作为TPM2.0的参考实现,它只允许有三个对象存储槽。一个对象存储槽就是用于存储一个对象或者序列的TPM内存区域。还有三个会话存储槽位。因此,虚拟化临时性实体(这里的临时性实体就是指对象,会话(这里先暂时忽略前面说的不虚拟化会话),和序列)是必要的。本小节描述了用于实现虚拟化上述实体的TPM功能和命令。
TPM用于管理临时实体上下文的能力是可以被查询的功能属性。这些功能包括特殊的错误代码,三个TPM命令,以及针对TPM2_Startup和TPM2_Shutdown需要做的特殊处理。应用开发者或者更理想的情况是RM会使用这些功能,从而实现临时实体的虚拟化和管理。
TPM将内部用于存储临时实体的内存由存储槽组成。用于对象和序列的最大存储槽数量是MAX_LOADED_OBJECTS。一个用于会话的类似定义是MAX_LOADED_SESSIONS。可以使用TPM2_GetCapability命令查询这两个定义的值。RM可以查询这两个定义的数值并将它们用于管理已经加载的上下文。或者它也可以依赖TPM返回的错误代码来管理资源。因为_TPM_Hash_Start相关的特殊情况,一些RM需要前述两种方式来实现资源管理。
特殊的错误代码用于指示RM TPM内存不足,意思是说没有多余的存储槽位了,RM必须做些什么:TPM_RC_OBJECT_MEMORY(对象和序列的内存空间不足),TPM_RC_SESSION_MEMORY(会话内存不足),或者TPM_RC_MEMORY(通用内存不足)。
需要使用对象,序列或者会话存储槽的TPM命令都有可能返回上述的错误代码。比如说TPM2_Load命令用于加载一个对象,如果没有足够的内存空间,它会返回TPM_RC_OBJECT_MEMORY或者TPM_RC_MEMORY。显式地使用对象或者序列存储槽的命令时TPM2_CreatePrimary,TPM2_Load,TPM2_LoadExternal,TPM2_HashSequenceStart,TPM2_HMAC_Start,以及TPM2_ContextLoad(前提是加载的上下文是对象或者序列,而非会话)。
另外,还有三个命令会隐式地使用对象存储槽。TPM2_Import使用一个存储槽用作便签内存;命令完成时会释放存储槽。类似的情况是,任何操作持续型handle的命令都会用一个存储槽作为便签内存,并在命令完成后释放存储槽。上述两种命令在没有足够的内存槽时都会返回之前介绍的错误代码。作为回应,RM必须清除一个临时实体并重试这个命令。
第三个会隐式地使用对象存储槽的命令有点奇怪:_TPM_Hash_Start。通常情况下这个命令会由硬件来触发(提前剧透一下:Intel TXT的GETSEC(Senter)和GETSEC(Enteraccs)会有这样的操作),并且如果内存不足时也不会返回错误代码。相反,它会清除一个对象,并且没有任何信息指明清除的是哪个对象。这就意味着RM或者应用程序最好确保,在任何时候硬件触发这个命令时,满足以下条件之一:
- 一个存储槽可用。RM可以使用TPM2_GetCapability命令查询MAX_LOADED_OBJECTS属性的值。RM可以根据响应清除一个对象。(最后的策略是_TPM_Hash_Start可以以某种方式通知RM,请求RM准备好空间)
- 所有当前占用存储槽的对象或者序列的上下文都曾经被保存过。否则,_TPM_Hash_Start命令清除的对象将无法重新加载。
- 所有当前占用存储槽的对象或者序列的上下文都不再被使用了。这时候,_TPM_Hash_Start命令清除的对象无法被重新加载也无所谓。(当然了,RM是无法知道当前的对象是否还有用,可能需要操作系统来出来这个情况)
显式地使用会话存储槽的命令是TPM2_StartAuthSession,TPM2_ContextLoad(当加载会话的上下文时),以及所有使用会话的命令(除了口令会话)。
用于管理临时性实体的TPM命令是TPM2_ContextSave,TPM2_ContextLoad,以及TPM2_FlushContext。这些命令会根据不同的临时实体产生不同的结果。
TPM2_ContextSave用于存储一个临时实体的上下文,并返回实体的上下文。这个命令返回的实体上下文是经过加密和完整性保护的。这是通过限制实体上下文只能被加载到同一个TPM上来实现的;也就是说一个实体的上下文不能被加载到其他的TPM中。需要特别提醒的是,将一个实体的上下文保存意味着上下文会保存到系统内存中,这个内存有可能是易失性的。因此如果遇到一些会擦出系统内存的事件时,如深度休眠(冬眠)和睡眠,需要一些其他机制来保护这些上下文。对于PC来说,对于休眠的情况,系统会将所有的内存信息保存到非易失性内存中,比如说硬盘中;对于一个睡眠事件,内存会继续保持上电状态,所以内存信息并没有消失。
实体的上下文被保存以后,如果实体是一个对象或者序列,那它还会驻留在TPM中,并且具有相同的handle。因此保存的上下文就是一份新的实体或者序列的拷贝。
但是对于会话来说,TPM的处理方式是不同的。当会话的上下文通过TPM2_ContextSave命令保存以后,会话会被清除出TPM内存。会话上下文的处理方式是非常独特的:上下文要么被清除出TPM,并存在于系统内存中,要么被加载到TPM中,但是这两种情况不能同时存在。不管会话的上下文存在于哪里,它总是有相同的handle,并且会话总是激活状态,直到上下文通过TPM2_FlushContext命令被清除,或者执行一个使用这个会话并且将continueSession标志位清除的TPM命令。
上述会话的特殊处理的目的是为了阻止一个会话多个拷贝的存在,因为一旦有多个会话的拷贝,就有可能发生针对会话的重放攻击。会话的上下文被保存之后只有一小部分会话的上下文还保留在TPM中。
对于对象,序列,以及会话来说,TPM2_FlushContext命令会清除所有临时实体的上下文。其中对于一个对象或者序列来说,一个对象——在执行TPM2_ContextSave之后对象仍然存在于TPM中——会被彻底地清除,但是它被保存的上下文仍可以被重新加载到TPM中。对于会话来说,上一段中提到的被保留的一小部分会话上下文会被TPM2_FlushContext命令清除,这也就意味着会话不再是激活状态了,之前保存的会话上下文也不能再加载到TPM中了,并且会话的handle会被释放,从而用于其他的会话。
TPM2_ContextLoad命令用于将保存的实体上下文重新加载到TPM中。(这里需要特别注意的一点是,尽管TPM2_Load和TPM2_ContextLoad在名字上很像,但是它们的功能差别很大。TPM2_Load主要用于在TPM2_Create命令创建新的对象之后将实体加载到TPM中。这个加载命令不能用于会话,也不能用于加载一个对象或者序列已保存的上下文。TPM2_ContextLoad用于加载使用TPM2_ContextSave命令保存的临时实体的上下文。这种相似的命令但是差异很大的功能(对于对象操作来说)曾经经常“绊倒”很多粗心的开发者,包括我)
对于一个对象或者序列,上下文被重新加载以后,TPM会为对象重新生成一个handle。一个对象或者序列的上下文可以多次被加载,每次都会返回不同的handle。这就意味着任何时刻都有可能多个对象或者序列的拷贝同时存在于TPM中。(多个对象副本可以存在于TPM中并没有实际的用途,并且会占用额外的对象存储槽位。本章开始时David Wooten的引言就是针对这个问题相关讨论的总结。虽然这个特点没有什么用处,但是它也不会引入安全隐患,因此,为了让TPM内部的固件实现起来更简单,这个现象就被允许了。)
对于一个会话来说,TPM2_ContextLoad命令会重新加载会话并返回于之前相同的会话handle。当然,一个会话的上下文只能在它的上下文被保存之后(保存的操作会将会话清除出TPM)重新加载,这样就可以防止针对会话的重放攻击。
TPM重启,TPM复位,以及TPM恢复会在第19章中详细描述。有一些特殊的上下文处理规则与上述的事件相关。这一小节从高层次的角度描述为什么需要这些规则,以及这些规则本身的细节。
一个TPM复位就像是一个冷启动,因此在复位之前保存的会话,对象和序列上下文在复位之后就不能重新被加载了。因为复位时TPM2_Shutdown(TPM_SU_CLEAR)会被执行,或者TPM2_Shutdown根本就不会被执行,所以用于重新加载上下文的信息都不复存在了。
一个TPM重启是为了在系统休眠之后重新启动系统,而一个TPM复位则用于系统睡眠之后被重新唤醒的过程。对于这两种情况,因为TPM2_Shutdown(TPM_SU_STATE)会被执行,所以之前保存的会话,对象和序列的上下文还可以被重新加载;但是有一个例外是,对于设置了stClear标志位的对象来说,TPM重启之后就不能被重新加载了。
详细的规则如下:
- 任何类型的TPM复位都会讲TPM所有的临时性实体清除。如果一个临时实体的上下文没有被保存,实体无法被再次加载。
- 对于之前已经保存上下文的情况,如果:
- 发生TPM恢复事件:保存的上下文可以被重新加载。
- 发生TPM重启事件,并且对象的stClear是0:保存的对象上下文可以被加载。
- 发生TPM复位,或者TPM重启事件并且对象的stClear是1:保存的对象上下文不能被加载。
- 对于一个会话,如果会话的上下文被保存了:
- TPM恢复和TPM重启事件后,上下文可以被重新加载。
- TPM复位事件后,上下文不能被重新加载。
因为上述的规则看起来实在有些复杂,用一些图表来表达,可能会帮助你理解相关实体的正常处理规则和涉及TPM复位,TPM重启,TPM恢复事件的特殊规则。(参考图18-1)
图18-1 TPM对象和序列的状态图
如下是关于图18-1的一些说明:
- 尽管我们使用了“对象”来描述相关概念,但它实际上指代的是对象和序列。
- 图中Load和ContextLoad弧线可以被多次执行。每次执行都会产生TPM对象的新副本并附带新的handle。除了handle之外,对象与其他的副本是一模一样的。我们之前也说过了,TPM中存在一个对象的多个副本并没有实际的用途。
- ContextSave弧线也可以被多次执行。每次都会产生一个对象上下文的新副本。
- 对于序列来说,它图表与图18-1有以下不同:一个序列的上下文必须在每次SequenceUpdate之后被保存。否则,ContextSave之后的ContextLoad命令将会导致错误的哈希或者HMAC。
相对于对象和序列来说,会话的状态图更简单(参考图18-2)。这两者的重要不同点再次总结如下:
- 对象和序列可以同时存在于TPM内外,但是会话不能。
- 对象可以被清除之后重新加载。但是会话在被清除出TPM之后就终止了,并且之前保存的会话上下文就不能被重新加载了。
- 与对象和序列不同的是,一个活跃的会话总是有相同的handle。
- 不管是在TPM内部还是上下文被保存在TPM外部,会话都是激活状态的。它们只有在被终止之后才是非激活态(图18-2的会话结束状态)。
图18-2 TPM会话的状态图
本章是关于上下文管理的讨论。TPM提供了所有实现RM需要的功能。尽管可能有很多方式来设计实现一个RM,但是我们还是从高层次的角度推荐主动式的实现方式。