解密安全令牌

Download sample

CardSpace 为用户提供了管理其数字标识的功能,使用 Internet Explorer 7.0(或支持信息卡的其他浏览器),网站可以向用户请求数字标识。 此标识以加密的安全令牌的形式进行传输。 可以使用 Token 示例类来对令牌进行解密以使用令牌中包含的声明。 本主题将分析令牌及其工作方式。

Web 服务器安装

若要完成练习,必须进行网站配置。 使用示例文件夹中提供的以下安装批处理文件可以完成配置:

Setup.bat

有关安装网站的更多信息以及一些疑难解答提示,请参见安装 CardSpace 示例证书

获取令牌

示例如何在 Internet Explorer 7.0 中使用 Windows CardSpace 中的代码演示了显示标识选择器所需的 HTML 代码。

使用的文件:

Sample.htm

标识选择器通过使用 <object> 元素或二进制行为对象进行显示。 本示例使用 <object> 元素和一些 JavaScript 向用户请求令牌,并在提交到网站之前将其显示在 <textarea> 中。

Sample.htm 文件:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
  <title>Sample 2</title>
  <object type="application/x-informationcard" name="_xmlToken">
    <param name="tokenType" value="urn:oasis:names:tc:SAML:1.0:assertion" />
    <param name="issuer" value="https://schemas.xmlsoap.org/ws/2005/05/identity/issuer/self" />
    <param name="requiredClaims" 
        value="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
        https://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname
        https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
        https://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier" />
  </object>
  <script language="javascript">
    function GetIdentity(){
      var xmltkn=document.getElementById("_xmltoken");
      var thetextarea = document.getElementById("xmltoken");
      thetextarea.value = xmltkn.value ;
    }
  </script>
</head>
<body>
  <form id="form1" method="post" action="login.aspx">
    <button name="go" id="go" onclick="javascript:GetIdentity();">Click here to get the token.</button>
    <button type="submit">Click here to send the card to the server</button>
    <textarea cols=100 rows=20 id="xmltoken" name="xmlToken" ></textarea>
  </form>
</body>
</html>

当用户单击**“提交”**按钮后,令牌将被发布到服务器上的表单中,必须在其中对令牌进行解密和验证,然后才能使用声明。

用 C# 编写的用于接受发布令牌的 login.aspx 页。

<%@ Page Language="C#"  Debug="true" ValidateRequest="false" %>
<%@ Import Namespace="Microsoft.IdentityModel.Samples" %>
<%@ Import Namespace="Microsoft.IdentityModel.TokenProcessor" %>

