Yubikey 一次性密码身份验证

作者:Dirk Merkel

许多因素促使我仔细研究 Yubikey。首先,它为当今安全行业面临的两大主要问题:身份验证和身份管理,提供了一个如此简单而优雅的解决方案。此外,我非常欣赏 Yubico(Yubikey 的制造商)尝试将开源运动融入其商业战略的方式。在本文中,我将介绍与这个小设备相关的三个主题。首先,我解释 Yubikey 的作用以及如何使用它。其次,我研究它是如何工作的。第三,我展示如何在您的基础设施中集成 Yubikey 身份验证服务,而无需过多麻烦。

它是什么?

Yubikey 是一个小的塑料长方形,基本上由一个 USB 连接器和一个按钮组成。它类似于一个微型 USB 闪存驱动器,尺寸仅为 18x45x2 毫米,重量仅为 2 克,可以轻松地放在钥匙链或钱包中(图 1 和图 2)。当您将其插入机器的 USB 端口时,它会将自身识别为键盘,这意味着只要主机设备支持通过 USB 人机接口设备 (HID) 规范输入数据,Yubikey 就是平台独立的。它从主机设备获取电力,因此不必依赖内部电池。整个设备非常紧凑,可以使用设备顶部附近的小孔连接到实际的钥匙环上。金色表面连接器非常坚固,预计可以使用设备的整个寿命。据 Yubico 代表称,Yubikey 在经过洗衣机循环后仍然可以使用。

Yubikey One-Time Password Authentication

图 1. 已插入的 Yubikey

Yubikey One-Time Password Authentication

图 2. Yubikey 尺寸

每次您按下设备上的按钮时,它都会生成一个一次性密码,并将其发送到主机,就像您在键盘上输入它一样。然后,服务可以使用此密码来验证您的用户身份。

如何使用它?

当我无法访问自己的系统时,我使用 RoundCube 阅读电子邮件。RoundCube 是一个以 AJAX 为中心的基于 Web 的电子邮件客户端。您可以通过 Web 浏览器使用它,就像您可能使用 Gmail 或大多数其他主要的在线电子邮件提供商一样。幸运的是,RoundCube 是开源的,并且基于 PHP,因此添加 Yubikey 身份验证并不需要太多工作。

通常,RoundCube 会要求您输入电子邮件地址和密码才能登录。但是,经过一些修改后,登录屏幕现在增加了一个第三个字段:Yubikey OTP(一次性密码)。现在,您只需像往常一样输入您的电子邮件和密码,将光标放在新添加的文本字段中,然后将手指放在 Yubikey 的按钮上。大约一秒钟后,Yubikey 会神奇地吐出一个 44 个字符的序列,后跟一个换行符。换行符会导致表单被提交。并且,假设您的 Yubikey 确实与您的帐户关联,您将登录。请看图 3,其中显示了略微修改的登录屏幕。

Yubikey One-Time Password Authentication

图 3. 修改后的 RoundCube 登录表单 UI

出于显而易见的原因,Yubikey 不应仅用作唯一的身份验证方法。如果那样的话,某人拿到您的 Yubikey 后,只要该人也知道您相应的登录名,就可以访问您启用了 Yubikey 的帐户。但是,如果您使用 Yubikey 为多属性身份验证方案添加另一个属性,则可以显着提高安全性。想象一下,有人未经您的同意监控您的网络流量。他们可能能够通过检查捕获的 TCP 数据包来获取您的密码,但他们捕获的 Yubikey 密码对他们毫无用处,因为它只能使用一次!在您使用 Yubikey 密码登录某个位置后,它将变得无用。在下一节中,我将确切解释这种一次性密码方案是如何工作的。

更多细节

让我们仔细看看 Yubikey 传输到主机的字符序列。以下是我的 Yubikey 生成的序列示例

tlerefhcvijlngibueiiuhkeibbcbecehvjiklltnbbl

以上实际上是一个一次性密码,它使用 AES-128 加密和 ModHex 编码进行保护。让我们看看 Yubikey 如何构造这个字符串。为了便于讨论,请参考图 4。

Yubikey One-Time Password Authentication

图 4. Yubikey 令牌构造

