Solaris 开发者安全性指南

第 4 章 编写使用 GSS-API 的应用程序

通用安全服务应用编程接口 (Generic Security Service Application Programming Interface, GSS-API) 为应用程序提供了一种用于保护发送到对等应用程序的数据的方法。通常,连接是指从一台计算机上的客户机到另一台计算机上的服务器。本章介绍了有关以下主题的信息:

GSS-API 介绍

使用 GSS-API,程序员在编写应用程序时,可以应用通用的安全机制。开发者不必针对任何特定的平台、安全机制、保护类型或传输协议来定制安全实现。使用 GSS-API,程序员可忽略保护网络数据方面的细节。使用 GSS-API 编写的程序在网络安全方面具有更高的可移植性。这种可移植性是通用安全服务 API 的一个特点。

GSS-API 是一个以通用方式为调用方提供安全服务的框架。许多底层机制和技术(如 Kerberos v5 或公钥技术)都支持 GSS-API 框架,如下图中所示。

图 4–1 GSS-API 层

该图显示了应用程序和安全机制之间的 GSS-API 和协议层。

从广义上讲,GSS-API 主要具有以下两种功能:

  1. GSS–API 可创建一个安全上下文,应用程序可在该上下文中相互传递数据。上下文是指两个应用程序之间的信任状态。由于共用同一个上下文的应用程序可相互识别,因此可以允许在上下文存在期间进行数据传送。

  2. GSS–API 可向要传送的数据应用一种或多种类型的保护,称为安全服务。安全服务在GSS-API 中的安全服务中进行介绍。

此外,GSS-API 还可执行以下功能:

GSS-API 中包括许多支持函数和便利函数。

使用 GSS-API 的应用程序的可移植性

GSS-API 为应用程序提供了以下几种类型的可移植性:

GSS-API 中的安全服务

GSS-API 提供了三种类型的安全服务:

GSS-API 中的可用机制

当前实现的 GSS-API 可使用以下机制:Kerberos v5TM、Diffie-Hellman 和 SPNEGO。有关 Kerberos 实现的更多信息,请参见《系统管理指南:安全性服务》中的第 20  章 “Kerberos 服务介绍”。如果任何系统上运行了能够识别 GSS-API 的程序,应在该系统上安装和运行 Kerberos v5。

使用 GSS-API 的远程过程调用

针对网络应用程序使用 RPC(Remote Procedure Call,远程过程调用)协议的程序员可以使用 RPCSEC_GSS 来提供安全性。 RPCSEC_GSS 是位于 GSS-API 上面的一个独立层。 RPCSEC_GSS 可提供 GSS-API 的所有功能,但其方式是针对 RPC 进行了调整的。 实际上,RPCSC_GSS 可用于向程序员隐藏 GSS-API 的许多方面,从而使 RPC 安全性具有更强的可访问性和可移植性。 有关 RPCSEC_GSS 的更多信息,请参见《ONC+ Developer’s Guide》中的“Authentication Using RPCSEC_GSS”

下图说明了 RPCSEC_GSS 层在应用程序和 GSS-API 之间的位置。

图 4–2 RPCSEC_GSS 和 GSS-API

该图显示了为远程过程调用提供安全性的 RPCSEC_GSS 层。

GSS-API 的限制

虽然 GSS-API 使数据保护变得很简单,但是它回避某些任务的做法并不符合 GSS-API 的一般本性。 因此,GSS-API 执行以下活动:

GSS-API 的语言绑定

本文档目前仅介绍 GSS-API 的 C 语言绑定,即函数和数据类型。 现已推出 Java 绑定版本的 GSS-API。 Java GSS-API 包含通用安全服务应用编程接口 (Generic Security Service Application Program Interface, GSS-API) 的 Java 绑定,如 RFC 2853 中所定义。

有关 GSS-API 的更多参考信息

以下两个文档提供有关 GSS-API 的更多信息:

GSS-API 的重要元素

本节介绍了以下 GSS-API 的重要概念:主体、GSS-API 数据类型、状态码和令牌。

GSS-API 数据类型

以下几节介绍了主要的 GSS-API 数据类型。 有关全部 GSS-API 数据类型的信息,请参见GSS-API 数据类型和值

GSS-API 整数

由于 int 的长度因平台而异,因此 GSS-API 提供了以下整数数据类型:OM_uint32,此为 32 位无符号整数。

GSS-API 中的字符串和类似数据

由于 GSS-API 处理的是内部格式的数据,因此在将字符串传递到 GSS-API 函数之前必须将其转换为 GSS-API 格式。 GSS-API 可处理具有 gss_buffer_desc 结构的字符串:

typedef struct gss_buffer_desc_struct {

     size_t     length;

     void       *value;

}  gss_buffer_desc *gss_buffer_t;

gss_buffer_t 是指向此类结构的指针。 在将字符串传递到使用它们的函数之前,必须先将其放到 gss_buffer_desc 结构中。 在以下示例中,通用的 GSS-API 函数在发送消息之前会先向此消息应用保护机制。


示例 4–1 在 GSS-API 中使用字符串

char *message_string;

gss_buffer_desc input_msg_buffer;



input_msg_buffer.value = message_string;

input_msg_buffer.length = strlen(input_msg_buffer.value) + 1;



gss_generic_function(arg1, &input_msg_buffer, arg2...);



gss_release_buffer(input_msg_buffer);

请注意,完成对 input_msg_buffer 的操作之后,必须通过 gss_release_buffer() 解除对 input_msg_buffer 的分配。

gss_buffer_desc 对象不只是用于字符串。 例如,令牌可以作为 gss_buffer_desc 对象进行处理。有关更多信息,请参见GSS-API 令牌

GSS-API 中的名称