<script runat="server">
    protected void ShowError(string text) {
        fields.Visible = false;
        errors.Visible = true;
        errtext.Text = text;
    }
    protected void Page_Load(object sender, EventArgs e) {
        string xmlToken;
        xmlToken = Request.Params["xmlToken"];
        if (xmlToken == null || xmlToken.Equals(""))
            ShowError("Token presented was null");
        else
        {
                Token token = new Token (xmlToken);
                givenname.Text = token.Claims[SelfIssued.GivenName];
                surname.Text = token.Claims[SelfIssued.Surname];
                email.Text = token.Claims[SelfIssued.EmailAddress];
                uid.Text = token.UniqueID;
        }
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Login Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div runat="server" id="fields">
        Given Name:<asp:Label ID="givenname" runat="server" Text=""></asp:Label><br/>
        Surname:<asp:Label ID="surname" runat="server" Text=""></asp:Label><br/>
        Email Address:<asp:Label ID="email" runat="server" Text=""></asp:Label><br/>
        Unique ID:<asp:Label ID="uid" runat="server" Text=""></asp:Label><br/>
    </div>
    <div runat="server" id="errors" visible="false">
        Error:<asp:Label ID="errtext" runat="server" Text=""></asp:Label><br/>
    </div>
        
    </form>
</body>
</html>

用 VB.NET 编写的用于接受发布令牌的 login.aspx 页:

<%@ Page Language="VB"  Debug="true" ValidateRequest="false" %>
<%@ Import Namespace="Microsoft.IdentityModel.Samples" %>
<%@ Import Namespace="Microsoft.IdentityModel.TokenProcessor" %>

<script runat="server">
Protected  Sub ShowError(ByVal text As String) 
   fields.Visible = False 
   errors.Visible = True 
   errtext.Text = text 
End Sub

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
   Dim xmlToken As String
   xmlToken = Request.Params("xmlToken")
   If xmlToken = Nothing Or xmlToken.Equals("") Then
      ShowError("Token presented was null")
   Else
      Dim token As New Token(xmlToken)
      givenname.Text = token.Claims(ClaimTypes.GivenName)
      surname.Text = token.Claims(ClaimTypes.Surname)
      email.Text = token.Claims(ClaimTypes.Email)
      uid.Text = token.UniqueID
   End If
End Sub

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Login Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div runat="server" id="fields">
        Given Name:<asp:Label ID="givenname" runat="server" Text=""></asp:Label><br/>
        Surname:<asp:Label ID="surname" runat="server" Text=""></asp:Label><br/>
        Email Address:<asp:Label ID="email" runat="server" Text=""></asp:Label><br/>
        Unique ID:<asp:Label ID="uid" runat="server" Text=""></asp:Label><br/>
    </div>
    <div runat="server" id="errors" visible="false">
        Error:<asp:Label ID="errtext" runat="server" Text=""></asp:Label><br/>
    </div>
        
    </form>
</body>
</html>

Token 类用于处理加密 XML 令牌的解密、验证和声明提取。

处理令牌:检查 XML 加密格式

从浏览器发布的令牌使用 W3C XML 加密标准进行加密。 架构描述文档为 XML 加密语法和处理(可能为英文网页)。 架构为核心架构(可能为英文网页)和 XML 加密(可能为英文网页)。 下面是一个典型的加密令牌示例。

<enc:EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" 
    xmlns:enc="http://www.w3.org/2001/04/xmlenc#">
  <enc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
  <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
    <e:EncryptedKey xmlns:e="http://www.w3.org/2001/04/xmlenc#">
      <e:EncryptionMethod 
       Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      </e:EncryptionMethod>
      <KeyInfo>
        <o:SecurityTokenReference 
           xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-
                    1.0.xsd">
          <o:KeyIdentifier 
            ValueType="http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-
                      1.1#ThumbprintSHA1"
            EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-
                      message-security-1.0#Base64Binary">
          1H3mV/pJAlVZAst/Dt0rqbBd67g=
          </o:KeyIdentifier>
        </o:SecurityTokenReference>
      </KeyInfo>
      <e:CipherData>
        <e:CipherValue>
        YJHp...==</e:CipherValue>
      </e:CipherData>
    </e:EncryptedKey>
  </KeyInfo>
  <enc:CipherData>
    <enc:CipherValue>
    ctct...9A==</enc:CipherValue>
  </enc:CipherData>
</enc:EncryptedData>

请查看发布的 XML 的构成部分,根元素为 <enc:EncryptedData>。 它包含访问令牌所需的三项内容。

<enc:EncryptedData>
  <enc:EncryptionMethod />
  <KeyInfo /> 
  <enc:CipherData />
</enc:EncryptedData>

首先,<enc:EncryptionMethod> 包含用于加密令牌的方法。

  <enc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />

在本示例中为 AES-256-cbc,这是一种对称密钥加密算法,在该算法中双方都使用同一密钥进行加密和解密。 对称密钥是由发起者随机生成的,该密钥也称为瞬态密钥。 瞬态密钥加密存储在 <e:EncryptedKey> 元素中,该元素由 <KeyInfo> 元素包装。

<KeyInfo >
  <e:EncryptedKey />
    <e:EncryptionMethod / >
    <KeyInfo />
    <e:CipherData />
  </e:EncryptedKey>
</KeyInfo>

建议使用对称算法对令牌进行加密并发送使用该令牌加密的密钥,因为非对称加密比对称加密要慢一些,并且使用非对称密钥对大于密钥大小的数据进行加密是不可取的。 使用非对称算法仅加密密钥可以显著减少服务器端所需的处理量。

瞬态密钥使用依赖方的公钥(来自其证书)进行加密。 瞬态密钥的加密方法可在 <e:EncryptionMethod> 元素中找到。

<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
   <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
</e:EncryptionMethod>

在本示例中,使用 RSA-OAEP-MGF1P 算法进行加密。 此操作的密钥可在下面的 <KeyInfo> 元素中找到。

<KeyInfo>
  <o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-
                                     wssecurity-secext-1.0.xsd">
    <o:KeyIdentifier 
      ValueType="http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-
                 1.1#ThumbprintSHA1"
      EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-
                    security-1.0#Base64Binary">

        1H3mV/pJAlVZAst/Dt0rqbBd67g=

   </o:KeyIdentifier>
  </o:SecurityTokenReference>
</KeyInfo>

发送方通过将其指纹(base64 编码)放入 <o:KeyIdentifier> 元素,可以通知依赖方是使用哪个证书的密钥对对称密钥进行加密的。

依赖方然后使用其私钥解密 <e:CipherData>/<e:CypherValue> 元素中的数据。

<e:CipherData>
    <e:CipherValue>
    YJHp...==</e:CipherValue>
</e:CipherData>

在检索到对称密钥之后,将使用该对称密钥对令牌本身进行解密。

<enc:CipherData>
    <enc:CipherValue>
    ctct...9A==</enc:CipherValue>
</enc:CipherData>

令牌处理器代码的突出特点

若要了解解密流程,需要注意解密流程的以下突出特点。 若要查看完整流程,请查阅 Token 类的源代码。

Token 类构造函数使用 decryptToken 方法对传入构造函数的加密 XML 数据执行解密。

private static byte[] decryptToken(string xmlToken)

为循环访问 XML 数据,这里使用了 XmlReader

XmlReader reader = new XmlTextReader(new StringReader(xmlToken));

在搜索 XML 元素时,仅允许非常小的灵活性,如果使用了无效令牌,则会很快失败(使用 ArgumentException)。 首先,必须找到 <EncryptionMethod> 元素,在其中访问 Algorithm 元素以检索令牌的加密方法。

if (!reader.ReadToDescendant(XmlEncryptionStrings.EncryptionMethod,  
                             XmlEncryptionStrings.Namespace))
  throw new ArgumentException("Cannot find token EncryptedMethod.");
encryptionAlgorithm = reader.GetAttribute(XmlEncryptionStrings.Algorithm).GetHashCode();

然后,找到瞬态密钥的 <EncryptionMethod> 元素,再次获取 Algorithm,它存储为自身 HashCode 以加快查找。

if (!reader.ReadToFollowing(XmlEncryptionStrings.EncryptionMethod, 
                            XmlEncryptionStrings.Namespace))
   throw new ArgumentException("Cannot find key EncryptedMethod.");
m_keyEncryptionAlgorithm = reader.GetAttribute(XmlEncryptionStrings.Algorithm).GetHashCode();

接下来找到元素 <KeyIdentifier>,它包含证书的指纹(解密对称密钥需要此指纹),并从 base64 字符串中提取该指纹。

if (!reader.ReadToFollowing(WSSecurityStrings.KeyIdentifier, WSSecurityStrings.Namespace))
  throw new ArgumentException("Cannot find Key Identifier.");
reader.Read();
thumbprint = Convert.FromBase64String(reader.ReadContentAsString());

<CypherValue> 元素包含加密形式的对称密钥(base64 编码)。

if (!reader.ReadToFollowing(XmlEncryptionStrings.CipherValue, XmlEncryptionStrings.Namespace))
    throw new ArgumentException("Cannot find symmetric key.");
reader.Read();
symmetricKeyData = Convert.FromBase64String(reader.ReadContentAsString());

包含实际加密令牌的 <CypherValue>

if (!reader.ReadToFollowing(XmlEncryptionStrings.CipherValue, XmlEncryptionStrings.Namespace))
  throw new ArgumentException("Cannot find encrypted security token.");
reader.Read();
securityTokenData = Convert.FromBase64String(reader.ReadContentAsString());

确保关闭读取器以释放资源。

reader.Close();

安全令牌的加密可采用两种对称算法(AES 和 Triple DES)中的一种。 可使用加密算法 URI 进行查找。

SymmetricAlgorithm alg = null;
X509Certificate2 certificate = FindCertificate(thumbprint );
              
foreach( int i in Aes )
if (encryptionAlgorithm == i)
{
  alg= new RijndaelManaged();
  break;
}
if ( null == alg )
foreach (int i in TripleDes)
if (encryptionAlgorithm == i)
{
  alg = new TripleDESCryptoServiceProvider();
  break;
}
  
if (null == alg)
  throw new ArgumentException("Could not determine Symmetric Algorithm");

若要获取对称密钥,请使用私钥对其进行解密。

alg.Key=(certificate.PrivateKey as RSACryptoServiceProvider).Decrypt(symmetricKeyData,true);

使用通过对称算法发现的算法解密令牌。

  int ivSize = alg.BlockSize / 8;
  byte[] iv = new byte[ivSize];
  Buffer.BlockCopy(securityTokenData, 0, iv, 0, iv.Length);
  alg.Padding = PaddingMode.ISO10126;
  alg.Mode = CipherMode.CBC;
  ICryptoTransform decrTransform = alg.CreateDecryptor(alg.Key, iv);
  byte[] plainText = decrTransform.TransformFinalBlock(securityTokenData, iv.Length,   
                                                       securityTokenData.Length iv.Length);
  decrTransform.Dispose();
  return plainText;
}