设备首先创建一个 16 字节的序列(图 4),其中各个字节的分配方式如下

  • 前六个字节保存密钥的秘密唯一 ID,该 ID 在 Yubikey 编程时分配。此 ID 仅为分配它的实体所知,并且无法从 Yubikey 中检索。六个字节转换为 2(6*8) = 281,474,976,710,656 个唯一的位组合,这是在 Yubico 不得不考虑新方案之前可以发出的 Yubikey ID 数量。考虑到这个数字超过了当前世界人口的 42,000 多倍,除非其商业模式比任何人预期的都更成功,否则 Yubico 不太可能在一段时间内用完唯一的 ID。

  • 我们序列中的接下来的两个字节,字节 7 和 8,用于在非易失性存储器中存储会话计数器。计数器从零开始,并在每次插入设备时递增。两个字节用于会话计数器,允许 2(2*8) = 65,536 个会话。换句话说,在会话计数器用完之前,您几乎可以每天插入 Yubikey 三次,持续近 60 年。请注意,您可以在每个会话中生成大量 OTP(见下文)。

  • 接下来的三个字节,字节 9 到 11,用作时间戳,在每个会话期间存储在易失性存储器中。这意味着每次插入设备时,时间戳都从零开始并持续增加。由于它由内部 8Hz 时钟递增,因此时间戳值将在大约 24 天后耗尽。届时,您需要拔下 Yubikey 并重新插入。

  • 序列中的字节 12 是一个会话计数器,它从零开始,并在每次生成令牌时递增一。当它达到最大值 255 时,它会回绕到零。

  • 序列中的字节 13 和 14 是由自由运行的振荡器提供的伪随机数。这些字节用于在将明文提交到密码之前向其添加额外的熵。

  • 最后两个字节,数字 15 和 16,包含使用 CRC-16 算法对令牌的所有值计算的校验和,其中两个校验和字节设置为零。此校验和用于数据完整性检查。

每次调用 Yubikey 时,它都会生成上述 16 字节序列。但是,如果您查看本文前面列出的 Yubikey 输出示例,您会注意到它实际上由 44 个字符组成。那是因为在 Yubikey 准备吐出最终令牌之前,我们仍然缺少三个关键步骤。首先,16 字节的令牌使用 AES-128 密钥加密,该密钥对于每个 Yubikey 都是唯一的。其次,Yubikey 在加密的 16 字节令牌前添加一个六字节的明文公共 ID。此公共 ID 与用于构造 16 字节序列的秘密 ID 完全不同。公钥不会更改,可用于将 Yubikey 令牌与帐户关联。最后,整个 22 字节序列(16 字节加密加上 6 字节公共 ID)将使用不太知名的 ModHex 算法进行编码。

Yubico 选择此算法仅仅是因为它仅限于许多不同键盘布局通用的字符。由于 Yubikey 模拟键盘,因此它尝试使用适用于它可能在野外遇到的各种键盘设置的字符。缺点是 ModHex 编码效率有点低,因为它对它编码的每个字节都需要两个字符,这就是为什么 22 字节序列变成 44 个字符序列的原因。但是,由于 Yubikey 完成所有输入,因此这不会给用户带来不便。

更多关于加密

让我们仔细看看生成令牌的加密步骤。与公共密钥加密方案(例如 PGP)中使用的非对称算法相反,AES 是一种对称算法。这意味着加密令牌的一方和解密和验证它的一方都需要访问 AES-128 密钥!AES 密钥的共享发生在设备编程时。与设备唯一的 ID 类似,唯一的 AES-128 密钥由 Yubico 生成并存储在设备上,然后在发货前。该公司维护一个数据库,其中唯一的公共 ID 以及秘密 ID 与其相应的 AES 密钥相关联。通过这种方式,Yubico 能够提供身份验证 Web 服务。

使用对称算法的优点是它通常非常快。此外,您无需依赖第三方进行密钥管理或为身份担保。

如果您想负责自己的 AES 密钥,您有两种选择。首先,您可以向 Yubico 请求您的 AES 密钥。在撰写本文时,Yubico 将向您发送包含 AES 密钥的 CD,但该公司也在努力寻找更方便的在线检索密钥的解决方案。其次,您可以使用 Yubico 的开发工具包自行编程密钥。通过这种方式,您可以根据自己的命名约定分配 AES-128 密钥以及公共 ID 和秘密 ID。如果您通过运行自己的身份验证 Web 服务来补充这种方法,您就可以消除在身份验证过程中对 Yubico 作为第三方的任何依赖。