名称是指主体。 在网络安全术语中,主体是指用户、程序或计算机。 主体可以是客户机或服务器。 下面是主体的一些示例:

在 GSS-API 中,名称会存储为 gss_name_t 对象,该对象对于应用程序是不透明的。 名称可以通过 gss_import_name() 函数从 gss_buffer_t 对象转换为 gss_name_t 形式。 所导入的每个名称都有一个指示名称格式的相关名称类型。 有关名称类型的更多信息,请参见GSS-API OID。 有关有效名称类型的列表,请参见名称类型

gss_import_name() 具有以下语法:

OM_uint32 gss_import_name (

       OM_uint32          *minor-status,

       const gss_buffer_t input-name-buffer,

       const gss_OID      input-name-type,

       gss_name_t         *output-name)
minor-status

基础机制返回的状态码。请参见GSS-API 状态码

input-name-buffer

包含要导入的名称的 gss_buffer_desc 结构。 应用程序必须明确分配此结构。 请参见GSS-API 中的字符串和类似数据示例 4–2。 应用程序不再使用为该参数分配的空间时,必须通过 gss_release_buffer() 解除分配该空间。

input-name-type

用于指定 input-name-buffer 的格式的 gss_OID。请参见GSS-API 中的名称类型。 另外,名称类型中还包含一个有效名称类型表。

output-name

接收名称的 gss_name_t 结构。

示例 4–1 中所示的通用示例进行了小修改,说明如何使用 gss_import_name()。 首先,将规则字符串插入到 gss_buffer_desc 结构中, 然后,使用 gss_import_name() 将该字符串放到 gss_name_t 结构中。


示例 4–2 使用 gss_import_name()

char *name_string;

gss_buffer_desc input_name_buffer;

gss_name_t      output_name_buffer;



input_name_buffer.value = name_string;

input_name_buffer.length = strlen(input_name_buffer.value) + 1;



gss_import_name(&minor_status, input_name_buffer, 

                    GSS_C_NT_HOSTBASED_SERVICE, &output_name);



gss_release_buffer(input_name_buffer);

使用 gss_display_name() 可以将所导入的名称放回到 gss_buffer_t 对象中,以便能够以可读格式显示。 但是,鉴于基础机制存储名称的方式,gss_display_name() 不能保证所得到的字符串与原来的字符串相同。 GSS-API 包括若干个用于处理名称的其他函数。请参见GSS-API 函数

gss_name_t 结构可以包含一个名称的多个版本。 可以为 GSS-API 所支持的每个机制都生成一个版本。 也即是说,user@companygss_name_t 结构可能会包含该名称的两个版本:一个版本由 Kerberos v5 提供,另一个版本由其他机制提供。 gss_canonicalize_name() 函数将内部名称和机制用作输入, gss_canonicalize_name() 可生成另一个内部名称,其中包含一个特定于该机制的名称版本。

此类特定于机制的名称称为机制名称 (Mechanism Name, MN)。 机制名称是指指定机制所生成的主体名称,而不是指机制的名称。 下图所示对此过程进行了说明。

图 4–3 内部名称和机制名称

该图说明如何派生机制名称。

在 GSS-API 中比较名称

请考虑以下情况:服务器从客户机收到一个名称,并且需要在访问控制列表中查找该名称。 访问控制列表(即 ACL)是指具有特定访问权限的主体的列表。 下面是一种用于进行查找的方法:

  1. 使用 gss_import_name() 以 GSS-API 内部格式导入客户机名称(如果该名称尚未导入)。

    在某些情况下,服务器将以内部格式接收名称,因此可不必执行该步骤。 例如,服务器可能会查找客户机本身的名称。 在启动上下文的过程中,客户机本身的名称以内部格式进行传递。

  2. 使用 gss_import_name() 将每个名称导入到 ACL 中。

  3. 使用 gss_compare_name() 将所导入的每个 ACL 名称与所导入的客户机名称进行比较。

此过程如下图所示。 在本示例中,假设步骤 1 是必需的。

图 4–4 比较名称(慢速)

该图说明如何使用 gss_compare_name 函数比较客户机的内部名称。

如果只有少数名称,则可以接受使用上述单独比较名称的方法。 如果有大量名称,则使用 gss_canonicalize_name() 函数会更高效。 此方法会执行以下步骤:

  1. 使用 gss_import_name() 导入客户机的名称(如果该名称尚未导入)。

    和上述比较名称的方法一样,如果名称已经采用内部格式,则不必执行此步骤。

  2. 使用 gss_canonicalize_name() 生成机制名称版本的客户机名称。

  3. 使用 gss_export_name() 生成所导出的名称,这是连续字符串形式的客户机名称。

  4. 使用 memcmp() 对所导出的客户机名称与 ACL 中的每个名称进行快速比较,该函数的开销较低。

此过程如下图所示。 同样,再次假设服务器需要导入从客户机收到的名称。

图 4–5 比较名称(快速)

该图说明如何使用 memcmp 函数比较客户机的内部名称。

由于 gss_export_name() 需要使用机制名称 (mechanism name, MN),因此 必须首先针对客户机名称运行 gss_canonicalize_name()

有关更多信息,请参见 gss_export_name(3GSS)gss_import_name(3GSS)gss_canonicalize_name(3GSS)

GSS-API OID

对象标识符 (Object identifier, OID) 用于存储以下几种数据:

OID 存储在 GSS-API gss_OID_desc 结构中。 GSS-API 提供了指向 gss_OID 结构的指针,如以下示例中所示。


示例 4–3 OID 结构

typedef struct gss_OID_desc_struct {

        OM_uint32   length;

        void        *elements;

     } gss_OID_desc, *gss_OID;

并且,gss_OID_set_desc 结构中可能会包含一个或多个 OID。


示例 4–4 OID 集的结构