反序列化令牌并对令牌进行身份验证

若要使用嵌入的令牌,在解密之后,.NET Framework 3.0 将使用 SamlSecurityTokenAuthenticator 对令牌进行反序列化(通过 WSSecurityTokenSerializer)和身份验证。 目前 Token 类支持 SAML 令牌。 如果需要其他令牌类型,开发人员必须提供身份验证器以支持该类型。 在身份验证器对令牌进行验证之后,Token 类将声明提取到可用表单中。

public Token(String xmlToken)
{
  byte[] decryptedData = decryptToken(xmlToken);
  
  XmlReader reader = new XmlTextReader(
            new StreamReader(new MemoryStream(decryptedData), Encoding.UTF8));

  m_token = (SamlSecurityToken)WSSecurityTokenSerializer.DefaultInstance.ReadToken(
            reader, null);

  SamlSecurityTokenAuthenticator authenticator = 
            new SamlSecurityTokenAuthenticator(new List<SecurityTokenAuthenticator>(
                 new SecurityTokenAuthenticator[]{
                 new RsaSecurityTokenAuthenticator(),
                 new X509SecurityTokenAuthenticator() }), MaximumTokenSkew);
  
  if (authenticator.CanValidateToken(m_token))
  {
    ReadOnlyCollection<IAuthorizationPolicy> policies = authenticator.ValidateToken(m_token);
    m_authorizationContext = AuthorizationContext.CreateDefaultAuthorizationContext(policies);
    FindIdentityClaims();
  }
  else
  {
    throw new Exception("Unable to validate the token.");
  }
}

Token 类的用法

下表描述了 Token 类公开的用于从安全令牌中提取声明的属性。

名称 说明

IdentityClaims

令牌中标识声明的 ClaimSet

AuthorizationContext

从令牌生成的 AuthorizationContext

UniqueID

获取令牌的 UniqueID (IdentityClaim)。

默认情况下,它使用 PPID 和颁发者的公钥,并对它们共同执行哈希算法来生成一个 UniqueID。

若要使用其他字段,请添加下面的代码行。

<add name="IdentityClaimType"  
     value="https://schemas.xmlsoap.org/ws/2005/05/identity/
     claims/privatepersonalidentifier" />

将该值替换为唯一声明的 URI。

Claims

令牌中声明的只读字符串集合。 为索引的声明访问器提供支持。

securityToken.Claims[ClaimsTypes.PPID]

IssuerIdentityClaim

返回颁发者的标识声明(通常为颁发者的公钥)。

Footer image

向 Microsoft 发送对本主题的评论。

版权所有 (C) 2007 Microsoft Corporation。保留所有权利。