验证算法:顺序至关重要

毫不奇怪,验证 OTP 的过程类似于反转构造 OTP 所需的步骤。基本的验证例程可能如下所示。首先,您对字符串进行 ModHex 解码。接下来,您将字符串拆分为公共 ID 和 16 字节令牌。然后,您使用公共 ID 查找相应的 AES 密钥。在使用 AES 密钥解密后,您将获得原始的 16 字节明文令牌。接下来,您将验证 CRC-16 校验和(最后两个字节)。然后,您将秘密 ID 与您使用公共 ID 从数据库中检索到的秘密 ID 进行比较。使用会话计数器和会话令牌计数器,确保当前令牌是在上次成功验证的令牌之后生成的。尽管您不确切知道任何两个令牌是何时生成的,但您始终可以判断它们的生成顺序。如果令牌通过所有这些测试,您可以向客户端发送响应,表示验证成功。否则,令牌将被拒绝。

可选地,您可以进一步加强验证算法。例如,您可以尝试计算自上次成功验证以来跳过了多少会话或令牌,并在您决定验证或拒绝令牌时考虑该信息。您可以以类似的方式使用会话时间戳。

Yubico 的开源方法

我发现 Yubico 的商业模式真正有吸引力的一点是,它尝试以开源形式提供所有软件。根据 Yubico 的声明,它计划从设备的制造和销售中获利,但打算保持所有软件开源。例如,上述 Web 服务的源代码可以作为参考实现免费获得。此外,Yubico 还提供在各种应用程序和平台上实现 Yubikey 身份验证所需的客户端库。目前,有 Java、C、C#/.NET、PAM、PHP、Ruby、Perl 和 Python 版本的客户端库可用。所有这些库和程序都设置为 Google Code 项目。此外,还有用于在 C 和 Java 中解密 OTP 的库项目,以及 Open ID 服务器和个性化工具,允许您编程自己的 Yubikey。尽管所有这些软件项目都是由 Yubico 发起的,但您已经可以看到其他人也在做出贡献。此外,许多使用 Yubikey 技术的独立开源项目也已浮出水面。Yubico 的讨论论坛是了解此类项目并获得支持的好地方。

Yubico 身份验证服务

当您订购 Yubikey 时,它已准备好利用 Yubico 的身份验证 Web 服务。由于 Yubico 维护着所有 API 密钥以及公共 ID 和秘密 ID 的数据库,这些 ID 在发货前已用于编程 Yubikey,因此 Yubico 决定提供针对这些凭据的身份验证 Web 服务。开发人员随后可以使用 Yubico 身份验证 Web 服务来验证从设备捕获的 OTP。Yubico 有一个网页,您可以在其中请求 API 密钥。任何人都可以获得 API 密钥。唯一的要求是您必须提交有效的 Yubikey OTP。这仅仅是为了避免数据库因过多虚假请求而膨胀的措施。API 密钥还附带一个 ID 编号。

API 密钥的目的是使用 HMAC-SHA1 哈希算法对往返 Yubico 身份验证 Web 服务的请求进行签名/验证。这样做是因为在 Web 服务客户端库必须运行的各种环境中,对 SSL 的支持通常是虚假的。请注意,严格来说没有必要使用 SSL,因为令牌已经加密!但是,作为额外的预防措施,只要可用,就应将 SSL 用作传输层。例如,在 PHP 客户端库中,您所要做的就是在指定身份验证服务器 URL 的 http 中添加一个 s。

为 Typo 添加 Yubikey 身份验证

现在我们对底层技术有了扎实的了解,让我们将 Yubikey 身份验证添加到现有应用程序中。我使用 Typo 写博客。Typo 是使用 Ruby on Rails 开发的,您可以通过该项目的公共 Subversion 存储库查看其最新的代码库。无论您是否喜欢 RoR 强加给开发人员的结构,在这种情况下它都对我们有利,因为它使我们可以轻松找到我们需要修改的文件。请看图 5,了解我们将要实现的验证例程的基本轮廓。

Yubikey One-Time Password Authentication