typedef struct gss_OID_set_desc_struct {

        size_t    count;

        gss_OID   elements;

     } gss_OID_set_desc, *gss_OID_set;


注意 – 注意 –

应用程序不应尝试使用 free() 解除分配 OID。


GSS-API 中的机制和 QOP

尽管 GSS-API 允许应用程序选择基础安全机制,但是应用程序还是应当尽可能使用 GSS-API 已选择的缺省机制。 同样,尽管 GSS-API 允许应用程序指定用于保护数据的保护质量级别,但还是应当尽可能使用缺省 QOP。 可以通过向需要使用机制或 QOP 作为参数的函数传递值 GSS_C_NULL_OID 来表示接受缺省机制。


注意 – 注意 –

明确指定安全机制或 QOP 会背离使用 GSS-API 的初衷。 类似的特定选择会限制应用程序的可移植性。 其他 GSS-API 实现可能无法以预期方式支持 QOP 或机制。 尽管如此,附录 C,指定 OID 中还是简要讨论了如何确定可以使用的机制和 QOP 以及如何进行选择。


GSS-API 中的名称类型

除了 QOP 和安全机制以外,OID 还用于指示名称类型,从而指明关联名称的格式。 例如,gss_import_name() 函数(用于将主体的名称从字符串转换为 gss_name_t 类型)将需要转换的字符串的格式用作一个参数。 例如,如果名称类型为 GSS_C_NT_HOSTBASED_SERVICE,则该函数便会知道所输入的名称是采用 service@host 形式。 如果名称类型为 GSS_C_NT_EXPORT_NAME,则该函数需要使用 GSS-API 导出的名称。 应用程序可以使用 gss_inquire_names_for_mech() 函数来确定指定机制可用的名称类型。 名称类型中提供了 GSS-API 所使用的名称类型的列表。

GSS-API 状态码

所有 GSS-API 函数都会返回两种类型的代码,其中提供了有关该函数执行成败与否的信息。 这两种类型的状态码都以 OM_uint32 值的形式返回。 这两种类型的返回码如下所示:

GSS-API 令牌

GSS-API 中的基本“流通”(currency) 单位是令牌 (token)。 使用 GSS-API 的应用程序可以借助令牌来相互通信。 使用令牌可以交换数据并创建安全布局。 令牌声明为 gss_buffer_t 数据类型。 令牌对于应用程序是不透明的。

令牌的类型包括上下文级别的令牌每消息令牌两种。 上下文级别的令牌主要在建立(即启动和接受)上下文时使用。 另外,还可以在以后通过传递上下文级别的令牌来管理上下文。

每消息令牌在建立了上下文之后使用。 每消息令牌用于为数据提供保护服务。 例如,请考虑一个希望将消息发送到另一个应用程序的应用程序。 该应用程序可能会使用 GSS-API 来生成随该消息传递的加密标识符。 该标识符可存储在令牌中。

可以考虑按如下方式针对消息使用每消息令牌。 消息是应用程序发送到其对等应用程序的一段数据。 例如,ls 命令可以是发送到 ftp 服务器的消息。 每消息令牌是 GSS-API 为该消息生成的对象。 每消息令牌可以是加密标记,也可以是加密形式的消息。 请注意,最后一个示例不太准确。 加密消息仍是消息,而不是令牌。 令牌是指 GSS-API 生成的信息。 但是,在非正式情况下,消息每消息令牌通常可以互换使用。

应用程序负责进行以下活动:

  1. 收发令牌。 开发者通常需要编写一般性的读写函数来执行这些操作。 send_token()recv_token() 函数在各种 GSS-API 样例函数中进行介绍。

  2. 区分不同类型的令牌并相应地处理这些令牌。

    由于令牌对于应用程序来说是不透明的,因此应用程序不会对不同的令牌加以区分。 如果不知道令牌的内容,则应用程序必须能够区分令牌的类型才能将令牌传递到相应的 GSS-API 函数。 应用程序可以通过以下方法来区分令牌类型:

    • 按状态。 通过程序的控制流程。 例如,等待接受上下文的应用程序可能会假设收到的任何令牌都与所建立的上下文有关。 对等应用程序应当等到上下文完全建立之后才发送消息令牌(即数据)。 建立上下文之后,正在等待接受上下文的应用程序会假设新令牌即是消息令牌。 这是处理令牌的相当常见的方法。 本书中的样例程序会使用此方法。

    • 按标志。 例如,如果某个应用程序包含用于向对等应用程序发送令牌的函数,则该应用程序可以引入一个标志,用于指示令牌类型。 请考虑以下代码:

      gss_buffer_t token;     /* declare the token */
      
      OM_uint32 token_flag       /* flag for describing the type of token */
      
      
      
      <get token from a GSS-API function>
      
      
      
      token_flag = MIC_TOKEN;     /* specify what kind of token it is */
      
      send_a_token(&token, token_flag);

      接收应用程序可包含用于检查 token_flag 参数的接收函数,例如 get_a_token()

    • 通过明确使用标记。 应用程序可以使用元令牌。 元令牌是一种用户定义的结构,其中包含从 GSS-API 函数收到的令牌。 元令牌包括用户定义的字段,这些字段指示如何使用 GSS-API 提供的令牌。

GSS-API 中的进程间令牌

GSS-API 允许在多进程应用程序中的进程之间传递安全上下文。 通常,应用程序已经接受了客户机的上下文, 然后,应用程序将在其进程之间共享该上下文。 有关多进程应用程序的信息,请参见在 GSS-API 中导出和导入上下文

gss_export_context() 函数可用于创建进程间令牌。 使用此令牌中包含的信息,另一个进程可以重建该上下文。 应用程序负责在进程之间传递进程间令牌, 这与应用程序负责将令牌传递到其他应用程序情况相似。

