认证系统设计经典会话
Bill Bryant,首次写与1988年2月
Theodore Ts'o与1997年2月整理并转换成HTML,并且追加了 afterword 章节来描述V5版本的一些变化
前言
本文虚构了一个关于公网认证系统--Charon构建过程的对话,随着对话的进行,Athena和Euripides探讨了公共网络环境里普遍存在安全问题,并在Charon系统设计之初就考虑好了这些问题的解决方式。所以直到对话完成,Athena和Euripides才算真正的把系统设计好。
当系统设计好后,Athena把系统的名字从Charon改成了Kerberos。非常巧合的是,在MIT的一个项目--Athena中,设计和实现的认证系统名称就叫做Kerberos。
对话中的“Kerberos”与1988年发表的“Kerberos:一个公共网络的认证系统“有着惊人的相似之处。
Scene I
在一个小隔间里,Athena和Euripides正在两个相邻的终端上工作
Athena:Rip,你们这个分时共享系统就是个累赘。因为其他人都登陆着,它慢的使我根本没法成我的工作。
Euripides:这个不要向我抱怨,我只是个打工的。
Athena:难道你不知道我们的需求吗?我们真正想要的是给每个人一个自己的工作站,之后再用网络链接所有的工作站,从而使大家能相互通信,这样他们就不用担心共享计算周期的问题。
Euripides:那么我们需要多少工作站呢,1000个?
Athena:差不多
Euripides:哪你有没有调研过普通工作站的磁盘,一般来讲是没有足够的空间来安装你在共享机器上所有的软件的?
Athena:这个我已经想好了,我们可以把所需的软件装在不同的服务器上,当你登陆到自己的工作站后,你可以通过网络向这些服务器发请求来使用那些软件服务。这样还能使所有的工作站都使用相同的软件版本,软件升级也会很便捷,只用把服务器上的软件升级就可以了,不用到每个工作站上面去操作。
Euripides:好吧,但是你怎么管理个人文件呢?在分时共享系统中,我可以通过任意一个和系统连接的终端登陆并访问我自己的文件。你这样设计后,我是否能通过任意工作站来获取我的个人文件,还是说我要随身带个软盘来保存我自己的文件呢?
Athena:我想我们可以用另外一台服务器来存储私有文件,这样你就可以登陆任意的工作站来获取你自己的文件。
Euripides:那打印功能怎么办,难道让每个工作站都要有自己的打印机吗?你用谁的钱来买这些设备呢?还有电子邮件怎么处理,怎么把邮件分发到所有的工作站上呢?
Athena:嗯…,很明显,我们没有钱给每个工作站配置一台打印机,但是我们可以用一台机器专门用来做打印服务。你把你的打印任务发给打印服务机,由它来帮你打印。你也可以用同样的思路来处理电子邮件,由一台机器专门做邮件服务,如果你想要你的邮件,你连接这个服务,由他来把你的邮件挑拣出来。
Euripides:你的工作站系统听起来是很好。当我有了自己的工作站,你知道我会做什么吗,我会找出你的名字,并使我的工作站认为我是你,这样我就可以连接到邮件服务获取你的邮件,我还可以连上文件服务器来删除你的文件,我还可以…
Athena:你真的会这样做吗?
Euripides:是的,那些服务怎么能知道现在是我在操作而不是你呢?
Athena:呃,我不知道,我需要好好想想这个问题。
Euripides:听起来是的。如果你知道怎么做了请一定告诉我。
Scene II
第二天早上,在Euripides 的办公室,Euripides正在他的办公桌前读他的点子邮件,Athena敲门进来
Athena:哈哈,我已经找到一种方式来加密网络环境,从而阻止像你这样调皮的家伙不能伪装成别人来使用那些服务。
Euripides:真的吗,坐下来讲吧。
Athena:在我开始之前,我能不能给我们的讨论定下一个基本准则?
Euripides:什么准则?
Athena:假如我说了下面的话:“我想看我的电子邮件,所以我连接上邮件服务,告诉邮件服务把邮件发到我的工作站。”实际上,并不是我直接连接邮件服务,而是我使用了一个程序去连接邮件服务,一个称作邮件服务客户端的程序。
但是当我每次描述用户和服务交互时,我不会说“客户端做了什么“,而是会说“我做了什么”,所以请记住是客户端作为我的代理做了这些事。我这样讲有没有问题?
Euripides:没有问题。
Athena:针对网络安全问题,最笨的方式是每次邮件服务都让用户提供密码来证明用户身份。
Euripides:这的确是够笨的。在这样一个系统中,需要每一个服务都知道你的密码。如果网络里面有一千个用户,每个服务都要有一千个用户的密码;当你想要修改密码时,你还得联系所有的服务去一个个修改。我想你的系统不会这么笨吧。
Athena:我的系统不笨。他像这样工作:不仅仅是用户有密码,服务也有密码。每个用户知道自己的密码,每个服务也知道自己的密码,并且还有一个认证服务知到所有的密码,包括用户的和服务的。认证服务把这些密码存储在一个中央仓库中。
Euripides:你有给这个认证服务起好名字吗?
Athena:我还没有想好,你有什么想法吗?
Euripides:那个帮助死者过冥河的摆渡人名字怎么样?
Athena:Charon吗?
Euripides:是的,就是他,除非你证明你的真实身份,否则他是不会摆渡你的。
Athena:Rip,你又来这一套,重新杜撰罗马神话。Charon并不关系你的身份,他只确保你已经死掉就够了。
Euripides:哪你有没有一个更好的名字?
Athena:没,还没有。
Euripides:那就把这个认证服务叫“Charon ”吧。
Athena:好吧,那我就开始描述这个系统了。
我们假定你现在要用邮件服务。在这个系统里,你不能直接使用一个服务,除非Charon告诉那个服务你就是你所声明的那个人。并且除非你先向Charon认证,否则你也不能获取到那个服务。当你向Charon认证时,你同时得告诉Charon你想使用哪一个服务,就是说如果你想用邮件服务,你得明确告诉Charon。
接着,Charon要求你提供认证信息。你向Charon提供你的密码,然后Charon和注册在中央数据库中的密码进行对比,如果密码时匹配的,Charon认为你证实了你的身份。
接下来,Charon要告诉邮件服务你就是你所声明的那个人。因为Charon知道所有服务的密码,当然也包括邮件服务的。很明显Charon可以告诉你邮件服务的密码,当你向邮件服务发起请求时你可以用这个密码证明你已经向Charon认证过了。
现在的问题是,Charon不能直接给你密码,因为你知道这个密码后,下一次你想访问邮件服务,你就可以绕过Charon不用再认证了,之后你仍可以伪装别人,用别人的名字来访问邮件服务获取他的邮件了。
所以,Charon会给你一个邮件服务的票据(TICKET)而不是直接给你密码。这个票据里面包含了你的名字,并且会利用邮件服务的密码进行加密。
TICKET = {username} K_server
- {X}K_Y 表示 用K_Y对X进行加密。
拿到票据后,你现在可以向邮件服务请求你的邮件了,你告诉邮件服务你的名字,同时把票据发给邮件服务来证明你的身份。
邮件服务拿到票据后,首先用自己的密码进行解码,如果能正常解码,邮件服务就能拿到Charon存放在票据中的名字。
邮件服务再用票据中的名字和你提供的名字进行对比,如果名字一致,邮件服务就认为你是你所声明的人,就把你的邮件给你。
你觉得这个方法怎样?
Euripides:我有几个问题。
Athena:预料之中,你继续讲。
Euripides:当一个服务解码一个票据,他如何判断自己正确解码了(这个票据是针对自己这个服务的,而不是用户申请访问别的服务对应的票据)?
Athena:这个我还没考虑好。
Euripides:最好能把服务的名字也放到票据中。这样当一个服务解码票据后,他可以通过能不能在票据中正确找到自己的名字来判断解码是否成功。
Athena:听起来不错,那么现在票据就变成了这样
TICKET - {username: servicename} K_server
Euripides:这样票据就包含用户名和服务名。
Athena:再用服务的密码进行加密。
Euripides:可是我还是不认为这些信息就能保证票据的安全性。
Athena:举个例子呢。
Euripides:比如说,你向Charon要了一个邮件服务的票据。Charon准备好了票据,并把你的名字“tina”放了进去。假定在Charon通过网络把票据发给你的过程中我把这个票据复制了一份,然后我欺骗我的工作站说我就是tina,这样工作在我工作站的邮件客户端程序就会认为我就是你。利用你的名字,邮件客户端再利用偷来的票据向邮件服务发请求。这样邮件服务收到请求后解码了票据,并会验证通过,邮件服务就会把你的服务发给我。
Athena:嗯,是的,这个设计还是不完美。
Euripides:但是我知道一种方式来修复这个问题,或者说局部解决这个问题。我认为Charon需要在票据中包含更多的信息。除了用户名,Charon还需要把用户请求票据时的网络地址包含进去,这样就可以多一个安全保证。
我用下面的过程来证明:假如说我现在偷取了你的票据,这个票据里面有你工作站的地址,这个地址和我工作站的地址是不一样的。当我伪造你的名字和偷来的票据请求邮件服务时,邮件服务从票据中解码出名字和地址并和请求中的用户名和地址进行匹配,虽然用户名匹配成功了,但是地址不匹配,很明显这个票据被偷了,服务器就拒绝这次请求。
Athena:太好了,我希望我也能想到这样的方法。
Euripides:哈,这就是我的目的。
Athena:那么修改后的票据就变成了
TICKET = {username:ws_address:servicename} K_server
Athena:我现在好兴奋,让我构建一个Charon系统来看看他是否能工作吧。
Euripides:还不能这么快,我对这个系统还有几个疑问。
Athena:好吧,继续。
Euripides:目前来看,每次只要我想访问一次服务,我都得获取一个票据。如果我一整天都在工作,我肯定会多次要获取我的邮件信息。如果每次访问服务都得获取一个票据,那我是不会喜欢这个系统的。
Athena:呃…,为什么票据不能重复使用呢,当你获取一个票据后,你是可以重复使用的。比方说,当邮件客户端程序向服务请求时,他把票据复制一份,把这个副本发送到服务端。
Euripides:听起来不错,但是还是有问题。目前来看,当我要访问一个我还没有票据的服务时,我都得向Charon提供我的密码。例如我想访问我的文件,我得向Charon提供我的密码来获取一个票据,当我想向邮件服务获取我的邮件时我还得向Charon提供我的密码,假如我又想打印些什么东西,我又得访问Charon,你能想象到那个画面的。
Athena:呃,是的。
Euripides:这还不是最糟的,目前你向Charon认证时,你以明文的形式在网络上传播你的密码。那些像你这样聪明的人肯定可以通过模拟网络来偷取别人的密码,如果我有了你的密码,我又可以以你的名义使用任意服务。
Athena:这些都是严重的问题,我需要重新设计一下。
Scene III
隔天早上,Athena在咖啡间遇见了Euripides,并在他的接满水后,把手搭在了他的肩膀上,一起向咖啡机走去
Athena:我想了一个新版本的Charon可以解决我们的问题。
Euripides:真的吗,这么快?
Athena:嗯,这个问题让我想了一晚上。
Euripides:这一定是你的负罪感,让我们找个小隔间再讨论讨论吧?
Athena:为什么不呢?
两人走向一个小隔间
Athena:作为开始部分,我会重新陈述一下我们的问题,看看我们的系统是不是真的需要这些功能。
Athena:第一个需求:用户只用在他们登陆进工作站的时候输入他们的密码一次。这个需求指的是你不用在每次获取其他服务的票据时都要输入密码。第二个需求:密码不能以明文的形式在网络上传播。
Euripides:是的
Athena:对于第一个需求:你只需要输入一次你的密码,为了满足这个需求,我会引入一个新的服务,一个叫做“票据分发”(ticket-granting)服务,当用户向Charon认证完成后,这个服务就会向这个用户分发票据。如果你有这个票据分发服务的票据(TGT),你就可以使用这个票据分发服务。
票据分发服务可以简单当作是Charon的一个变体,他也能访问中央数据库。Charon中的这个票据分发服务可以让你用票据代替密码来认证。
现在,认证系统将这样工作:你登陆到你的工作站,然后利用Kinit这个程序来连接Charon服务,之后你向Charon提供凭据,Kinit从Charon服务获取你对应的TGT。
假如现在你想要访问邮件服务,而你还没有邮件服务的票据,你就可以用TGT向Charon来获取邮件服务的票据,而不是使用密码来获取这个票据。
Euripides:那我需要访问其他服务时要不要每次都得获取一个新的TGT呢?
Athena:不用,记不记得上次我们达成的共识,票据是可以重复使用的。一旦你有一个TGT,你不需要再获取一个,之后你就可以重复使用这个TGT来获取其他服务的票据。
Euripides:嗯,很有道理。因为你可以重复使用票据,所以一旦票据分发服务给你一个针对特定服务的票据(ST),就不需要再获取这票据了。
Athena:是的,这样是不是比较简洁。
Euripides:嗯,到目前为止还行,只要在获取TGT时不通过明文在网络上传输我的密码。
Athena:就像我说的,我这问题我也解决了。事实上,当向Charon请求TGT时,我表述的好像是我要通过网络发送你的密码,但这并不是必须的。
真实情况是,当用Kinit程序向Charon获取TGT时,kinit并不向Charon服务发送密码,kinit只发送你的名字。
Euripides:好
Athena:Charon服务利用你的名字来查找你的密码,接着Charon构造一个包含TGT的数据包,在Charon把这个数据包发给你之前,他会用你的密码对数据包进行加密。
你的工作站收到这个数据包后,提示你输入密码,kinit会尝试用你输入的密码来解码这个数据包,如果解码成功,预示着你认证成功,并且你会拥有TGT,然后你可以用这个票据来获取你想要服务的票据。
这个是不是你所想要的?
Euripides:我还不清楚,我需要再考虑考虑。我认为你刚才描述的那部分系统功能工作的相当好,它只需要我认证一次,此后Charon会自动的为我分发其他服务的票据也不用我再关心。但是关于票据的设计我还是有困惑,因为基于一个前提,那就是票据是可重复用的,我同意票据可以重复使用,但是可重复用本质上讲是危险的。
Athena:具体来说呢
Euripides:打比方来说,假如你使用了一个不安全的工作站,期间你获取了邮件服务的票据、打印服务的、文件服务的,然后你退出了工作站。
现在假定我也登陆到了这个工作站,并且找到了这些票据。我呢,又比较喜欢制造麻烦,所以我把我的名字改成你的来欺骗工作站说我就是你,因为那些票据都是用你的名字生成的,所以我可以用邮件客户端来获取你的邮件,也能获取你的文件,也能用你的账号发送成千上万的打印任务,所有的这一切都是因为你把票据忘在了这个工作站上。
并且也没有什么方法能阻止我把这些票据再复制下来,这样我就可以一直使用它们了。
Athena:这个很好解决呀,我们可以写一段程序在用户登出时销毁用户的票据,这样你就不能使用了。
Euripides:好吧,很明显你的系统需要一个销毁程序,但是让用户依赖于这样一个程序是愚蠢的。你不能确保用户在每一次使用完工作站登出时都执行这个销毁程序,即便你依赖于用户执行销毁程序,在下面的场景下也有问题
我写了一个程序,它可以监听网络,当有服务票据从网络上传播时我就把它复制下来。假如我想陷害你,当你登陆进工作站时,我也把这个程序打开,从而能copy你的票据。
当你完成工作退出工作站并离开后,我把我的工作站地址篡改成你的地址,更改用户名使我的工作站相信我就是你,这样我就有了你的票据,你的名字,你的地址,我又可以以你的名义重新使用这些票据。
即便是你在登出时销毁了你的票据也没有关系,因为我偷的这些票据只要我想用就会一直有效,而这都是因为你现在的票据设计里面并没有对票据的使用次数或者使用时间做限制。
Athena:哦,我知道你所说的了,票据永久有效是一个大的安全隐患。我们必须限制一个票据可用的最大时长,也许我们可以给每个票据一些过期信息。
Euripides:实际上,我认为每个票据都得追加下面的信息:一个有效期(lifespan)字段,表明这个票据的最大有效时间;还有一个时间戳(timestamp)字段,表明Charon创建这个票据的时间。所以现在票据信息就变成了
TICKET = {username:ws_address:servicename:lifespan:timestamp} K_server
Euripides:现在当一个服务解码票据之后,首先检查下票据里面的用户名和地址和用户发过来的是否一致,接着再用有效期和时间戳字段判断这个票据是否过期。
Athena:很好,那这个有效期设置多久比较好呢?
Euripides:我也不清楚,设置成大多数工作站的会话时长怎么样,即8小时?
Athena:这也就是说,如果我在我的工作站工作超过8小时,我所有的票据都将失效,也包括票据分发票据,所以在8小时后,我必须再次向Charon认证。
Euripides:这个也不是难以接受的吧?
Athena:嗯,也是。那我们就定下来票据在8小时后过期。现在我有一个新的疑问了,假如我从网络上复制了你的票据..
Euripides:哦,Tina,你不会真的这样做吧,对不对。
Athena:这只是我们交谈的一种假设情况。现在我复制了你的票据,并等着你退出你的工作站。可能是你和医生有一个约谈或者是要参加一个会议,所以你在两个小时左右就退出了你的工作站。虽然你比较谨慎的在推出时销毁了你的票据,但是我已经偷取了这些票据,并且在接下来的6个小时里面它们是完好可用的,我将有足够的时间来操作你的文件,以你的名义发送成千上万的打印任务。
所以说,追加有效期和时间戳的方式对于票据过期后的攻击是起作用的,但是如果在票据过期前攻击那。。
Euripides:哦,是的,你是对的。
Athena:我想我们找到核心问题了。
Euripides:我猜你今天晚上又要忙活了,要去喝些咖啡吗?
Athena:要的,走
Scene IV
隔天早上,在Euripides的办公室,Athena敲门进来
Euripides:Tina,一大早就戴着个黑眼圈呀。
Athena:是啊,你知道又一个漫漫长夜。
Euripides:你找到解决问题的方法了吗?
Athena:我想我找到了。
Euripides:坐下来慢慢说。
Athena:按惯例,首先我要重述下这个问题:票据都有一个有效期-8小时,如果在过期前其他人偷走了你的票据,那我们就没有办法阻止他了。
Euripides:正是这个问题
Athena:如果我们把票据设计成不可重复使用,那我们可以修复这个问题
Euripides:但是这样你就不得不在每次想要利用一个网络服务的时候都去请求一个票据了
Athena:是的,这算是一种粗暴的解决方案。哦,我该怎么继续我的论述呢?
好吧,让我们从需求的角度再把问题重述一下:一个服务必须要证明现在用票据发起请求的人就是Charon分给票据的那个人。
让我们再把认证过程推演一遍,看我能否想出一个合适的方法来表述我的解决方案。
当我想访问一个特定的服务时,第一步,我在我的工作站上启动一个客户端程序,这个客户端发送三个信息给服务,我的名称、地址、以及相应的票据。其中票据中包含认证时的用户名,认证时工作站对应的网络地址,还包含了票据的过期信息。所有这些信息在返回给客户端时Charon都用对应服务的密码进行了加密。
我们现在的认证模式基于下面的测试:
- 服务能否解码对应的票据
- 票据是否过期
- 票据中的用户名和网络地址是否和票据一起发送的用户名和地址匹配
这些测试能证明什么呢?
第一,这个票据是否是Charon分发的,如果票据不能正确解码,那就不是从真正的Charon过来的。真正的Charon会使用服务对应的密码加密,而只有Charon和服务知道这个服务的密码。如果票据能正常解码器,那服务就能确定票据是从真正的Charon过来的的,这样就能防止攻击者伪造Charon分发票据。
第二,这个票据是否过期,如果过期,服务端就拒绝这个请求,这样能阻止攻击者使用旧的票据,因为这些票据很有可能被偷走了。
第三,检查票据中的用户名和网络地址,如果检查失败,说明用户在使用别人的票据(可能是偷偷在用),服务端肯定要拒绝这样的请求。
如果用户名和地址匹配上,那证明了什么呢?什么也没有,攻击者可以从网络上偷走票据,更改他的工作站地址和用户名,从而偷窃别人的资源。就像我昨天说的,票据在过期之前是可以无限制的重用的。它们能被重用是因为服务端没办法区分现在请求的用户是否是票据真正的拥有者。
服务端不能做区分是因为他和用户之间没有一个共享的机密信息。打比方来说,我在Elsinore(哈姆雷特里面的城堡)站岗,现在你要来换下我,除非你告诉我正确的暗号,否则我是不会同意你替换我的,这就是我们两个之间需要共享一个机密的例子,这个机密可能是某个人为所有的站岗者想出的暗号。
这就是我昨天晚上想的,为什么不能让Charon为票据真正的拥有者和服务生成一个共享的密码呢?Charon把这个会话密钥(session key 就是刚说的共享密码)的一份拷贝发给服务端,再把另一份拷贝发给用户,当服务端收到用户的请求时,他可以用这个会话密钥来验证用户。
Euripides:等一下,Charon怎么给服务端和用户发送这个密钥呢?
Athena:Charon把会话密钥作为返回值的一部分,像这样:
CHARON REPLY = {sessionkey|ticket} K_user
服务端通过解码票据来获取会话密钥,所以现在票据变成了如下的样子
TICKET = {sessionkey:username:address:servicename:lifespan:timestamp}K_server
现在当你想要访问一个服务时,你使用的客户端程序会构建一个我们称之为“认证对象”(AUTHENTICATOR),这个认证对象中包含你的用户名和你工作站的网络地址。客户端用Charon返回的会话密钥对认证对象进行加密,就是他向Charon认证时对应的会话密钥。
AUTHENTICATOR - {username:address} K_session
客户端构造好认证对象后,和票据一起发送到服务端,这时服务端还不能解码这个认证对象,因为他还没有拿到回话密钥,密钥存放在票据中,所以第一步服务端必须先解码票据
服务端解码票据后将获取下面的信息:
- 票据有效时间和创建时间
- 票据拥有者的名字
- 票据拥有者对应的网络地址
- 会话密钥
服务端先检查票据是否过期,如果是好的,接着,服务端用会话密钥解码对应的认证对象,如果能顺利解码,服务端就会获取到用户名和网络地址,然后服务端把获取到的用户名和网络地址和票据中的对应信息比较,并且再和发送票据、认证对象时一起发送过来的用户名和网络地址比较,如果一切都对比正确,则服务端就可以确认现在发送票据的人确实是票据的拥有者
我认为会话密钥-认证对象能解决票据重用带来的问题
Euripides:可能吧,我还是有困惑,要想用这个方式,我必须构建服务相应的认证对象
Athena:不,你不仅要有认证对象,还要有正确的票据。如果没有票据只有认证对象是没有任何意义的,因为没有解码票据前你是获取不到会话密钥的,而没有会话密钥你就没法解码认证对象。
Euripides:这个我知道,但是你也说了,当一个客户端程序请求服务端时,他会把认证对象和票据一起发送过去
Athena:是的,我这样说过
Euripides:如果真是这样,那怎么阻止我把认证对象和票据同时偷走呢?我可以确定通过程序我能做到这个。如果我能同时偷取到认证对象和票据,这样在票据过期前我就可以用这两个信息,改变我工作站的地址和用户名来伪装请求了,是不是?
Athena:是的,好失望
Euripides:等等,这没什么大不了的。票据在过期之前可以重复使用,但这并不预示着认证对象也可以重复使用。假如我们设计一个方案只允许认证对象使用一次,是不是就没有问题了
Athena:是的,很有可能。我们再讨论一下这个过程,客户端程序构建了一个认证对象,然后把它和票据一起发送给服务端。你把票据和认证对象都复制一份偷走了,但是这个票据和认证对象在你能发给服务端之前已经到达了服务端。如果认证对象只能够使用一次,那你复制下来的票据和认证对象就不起效了,当你尝试认证时就会失败
好了,总算放心了,所以现在需要做的事是用一种方式保证认证对象只能使用一次
Euripides:没有问题,让我们在认证对象里面也存放一个有效期和生成时间。假如每一个认证对象的有效期只有几分钟。当你想使用一个服务时,你的客户端程序构建一个认证对象,添加上当前的时间戳,接着把它发送到服务端。
服务端收到票据和认证对象后,先解码票据获取会话密钥,然后用会话密钥解码认证对象,接着检查认证对象是否过期,如果没有,并且其他的检查都通过,服务端通过认证。
假如现在我复制了你的票据和认证对象,为了使用它们,我需要在几分钟内修改好我的工作站网络和用户名,这个要求是很高的,我不认为能做到,除非…
好吧,还有一个潜在的风险,假如我不是在当你向服务端认证时复制票据和认证对象,而是复制Charon发给你的原始数据包,就是你向Charon要票据时它返回给你的数据包
这个数据包,包含了两份会话密钥在里面,一个给你,一个给服务端。给服务端的在票据里面,我不能拿到,但是另一个呢,那个你用来生成认证对象的密钥呢
如果我能拿到这个密钥,我就可以构建自己的认证对象,这样我就可以攻破这个系统
Athena:这个我昨晚上也想到过,但是我推演了一下获取票据的过程,发现以这种方式偷取认证对象是不可能的。
你坐在你的工作站前,用kinit程序获取你的票据分发票据(TGT),kinit需要你的名字,你输入后,kinit把它发送到Charon。
Charon用你的名字到数据库中查询你的密码,接着创建一个TGT。作为这个过程的一部分,Charon创建一个你和票据分发服务共享的会话密钥。Charon把这个密钥的一份拷贝放到TGT中,把另一个拷贝放到数据包中,在把这个数据包发送给你之前,Charon会用你的密码把它加密。
CHARON REPLY = [sessionkey|TGT] K_user
Charon通过网络把加密后的数据发给你,如果有人偷取了这个数据包,没有你的密码他没法解码这个数据包,也就不能获取到会话密钥。
Kinit获取到这个数据包之后,提示你输入密码,如果你输入了正确的密码,Kinit就会解码这个数据包,给你对应的会话密钥。
客户端为票据分发请求创建一个认证对象,并使用会话密钥对认证对象加密,接着客户端把这个加密后的认证对象发送给Charon,同时把TGT,你的名字,工作站的网络地址还有你要访问的-邮件服务的名称一起发过去,
票据分发服务(TGS)收到这些信息,(其中TGT和普通的ST,也没有区别,它就是票据分发服务对应的票据-ST,里面也是包含了{sessionkey:username:address:servicename:lifespan:timestamp}这些信息,只不过服务名字就是固定的TGS的名字,并用票据分发服务对应的密码进行了加密),就开始正常的认证检查,(也是先用自己的密码解码TGT,看是否过期,再用里面的会话密钥解码认证对象,验证用户名,地址)如果检查通过,票据分发服务(TGS)就会拿到一个正确的会话密钥,接着票据分发服务为你构建一个邮件服务的票据,并为你和邮件服务创建一个新的共享的会话密钥。
接下来,票据分发服务会准备一个发往你工作站的票据数据包,这个数据包中包含了刚创建的邮件服务的票据和新建的你和邮件服务共享的会话密钥,在发送数据包之前,票据分发服务会用你和票据分发服务之间共享的会话密钥把数据包加密。
现在看下邮件服务的票据包的发送,假如有人在这个数据包在网络上传输的工程中复制了一份,由于数据包用数据分发服务的会话密钥加密过,他也得不到里面的内容,因而不能得到在数据包中的邮件服务的会话密钥,没有这个密钥,他就不能创建认证对象,从而也不能使用TGS生成的并通过网络传输的邮件服务票据。
所以现在来看,我们安全了,你认为呢?
Euripides:可能吧
Athena:可能,你就只能说个这吗
Euripides:别生气,这只是对我自己刻薄的说法,你为这个都搞了大半夜了
Athena:Pthhhhh!
Euripides:好吧,是3/4个晚上,现在,这个系统听起来可以接受了,会话密钥的方式解决了昨晚上我思考的一个问题,一个双向认证的问题,介意我再说几分钟吗?
Athena:当然
Euripides:你真和善。昨晚上当会话密钥和认证对象跳入的大脑时,我尝试着找到系统的新问题,最后还真找到了一个严重问题,接下来我会用一个例子慢慢说明它:
假如说你厌倦了你现在的工作,并且也找到了新的感兴趣的想跳槽过去。你想在公司的打印机上打印你的简历,使猎头或者潜在的雇主能注意到你的优点。
所以你点击了打印功能,直接把你的简历发送给相应的打印机。如果你还没有打印服务的票据,打印客户端首先会获取打印服务的票据,再以你的名义把票据发送给服务端,至少在你认为它是按照这样的流程来工作的,你并不知道这个任务是否发给了正确的打印机。
假如现在有一些恶意的黑客--你的老板--有一个转换系统,利用这个系统,它能把你的请求和票据重定向到他办公室的打印机上。如果他的这个打印机服务设计的不关心对应的票据和内容,它直接忽略掉票据,并返回给你的工作站一个信息说票据通过验证,服务端也准备好为你提供打印服务了。你的打印客户端发送打印命令给这个伪装的打印机,这样你的敌人就拿到了你的简历。
我用对比的方式重述下这个问题,就是在没有会话密钥和认证对象的时候,Charon能够阻止服务端不被错误的用户访问,但是并不能阻止用户访问错误的服务端。系统需要一种方式能让客户端在发送敏感信息给服务端前先验证服务端的正确性,也就是说系统必须能双向认证。
利用会话密钥,只要你能正确设计客户端程序,就可以解决掉这个问题。让我们再以打印服务为例,我们希望打印客户端能确认它发的打印服务确实是被正确的打印服务接收到的。
那么打印客户端应该这样做,我输入一个打印任务和对应的打印文件名-我简历的名称,假如我拥有打印服务的票据和会话密钥,客户端程序用会话密钥构建一个认证对象,把认证对象和票据发给认为正确的打印机。这时客户端并没有发送要打印的简历,他要等待服务端返回信息。
现在正真的打印服务接收到票据和认证对象,首先解码票据获取会话密钥,接着用会话密钥解码认证对象,之后做各种认证检查;
假如认证通过,服务端返回给客户端一个能证明自己身份的数据包给客户端,并用会话密钥把返回的数据包加密。
客户端收到数据包后会尝试用会话密钥解码数据包。如果数据包正确解码,并且得到了正确的服务端返回信息,客户端就可以判定现在返回信息的是正确的打印服务,接着才会把对应的打印简历任务发过去。
这样假定你的老板用转换系统把他的打印机伪装成我想用的那一台,我的客户端发送认证对象和票据给他并开始等待返回结果,而伪装的打印机不能生成正确的返回信息,因为它不能解码票据得到会话密钥。这时我的客户端收不到正确的返回信息,就不会把真正的打印任务发过去,最终客户端等待超时并退出,虽然我的打印任务没有完成,但至少我的简历没有出现在敌人的办公桌上。
你看,我们有一个坚实的理论依据来实现Charon认证系统了。
Athena:是的。可不管怎么说,我并不喜欢Charon这个名字
Euripides:你不喜欢,从什么时候开始的
Athena:我一直都不喜欢,因为这个名字没有任何意义,我之前跟我的叔叔谈起这个事,他建议我用另一名字,Charon的那只有三个头的狗
Euripides:哦,你是说“Cerberus”
Athena:注意你的发音,是“Cerberus”
Euripides:呃,是这个名字吗?
Athena:是的,如果你是个罗马人,我是一个希腊神,我有一个希腊看门狗,那么它的名字会读作“Kerberos”,首字母是K
Euripides:好了好了,不要再扔霹雳了,就用这个名字了,事实上,他有一个漂亮的黑眼圈。现在,再见,Charon,你好,Kerberos。
Afterword
这个对话是在1988年写的,用来帮助读者理解为什么Kerberos V4版本设计成这个样子,这么多年,它很好的完成了任务。
当我把这个对话整理成HTML后,我惊讶的发现这个文档也同样适用于V5版本。虽然很多事情发生了变化,但是协议的核心还是一样的。只有两个地方在V5版本中做了调整。
第一个变化是意识到如果一个黑客用程序来抓取票据和认证对象,并立即将它们发送到服务端,这样把认证对象的有效期设置成5分钟并不能有效的阻止这种重放攻击。
所以在V5版本中,认证对象被真正设计成“只能使用一次”,这通过在服务端设计一个缓存,用来保存最近发过来的认证对象的记录来实现。攻击者尝试偷取认证对象并重用它,即便在5分钟的窗口期里,服务端的缓存也能区分出来这个认证对象已经被提交过了。
第二个主要变化是当用户用Kinit首次从Kerberos获取服务的票据(TGT)时,在Kerberos把票据返回给客户端时,票据不需要用用户的密码加密。因为这个票据已经用票据分发服务的密码加密过了,之后用户用这个票据再向Kerveros请求其他服务的票据时也是以未加密的形式直接在网络上传播的。所以这个地方就没有必要再用用户的密码去进行加密了(返回数据包中的其他部分如票据会话密钥等还是要加密的)
票据分发服务(TGS)也做了同样的修改,票据分发服务生成的票据,也不再用票据分发服务对应的会话密钥加密,因为这个票据已经用服务对应的密码加密过了。
例如在V4版本中,数据包是下面这样的
KDC_REPLY = {TICKET, client, server, K_session}K_user
TICKET = {client, server, start_time, lifetime, K_session}K_server
在V5版本中会变成下面的样子
KDC_REPLY = TICKET, {client, server, K_session}K_user
最终的请求过程如下
当然,Kerberos在V5版本中还加入了其他的新特性。用户可以安全的分发他的票据,从而在其他的地方使用;用户也可以把部分权限授予一个服务器,使这个服务器可以作为自己的代理。其他的新特性包括用更加安全的加密算法代替DES,例如triple-DES。读者如果想了解更多关于V5和V4之间不同点的可以访问【Kerberos系统的进化】,作者是 Cliff Neumann 和 Theodore Ts'o。
最后希望你能喜欢关于Kerveros的简单介绍,祝你在之后的使用中一切顺利。
Theodore Ts'o 1997.2