图 5. Yubikey OTP 验证流程

首先,让我们将 Ruby Web 服务客户端库 yubico.rb 放到项目的 lib 目录中。在将相应的 require 命令添加到 config/environments.rb 文件后,我们可以确保该库在整个应用程序中都可用。

配置 Yubikey 身份验证需要两组设置。首先,是站点范围的设置,即 API 密钥和相应的 ID,用于向 Web 服务提交身份验证请求。还有一个用于在博客范围内启用或禁用 Yubikey 身份验证的开关。Typo 通过序列化这些特定于博客的设置并将它们持久化到 blogs.settings 列中来存储它们。对我们来说幸运的是,这意味着我们不必对数据库进行任何更改。但是,我们确实需要修改用于在应用程序中存储这些设置的 UI 和数据模型。列表 1 显示了如何在管理用户界面中将这三个 Yubikey 配置选项添加到相应的 HTML 模板中。类似地,列表 2 显示了如何将相同的设置添加到模型中。这就是 Rails 为每个博客呈现用于输入这些设置并将其存储在数据库中的表单所需的全部内容。图 6 显示了最终结果。

列表 1. Typo:博客范围的 Yubikey 设置 HTML

filename: app/views/admin/settings/index.html.erb

...
<!-- Yubikey authentication - start -->
<fieldset id="authentication" class="set" style="margin-top:10px;">
  <legend><%= _("Authentication")%></legend>
  <ul>
    <li>
      <label class="float"><%= _("Require Yubikey OTP")%>:</label>
      <input name="setting[yubikey_required]"
          id="yubikey_required" type="checkbox" value="1"
          <%= 'checked="checked"' if this_blog.yubikey_required%> />
      <input name="setting[yubikey_required]" type="hidden"
          value="0" />
    </li>
    <li>
      <label for="yubikey_api_id"
          class="float"><%= _("Yubico API ID")%>:</label>
      <input name="setting[yubikey_api_id]" id="yubikey_api_id"
          type="text" value="<%=h this_blog.yubikey_api_id %>"
          size="6" />
    </li>
    <li>
      <label for="yubikey_api_key"
          class="float"><%= _("Yubico API Key")%>:</label>
      <input name="setting[yubikey_api_key]"
          id="yubikey_api_key" type="text"
          value="<%=h this_blog.yubikey_api_key %>" size="50" />
    </li>
  </ul>
</fieldset>
<!-- Yubikey authentication - end -->
...

列表 2. Typo:向模型添加博客范围的 Yubikey 设置

filename: app/model/blog.rb

...
  # Authentication
  setting :yubikey_required,       :boolean, false
  setting :yubikey_api_id,         :string, ''
  setting :yubikey_api_key,        :string, ''
...
Yubikey One-Time Password Authentication

图 6. Typo:博客范围的 Yubikey 设置 UI

其次,有两个用户特定的设置:Yubikey ID 和 Yubikey Required。前者是必要的,用于将 Typo 帐户与用户的唯一公共 Yubikey ID 相关联;而后者允许用户仅为其帐户选择性地启用 Yubikey 身份验证。现在,让我们从应用程序管理界面中的用户首选项设置中提供这两个选项。为了使新选项出现在 UI 中,我在部分 HTML 模板中添加了一个新部分,该模板呈现用于编辑用户选项的表单(列表 3)。感谢 RoR 的 ActiveRecord 支持,我们不需要编写任何代码将这些新选项保存到数据库中;但是,我们确实需要确保我们将相应命名的字段添加到用户表,屏幕上的所有值都将持久化到该表中。在 Rails 中,这是通过添加数据库迁移来完成的,数据库迁移只不过是描述数据库增量修改的抽象方法。在我们的例子中,我们通过创建列表 4 中显示的迁移来向用户表添加字段 yubikey_id 和 yubikey_required。现在,您需要做的就是从命令行运行 rake 实用程序并告诉它升级数据库rake db:migrate。Rails 迁移的优点在于它们与数据库提供程序无关。我们列表 4 中创建的迁移可以与 Typo 支持的任何底层数据库一起使用。在撰写本文时,这包括 MySQL、PostgreSQL 和 SQLite。最后,您可以在图 7 中欣赏帐户特定选项中的新设置。