进程间令牌可能包含密钥或其他敏感信息。 并非所有的 GSS-API 实现都以加密方式保护进程间令牌。 因此,在进行交换之前,应用程序必须保护进程间令牌。 这种保护可能涉及到使用 gss_wrap() 对令牌进行加密(如果加密机制可用)。


注 –

请勿假设进程间令牌可以在不同的 GSS-API 实现之间传送。


开发使用 GSS-API 的应用程序

本节说明如何使用 GSS-API 来实现安全的数据交换。 本节将重点介绍那些对于使用 GSS-API 至关重要的函数。 有关更多信息,请参见附录 B,GSS-API 参考,其中包含所有 GSS-API 函数、状态码和数据类型的列表。 要查找有关任何 GSS-API 函数的更多信息,请检查相应的手册页。

本手册中的示例遵循一个简单的模型。 客户机应用程序将数据直接发送到远程服务器, 而无需通过传输协议层(如 RPC)进行中间调用。

GSS-API 的一般用法

下面是使用 GSS-API 的一般步骤:

  1. 每个应用程序(无论是发送者还是接受器)都明确获取凭证,除非已经自动获取凭证。

  2. 发送者启动一个安全上下文, 接受器接受该上下文。

  3. 发送者向要传送的数据应用安全保护机制。 发送者会对消息进行加密或者使用标识标记对数据进行标记。 发送者随后将传送受保护的消息。


    注 –

    发送者可以选择不应用安全保护机制,在这种情况下,消息仅具有缺省的 GSS-API 安全服务,即验证。


  4. 接受器根据需要对消息进行解密,并在适当的情况下对消息进行验证。

  5. (可选)接受器将标识标记返回到发送者进行确认。

  6. 这两个应用程序都会销毁共享的安全上下文。 如有必要,分配功能还可以解除分配其余任何 GSS-API 数据。


注意 – 注意 –

调用应用程序负责释放已经分配的所有数据空间。


使用 GSS-API 的应用程序需要包含头文件 gssapi.h

在 GSS-API 中使用凭证

凭证是指向主体名称提供应用程序声明证明的数据结构。 应用程序使用凭证来建立其全局标识。 此外,凭证还可用于确认实体的权限。

GSS-API 不提供凭证。 凭证由那些以 GSS-API 为基础的安全机制在调用 GSS-API 函数之前创建。 在许多情况下,用户会在登录时接收凭证。

指定的 GSS-API 凭证对于单个主体有效。 单个凭证可以包含该主体的多个元素,每个元素由不同的机制创建。 如果某个凭证是在包含多个安全机制的计算机上获取的,则该凭证在传输到包含其中部分机制的计算机上时有效。 GSS-API 通过 gss_cred_id_t 结构访问凭证。 此结构称为凭证句柄。 凭证对于应用程序是不透明的。 因此,应用程序无需知道指定凭证的具体信息。

凭证采用以下三种形式:

在 GSS-API 中获取凭证

服务器和客户机都必须获取各自的凭证,然后才能建立安全上下文。 应用程序可以在凭证到期之前重用它,在到期之后必须重新获取凭证。 客户机使用的凭证与由服务器使用的凭证可以具有不同的生命周期。

基于 GSS-API 的应用程序可以通过以下两种方法获取凭证:

大多数情况下,仅有上下文接受器(即服务器)才能调用 gss_acquire_cred()。 上下文启动器(即客户机)通常在登录时接收凭证。 因此,客户机通常会指定缺省凭证。 服务器也可以跳过 gss_acquire_cred() 而使用其缺省凭证。

客户机的凭证用于向其他进程证明该客户机的身份。 服务器在获取凭证后即可接受安全上下文。 因此,如果某个客户机向服务器发出 ftp 请求,则表明该客户机可能已经在登录时获取了凭证。 客户机尝试启动上下文时,GSS-API 会自动检索凭证。 但是,服务器程序可以明确获取所请求服务 (ftp) 的凭证。

如果 gss_acquire_cred() 成功完成,则将返回 GSS_S_COMPLETE。 如果不能返回有效的凭证,则将返回 GSS_S_NO_CRED。 有关其他错误代码,请参见 gss_acquire_cred(3GSS) 手册页。 有关相应的示例,请参见第 8 章中的“获取凭证”。

gss_add_cred()gss_acquire_cred() 相似。 但是,gss_add_cred() 允许应用程序使用现有的凭证来创建新句柄或者添加新的凭证元素。 如果将 GSS_C_NO_CREDENTIAL 指定为现有的凭证,则 gss_add_cred() 会根据缺省行为创建新凭证。 有关更多信息,请参见 gss_add_cred(3GSS) 手册页。

在 GSS-API 中使用上下文

在提供安全性方面,GSS-API 最重要的两个任务就是创建安全上下文和保护数据。 应用程序获取必要的凭证之后,必须建立安全上下文。 建立上下文时会涉及到两个应用程序,一个应用程序(通常是客户机)用于启动上下文,另一个应用程序(通常是服务器)用于接受上下文。 允许在对等应用程序之间使用多个上下文。

相互通信的应用程序通过交换验证令牌来建立共同的安全上下文。 安全上下文是一对 GSS-API 数据结构,其中包含要在两个应用程序之间共享的信息。 这些信息从安全的角度描述了每个应用程序的状态。 安全上下文是保护数据所必需的。

在 GSS-API 中启动上下文

gss_init_sec_context() 函数用于启动应用程序和远程服务器之间的安全上下文。 如果成功,该函数将返回一个上下文句柄以建立上下文,还将返回一个要发送到接受器的上下文级别的令牌。 调用 gss_init_sec_context() 之前,客户机应当执行以下任务:

  1. 使用 gss_acquire_cred() 获取凭证(如有必要)。 通常,客户机会在登录时接收凭证。gss_acquire_cred() 只能从正在运行的操作系统中检索初始凭证。

  2. 使用 gss_import_name() 以 GSS-API 内部格式导入服务器的名称。 有关名称和 gss_import_name() 的更多信息,请参见GSS-API 中的名称