列表 3. Typo:帐户特定的 Yubikey 配置选项 HTML

filename: app/views/admin/users/_form.html.erb:

...
<li>
  <label class="float" for="user_notify_on_new_articles"><%=
      _("Send notification messages when new articles are posted")%>?
  </label>
  <%= check_box 'user', 'notify_on_new_articles' %>
</li>
<!-- new options for Yubikey authentication - start -->
<li>
  <label class="float" for="user_yubikey_required"><%=
      _("Yubikey Required")%>?
  </label>
  <%= check_box 'user', 'yubikey_required' %>
</li>
<li>
  <label class="float" for="user_yubikey_id"><%=
      _("Yubikey ID")%>:
  </label>
  <%= text_field 'user', 'yubikey_id' %>
</li>
<!-- new options for Yubikey authentication - end -->
</ul>
</fieldset>
<!--[eoform:user]-->

列表 4. Typo:Yubikey 设置数据库迁移

filename: db/migrate/071_add_yubikey_columns_to_user.rb:

class AddYubikeyColumnsToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :yubikey_id, :string, 
               :null => false, :default => ''
    add_column :users, :yubikey_required,
               :boolean, :null => false, :default => false
  end

  def self.down
    remove_column :users, :yubikey_id
    remove_column :users, :yubikey_required
  end
end

Yubikey One-Time Password Authentication

图 7. Typo:帐户特定的 Yubikey 配置选项 UI

现在我们已经处理好了所有设置,我们可以专注于登录期间的实际身份验证。首先,让我们在登录屏幕上添加一个 Yubikey OTP 输入字段,前提是整个博客都启用了 Yubikey 身份验证。我已经通过修改列表 5 中呈现登录表单的部分模板来完成此操作。请注意,我们始终必须在登录期间显示 Yubikey OTP 字段,因为在用户提供其用户名之前,我们不知道特定用户是否需要 Yubikey 身份验证。图 8 显示了修改后的登录屏幕。

当登录表单提交时,Rails 会将其路由到 AccountsController 类的 login 方法(列表 6)。这就是我们添加逻辑来检查是否需要处理 Yubikey 身份验证的地方。在现有代码验证了常规登录名和密码之后,我们现在有了一个实例化的用户对象,该对象可以告诉我们该用户是否需要 Yubikey 身份验证。如果是这样,我们调用用户对象的静态方法 authenticate_yubikey。查看列表 7,我们检查登录表单中的 Yubikey OTP 和用户的公共 Yubikey ID 都不为空。此外,根据定义,OTP 的前 12 个字符必须与帐户关联的公共 ID 匹配。如果一切正常,我们将实例化一个 Yubico 对象,它将为我们处理 Web 服务身份验证请求。该方法只返回一个布尔值。True 表示用户已成功通过身份验证。相反,false 表示无效的 OTP 或未经授权的用户尝试 - 可能是尝试入侵帐户。

列表 5. Typo:修改后的登录表单 HTML

filename: app/views/shared/_loginform.html.erb:

<% form_tag :action=> "login" do %>
<ul>
  <li>
    <label for="user_login"><%= _('Username')%>:</label>
    <input type="text" name="user_login" id="user_login" value=""/>
  </li>
  <li>
    <label for="user_password"><%= _('Password') %>:</label>
    <input type="password" name="user_password" id="user_password" />
  </li>
<!-- Yubikey authentication - start -->
<% if this_blog.yubikey_required %>
  <li>
    <label for="yubikey_otp"><%= _('Yubikey OTP') %>:</label>
    <input type="text" name="yubikey_otp" id="yubikey_otp" />
  </li>
<% end %>
<!-- Yubikey authentication - end -->
  <li class="r"><input type="submit" name="login"
      value= "<%= _('Login') %> &#187;"
      class="primary" id="submit" />
  </li>
</ul>
<p><%= link_to
      "&laquo; " + _('Back to ') + this_blog.blog_name,
      this_blog.base_url %></p>
<% end %>

列表 6. Typo:Yubikey 身份验证第 1 部分

filename: app/controllers/accounts_controller.rb:

...
def login
  case request.method
    when :post
    self.current_user =
      User.authenticate(params[:user_login], params[:user_password])

    # check whether Yubikey authentication is required and perform
    # authentication
    if logged_in? &&
           (!this_blog.yubikey_required ||
            !self.current_user.yubikey_required ||
            self.current_user.authenticate_yubikey(
                this_blog,
                self.current_user.yubikey_id,
                params[:yubikey_otp]))
      session[:user_id] = self.current_user.id

      flash[:notice]  = _("Login successful")
      redirect_back_or_default :controller => "admin/dashboard",
                               :action => "index"
    else
      flash.now[:notice]  = _("Login unsuccessful")
      @login = params[:user_login]
    end
  end
end
...

列表 7. Typo:Yubikey 身份验证第 2 部分

filename: app/model/user.rb

...
  # Authenticate a user's Yubikey ID.
  #
  # Example:
  #   @user.authenticate_yubikey(this_blog, 'thcrefhcvijl',
  #   'thcrefhcvijldvlfugbhrghkibjigdbunhjlfnbtvfbc')
  #
  def authenticate_yubikey(this_blog,
                           yubikey_id = '', yubikey_otp = '')
    if (yubikey_id.empty? ||
        yubikey_otp.empty? ||
        !yubikey_otp[0, 12].eql?(yubikey_id))
      return false
    else
      begin
        yk = Yubico.new(this_blog.yubikey_api_id,
                        this_blog.yubikey_api_key)
        return yk.verify(yubikey_otp).eql?('OK')
      rescue
        return false
      end
    end
  end
...
Yubikey One-Time Password Authentication

图 8. Typo:修改后的登录表单 UI

就这样!我的 Typo 博客现在已启用 Yubikey。我将提交一个补丁,以使这些更改永久化,方法是将它们集成到 Typo 代码库中。

实现变体

在实现 Yubikey 身份验证时,您可能需要考虑一些变体。首先,您可以选择省略用户名,因为 Yubikey 令牌已经包含一个公共 ID,该 ID 可用于链接到用户的帐户。只要您不允许用户将单个 Yubikey 与多个帐户关联,此方案就可以工作。

其次,您可以通过将 Yubikey 令牌包含在密码字段中来最大限度地减少对现有系统 UI 所需的修改。由于 OTP 的长度是固定的,因此有理由认为其余字符属于密码。此外,由于 Yubikey 在令牌后附加了一个换行符,因此用户必须先键入密码,然后再键入 OTP,而不是反过来。

第三,您可能需要考虑使登录成为一个两步过程。首先,提示用户输入 OTP 并验证它。如果验证请求获得批准,则提示用户输入常规登录名和密码。要了解此方法的优势,请考虑同时提交用户名、密码和 OTP 的情况。如果恶意方能够拦截提交并阻止 OTP 提交到验证服务器,则他们实际上拥有渗透您尝试验证身份的系统所需的所有三条信息。但是,如果您仅在登录过程的第一步中提交 OTP,则恶意方可以拦截令牌,而不会访问系统,因为他们没有相应的用户名和密码。为了让您提供用户名和密码,他们需要让 OTP 通过并被验证,这也使得 OTP 对于后续使用变得无用。因此,攻击者的任务将变得非常复杂。

实际应用中的 Yubikey

在其网站上,Yubico 维护着一个不断增长的应用程序和服务列表,这些应用程序和服务利用了 Yubikey。有 WordPress 插件、SSH 集成、phpBB 论坛访问和 Windows 登录(商业测试版)。正如上面将 Yubikey 集成到 Typo 博客软件的身份验证例程中的示例所示,该过程相当简单。希望本文能启发您以此为起点,通过添加 Yubikey 身份验证,使您最喜欢的开源软件更加安全。

资源

Yubico 的 Yubikey 页面: www.yubico.com/products/yubikey

支持 Yubikey 的应用: yubico.com/products/apps

RoundCube 基于 Web 的电子邮件客户端: www.roundcube.net

Typo 博客软件: www.typosphere.org

Dirk Merkel 是 Vivantech Inc. 的首席技术官。在业余时间,他喜欢通过提交未经请求的补丁来“破坏”原本优秀的开源项目。他还撰写关于 Web 开发的文章。他和他可爱的妻子以及两个出色的女儿住在圣地亚哥。可以通过 dmerkel@vivantech.com 联系 Dirk。

加载 Disqus 评论