调用 gss_init_sec_context() 时,客户机通常会传递以下参数值:

应用程序不一定要使用这些缺省值。 此外,客户机还可以使用 req_flags 参数指定其他安全参数的要求。 下面描述了完整的 gss_init_sec_context() 参数集。

上下文接受器可能需要多次握手才能建立上下文, 也即是说,接受器会要求启动器先发送多段上下文信息,然后再建立完整的上下文。 因此,为了实现可移植性,应始终在检查上下文是否已完全建立的循环过程中启动上下文。

如果上下文不完整,则 gss_init_sec_context() 将返回一个主状态码 GSS_C_CONTINUE_NEEDED。 因此,循环应当使用 gss_init_sec_context() 的返回值来测试是否继续执行启动循环操作。

客户机将上下文信息以输出令牌形式传递到服务器,输出令牌通过 gss_init_sec_context() 返回。 客户机可接收从服务器返回的输入令牌形式的信息。 以后调用 gss_init_sec_context() 时,输入令牌可以作为参数传递。 但是,如果所收到的输入令牌的长度为零,则表明服务器不再需要其他输出令牌。

因此,除了检查 gss_init_sec_context() 的返回状态以外,该循环还应当检查输入令牌的长度。 如果长度是非零值,则需要向服务器发送其他令牌。 开始循环之前,应将输入令牌的长度初始化为零。 请将输入令牌设置为 GSS_C_NO_BUFFER 或者将结构的长度字段设置为零值。

下面的伪代码举例说明如何从客户端建立上下文:

context = GSS_C_NO_CONTEXT

input token = GSS_C_NO_BUFFER



do



     call gss_init_sec_context(credential, context, name, input token, 

                                           output token, other args...)



     if (there's an output token to send to the acceptor)

          send the output token to the acceptor

          release the output token



     if (the context is not complete)

          receive an input token from the acceptor



     if (there's a GSS-API error)

          delete the context



until the context is complete

错误检查范围越大,实际循环将越完整。 有关此类上下文启动循环的实际示例,请参见建立与服务器的安全上下文。 此外,gss_init_sec_context(3GSS) 手册页还提供了比较特殊的示例。

通常,在上下文未完全建立时返回的参数值即是那些会在上下文完整建立时返回的值。 有关更多信息,请参见 gss_init_sec_context() 手册页。

如果 gss_init_sec_context() 成功完成,则将返回 GSS_S_COMPLETE。 如果对等应用程序需要上下文建立令牌,则将返回 GSS_S_CONTINUE_NEEDED。 如果出现错误,则将返回 gss_init_sec_context(3GSS) 手册页中所示的错误代码。

如果上下文启动失败,则客户机会断开与服务器的连接。

在 GSS-API 中接受上下文

建立上下文的另一方面是接受上下文,这可通过 gss_accept_sec_context() 函数来完成。 通常情况下,服务器接受客户机使用 gss_init_sec_context() 已启动的上下文。

gss_accept_sec_context() 的主要输入是来自启动器的输入令牌。 启动器会返回一个上下文句柄,以及一个要返回到启动器的输出令牌。 但是,服务器应当首先获取客户机所请求服务的凭证,然后才能调用 gss_accept_sec_context()。 服务器使用 gss_acquire_cred() 函数获取这些凭证。 或者,服务器可以指定在调用 gss_accept_sec_context() 时的缺省凭证(即 GSS_C_NO_CREDENTIAL),从而无需明确获取凭证。

调用 gss_accept_sec_context() 时,服务器可以按如下方式设置以下参数:

以下几段将介绍完整的 gss_accept_sec_context() 参数集。

建立安全上下文可能需要几次握手。 启动器和接受器通常需要先发送多段上下文信息,然后才能建立完整的上下文。 因此,为了实现可移植性,应始终在检查上下文是否已完全建立的循环过程中接受上下文。 如果上下文尚未建立,则 gss_accept_sec_context() 将返回一个主状态码 GSS_C_CONTINUE_NEEDED。 因此,循环应当使用 gss_accept_sec_context() 的返回值来测试是否继续执行接受循环操作。

上下文接受器将上下文信息以输出令牌形式返回到启动器,输出令牌通过 gss_accept_sec_context() 返回。 以后,接受器可以从启动器接收输入令牌形式的其他信息。 以后调用 gss_accept_sec_context() 时,输入令牌将作为参数传递。 gss_accept_sec_context() 不再向启动器发送令牌时,将返回一个长度为零的输出令牌。 除了检查返回状态 gss_accept_sec_context() 以外,循环还应当检查输出令牌的长度,查看是否必须发送其他令牌。 开始循环之前,应将输出令牌的长度初始化为零。 请将输出令牌设置为 GSS_C_NO_BUFFER 或者将结构的长度字段设置为零值。

下面的伪代码举例说明如何从服务器端建立上下文:

context = GSS_C_NO_CONTEXT

output token = GSS_C_NO_BUFFER



do



     receive an input token from the initiator



     call gss_accept_sec_context(context, cred handle, input token,

                                                output token, other args...)



     if (there's an output token to send to the initiator)

          send the output token to the initiator

          release the output token



     if (there's a GSS-API error)

          delete the context



until the context is complete

错误检查范围越大,实际循环将越完整。有关此类上下文接受循环的实际示例,请参见建立与服务器的安全上下文。此外,gss_accept_sec_context() 手册页还提供了一个示例。

同样,GSS-API 不会收发令牌,令牌必须由应用程序进行处理。有关令牌传送函数的示例可以在各种 GSS-API 样例函数中找到。

gss_accept_sec_context() 在成功完成时将返回 GSS_S_COMPLETE。如果上下文不完整,该函数将返回 GSS_S_CONTINUE_NEEDED。如果出现错误,该函数将返回错误代码。有关更多信息,请参见 gss_accept_sec_context(3GSS) 手册页。

在 GSS-API 中使用其他上下文服务

gss_init_sec_context() 函数允许应用程序请求超出所建立基本上下文范围的其他数据保护服务。可通过 gss_init_sec_context()req_flags 参数来请求这些服务。

并非所有的机制都提供所有这些服务。gss_init_sec_context()ret_flags 参数用于指示指定上下文中可用的服务。同样,上下文接受器也会通过检查 gss_accept_sec_context() 返回的 ret_flags 值来确定可用的服务。这些服务将在以下几节中进行介绍。

在 GSS-API 中授予凭证

如果允许的话,上下文启动器可以请求由上下文接受器充当代理。在此类情况下,接受器可以代表启动器启动进一步的上下文。

假设计算机 A 上的某个用户希望通过 rlogin 登录到计算机 B,然后通过 rlogin 从计算机 B 登录到计算机 C。根据所使用的机制,所授予的凭证会将 B 标识为 A,或者将 B 标识为 A 的代理。

如果允许授予的话,可以将 ret_flags 设置为 GSS_C_DELEG_FLAG。接受器可接收所授予的凭证并将其作为 gss_accept_sec_context()delegated_cred_handle 参数。授予凭证不同于导出上下文。请参见在 GSS-API 中导出和导入上下文。二者的一个区别就是应用程序可以将其凭证同时授予多次,而上下文一次只能由一个进程持有。

在 GSS-API 中的对等应用程序之间执行相互验证

将文件传送到 ftp 站点的用户通常无需站点标识的证明。另一方面,需要向应用程序提供信用卡号的用户则需要接收者标识的确切证明。在此类情况下,需要进行相互验证。上下文启动器和上下文接受器都需要证明各自的标识。

上下文启动器可以通过将 gss_init_sec_context() req_flags 参数设置为值 GSS_C_MUTUAL_FLAG 来请求相互验证。如果已经对相互验证进行了授权,则函数会通过将 ret_flags 参数设置为该值来表示授权。如果所请求的相互验证不可用,则启动应用程序会负责进行相应的响应,在此情况下 GSS-API 不会自动终止上下文。另外,即使没有特定的请求,某些机制也会始终执行相互验证。

在 GSS-API 中执行匿名验证

正常使用 GSS-API 时,启动器的标识会在建立上下文的过程中提供给接受器。 但是,上下文启动器会请求不将其标识显示给上下文接受器。

例如,请考虑提供对医疗数据库进行无限制访问的应用程序。 此类服务的客户机可能需要对该服务进行验证。此方法会信任从该数据库中检索的所有信息。例如,出于保密性方面的考虑,客户机可能不希望暴露其标识。

要请求进行匿名验证,请将 gss_init_sec_context()req_flags 参数设置为 GSS_C_ANON_FLAG。要验证匿名验证是否可用,请检查 gss_init_sec_context()gss_accept_sec_context()ret_flags 参数,查看是否返回了 GSS_C_ANON_FLAG

如果匿名验证有效,则针对 gss_accept_sec_context()gss_inquire_context() 返回的客户机名称调用 gss_display_name() 时会生成通用的匿名名称。


注 –

应用程序负责在所请求的匿名验证不被许可时采取相应的措施。在此类情况下,GSS-API 不会终止上下文。


在 GSS-API 中使用通道绑定

对于许多应用程序来说,建立基本的上下文足以确保对上下文启动器进行正确的验证。如果需要实现额外的安全性,则可以使用 GSS-API 所提供的通道绑定功能。通道绑定是用于标识所使用的特定数据通道的标记。具体来说,通道绑定可标识起点和终点,即上下文的启动器和接受器。由于标记特定于始发者应用程序和接收者应用程序,因此此类标记可为有效标识提供更多证明。

gss_channel_bindings_t 数据类型是一个指针,它指向如下所示的作为通道绑定的 gss_channel_bindings_struct 结构。

typedef struct gss_channel_bindings_struct {

OM_uint32       initiator_addrtype;

gss_buffer_desc initiator_address;

OM_uint32       acceptor_addrtype;

gss_buffer_desc acceptor_address;

gss_buffer_desc application_data;

} *gss_channel_bindings_t;

第一个字段表示地址类型,用于标识所发送的启动器地址采用的格式。第二个字段表示启动器的地址。例如,initiator_addrtype 可能会发送到 GSS_C_AF_INET,表示 initiator_address 采用的是 Internet 地址(即 IP 地址)形式。同样,第三和第四个字段分别表示接受器的地址类型和接受器的地址。应用程序可以根据需要使用最后一个字段 application_data。如果不打算使用 application_data,请将 application_data 设置为 GSS_C_NO_BUFFER。如果没有为某个应用程序指定地址,则应当将该应用程序的地址类型字段设置为 GSS_C_AF_NULLADDR通道绑定的地址类型一节提供了有效地址类型值的列表。

地址类型用于表示地址族,而不是表示特定的寻址格式。 对于包含多种替换地址形式的地址族,initiator_addressacceptor_address 字段中必须包含足够的信息才能确定所使用的形式。 除非另行指定,否则应当以网络字节顺序(即地址族的本机字节排序)指定地址。

要建立使用通道绑定的上下文,gss_init_sec_context()input_chan_bindings 参数应当指向所分配的通道绑定结构。该结构的字段将连接到一个八位字节字符串,并将派生一个 MIC。该 MIC 随后会绑定到输出令牌。然后,应用程序会将该令牌发送到上下文接受器。接受器在收到该令牌之后将调用 gss_accept_sec_context()。有关更多信息,请参见在 GSS-API 中接受上下文gss_accept_sec_context() 会针对所收到的通道绑定计算 MIC。如果 MIC 不匹配,gss_accept_sec_context() 随后将返回 GSS_C_BAD_BINDINGS

由于 gss_accept_sec_context() 会返回已传送的通道绑定,因此接受器可以使用这些值来执行安全检查。例如,接受器可以针对保留在安全数据库中的代码字检查 application_data 的值。


注 –

基础机制可能不会为通道绑定信息提供保密性。因此,除非确保能够实现保密性,否则应用程序不应当在通道绑定中包括敏感信息。要测试保密性,应用程序可以检查 gss_init_sec_context()gss_accept_sec_context()ret_flags 参数。值 GSS_C_CONF_FLAGGSS_C_PROT_READY_FLAG 可用于指示保密性。有关 ret_flags 的信息,请参见在 GSS-API 中启动上下文在 GSS-API 中接受上下文


单个机制可以针对出现在通道绑定中的地址和地址类型施加额外的约束。 例如,一个机制可能会验证通道绑定的 initiator_address 字段是否返回到 gss_init_sec_context()。因此,具有可移植性的应用程序应当为地址字段提供正确的信息。如果无法确定正确的信息,则应当将 GSS_C_AF_NULLADDR 指定为地址类型。

在 GSS-API 中导出和导入上下文

GSS-API 提供了用于导出和导入上下文的方法。此方法允许多进程应用程序(通常是上下文接受器)在进程之间传送上下文。例如,接受器可以有两个进程,一个进程侦听上下文启动器,另一个进程使用在上下文中发送的数据。使用 test_import_export_context() 函数一节说明了如何使用这些函数来保存和恢复上下文。

gss_export_sec_context() 函数可创建进程间令牌,其中包含有关所导出上下文的信息。有关更多信息,请参见GSS-API 中的进程间令牌。调用 gss_export_sec_context() 之前,应当将接收令牌的缓冲区设置为 GSS_C_NO_BUFFER

应用程序随后会将令牌传递到另一个进程。新进程将接受该令牌并将其传递到 gss_import_sec_context()。用于在应用程序之间传递令牌的函数通常也可用于在进程之间传递令牌。

一次只能存在一个安全进程实例。gss_export_sec_context() 可取消激活所导出的上下文并将上下文句柄设置为 GSS_C_NO_CONTEXTgss_export_sec_context() 还可解除配置与该上下文相关联的任何进程范围内的资源。如果无法完成对上下文的导出,gss_export_sec_context() 会保持现有的安全上下文不变,并且不返回进程间令牌。

并非所有机制都允许导出上下文。应用程序可以通过检查 gss_accept_sec_context()gss_init_sec_context()ret_flags 参数来确定上下文是否可以导出。如果此标志设置为 GSS_C_TRANS_FLAG,则可以导出上下文。(请参见在 GSS-API 中接受上下文在 GSS-API 中启动上下文。)

图 4–6 说明多进程接受器如何将上下文导出到多个任务。在这种情况下,进程 1 接收和处理令牌。此步骤会将上下文级别的令牌与数据令牌分开,并将令牌传递到进程 2。进程 2 按照应用程序特定的方式处理数据。在该图中,客户机已经从 gss_init_sec_context() 获取了导出令牌。客户机将令牌传递到用户定义的函数 send_a_token(),该函数指示要传送的令牌是上下文级别的令牌还是消息令牌。send_a_token() 将这些令牌传送到服务器。send_a_token() 可能会用于在线程之间传递令牌,但是该图中未显示这一点。

图 4–6 导出上下文:多线程接受器示例

该图说明了多进程接受器如何将上下文令牌和数据令牌分开并将上下文令牌传递到另一个进程。

在 GSS-API 中获取上下文信息

GSS-API 提供了一个 gss_inquire_context(3GSS) 函数,该函数可用于获取有关指定的安全上下文的信息。请注意,上下文无需是完整的上下文。如果提供了上下文句柄,则 gss_inquire_context() 将提供以下有关上下文的信息:

在 GSS-API 中发送受保护的数据

在对等应用程序之间建立上下文之后,即可在发送消息之前对其进行保护。

建立上下文时使用的只是最基本的 GSS-API 保护:验证。根据基础安全机制,GSS-API 提供了另外两个保护级别:

gss_get_mic()gss_wrap() 之间的区别如下图所示:使用 gss_get_mic(),接受器可获取用于指示消息是否保持不变的标记;使用 gss_wrap(),接受器除了获取标记外还可获取经过加密的消息。

图 4–7 gss_get_mic()gss_wrap()

该图对 gss_get_mic 和 gss_wrap 函数进行比较。

要使用哪个函数取决于具体情况。由于 gss_wrap() 包括完整性服务,因此许多程序都使用 gss_wrap()。程序可以测试保密性服务的可用性,随后可以根据保密性服务的可用情况,以应用或不应用保密性的方式调用 gss_wrap()包装和发送消息即是一个示例。但是,由于无需展开使用 gss_get_mic() 的消息,因此所使用的 CPU 周期比 gss_wrap() 所使用的要少。所以,不需要保密性的程序可能会使用 gss_get_mic() 来保护消息。

使用 gss_get_mic() 标记消息

程序可以使用 gss_get_mic() 向消息添加加密 MIC。接收者可以通过调用 gss_verify_mic() 检查消息的 MIC。

gss_wrap() 相比,gss_get_mic() 会针对消息和 MIC 生成不同的输出,这意味着发送者应用程序必须既能发送消息又能发送随附的 MIC。更重要的是,接收者必须能够区分消息和 MIC。以下方法可确保对消息和 MIC 进行正确的处理:

如果 gss_get_mic() 成功完成,则将返回 GSS_S_COMPLETE。如果指定的 QOP 无效,则将返回 GSS_S_BAD_QOP。有关更多信息,请参见 gss_get_mic(3GSS)

使用 gss_wrap() 包装消息

可以使用 gss_wrap() 函数来包装消息。与 gss_get_mic() 一样,gss_wrap() 也提供了 MIC。如果基础机制允许使用所请求的保密性,则 gss_wrap() 也会对指定的消息进行加密。消息的接收者通过使用 gss_unwrap() 来展开消息。

gss_get_mic() 不同,gss_wrap() 会将消息和 MIC 一起包装到传出消息中。用于传送该包的函数只需调用一次。在另一端,可以使用 gss_unwrap() 提取消息。MIC 对于应用程序不可见。

如果消息已成功包装,则 gss_wrap() 将返回 GSS_S_COMPLETE。如果所请求的 QOP 无效,则将返回 GSS_S_BAD_QOP。有关 gss_wrap() 的示例,请参见包装和发送消息

在 GSS-API 中处理包装大小问题

使用 gss_wrap() 来包装消息会增加要发送的数据量。由于所保护的消息包需要适应指定的传输协议,因此 GSS-API 提供了 gss_wrap_size_limit() 函数。gss_wrap_size_limit() 用于计算可以包装的消息的最大大小,使其不至于因变得过大而不适用于该协议。应用程序可以在调用 gss_wrap() 之前截断超出该大小的消息。在实际包装消息之前,请始终检查包装大小限制。

包装大小的增加取决于以下两个因素:

缺省的 QOP 会因不同的 GSS-API 实现而异。因此,即使指定了缺省的 QOP,所包装消息的大小也会有所不同。下图说明了这种可能性:

该图显示选定的 QOP 如何影响消息大小。

无论是否应用了保密性,gss_wrap() 仍然会增加消息的大小。gss_wrap() 可将 MIC 嵌入到所传送的消息中。但是,对消息进行加密会进一步增加消息的大小。此过程如下图所示。

该图显示使用保密性会增加消息大小。

如果 gss_wrap_size_limit() 成功完成,则将返回 GSS_S_COMPLETE。如果指定的 QOP 无效,则将返回 GSS_S_BAD_QOP包装和发送消息中提供了一个说明如何使用 gss_wrap_size_limit() 返回原始消息最大大小的示例。

成功完成此调用并不一定保证 gss_wrap() 可以保护长度为 max-input-size 个字节的消息。能否保护消息取决于调用 gss_wrap() 时系统资源是否可用。有关更多信息,请参见 gss_wrap_size_limit(3GSS) 手册页。

在 GSS-API 中检测顺序问题

由于上下文启动器会向接受器传送顺序的数据包,因此某些机制允许上下文接受器检查顺序是否正确。这些检查包括数据包是否按正确的顺序到达以及是否存在不必要的数据包重复。请参见下图。接受器可以在检验数据包和展开数据包的过程中检查这两种情况。有关更多信息,请参见展开消息

图 4–8 消息重放和消息失序

该图显示了消息重复和消息失序的情况。

启动器可以使用 gss_init_sec_context() 来检查顺序,方法是通过 GSS_C_REPLAY_FLAGGSS_C_SEQUENCE_FLAGreq_flags 参数应用逻辑 OR

在 GSS-API 中确认消息传送

接收者展开或检验所传送的消息之后,会向发送者返回确认信息,这意味着会发回该消息的 MIC。请考虑以下情况:消息没有由发送者进行包装,而只是通过 gss_get_mic() 使用 MIC 进行了标记。图 4–9 中说明的过程如下所示:

  1. 启动器使用 gss_get_mic() 对消息进行标记。

  2. 启动器将该消息和 MIC 发送到接受器。

  3. 接受器使用 gss_verify_mic() 检验该消息。

  4. 接受器将该 MIC 发回到启动器。

  5. 启动器使用 gss_verify_mic() 来针对原始消息检验所收到的 MIC。

图 4–9 确认 MIC 数据

该图说明如何确认消息完整性代码。

对于已包装的数据,gss_unwrap() 函数从不生成单独的 MIC,因此,接收者必须根据所收到的未包装的消息生成它。图 4–10 中说明的过程如下所示:

  1. 启动器使用 gss_wrap() 包装消息。

  2. 启动器发送已包装的消息。

  3. 接受器使用 gss_unwrap() 展开该消息。

  4. 接受器调用 gss_get_mic() 以生成未包装的消息的 MIC。

  5. 接受器将派生的 MIC 发送到启动器。

  6. 启动器使用 gss_verify_mic() 将所收到的 MIC 与原始消息进行比较。

对于已经为 GSS-API 数据分配的任何数据空间,应用程序应当解除对其进行分配。相关函数包括 gss_release_buffer(3GSS)gss_release_cred(3GSS)gss_release_name(3GSS)gss_release_oid_set(3GSS)

图 4–10 确认已包装的数据

该图说明如何确认已包装并且具有消息完整性代码的消息。

清除 GSS-API 会话

最后,发送和接收所有的消息,之后启动器应用程序和接受器应用程序就将完成操作。此时,这两个应用程序都应当调用 gss_delete_sec_context() 以销毁共享的上下文。gss_delete_sec_context() 可删除与该上下文相关的局部数据结构。

对于已经为 GSS-API 数据分配的任何数据空间,确保应用程序解除对其进行分配是不错的方法。这可以通过 gss_release_buffer()gss_release_cred()gss_release_name()gss_release_oid_set() 函数来完成。