Asp.Net 核心 SAML 响应签名验证

Asp.Net Core SAML Response Signature Validation

我正在开发一个需要使用第三方 idP(SP 启动)实施 SAML SSO 的 Web 应用程序。我已经到了从 idP 收到 SAMLResponse 的地步,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" InResponseTo="63622fa6-9a00-4d39-9c92-791c3a1efc3f" IssueInstant="2017-12-04T13:47:30Z" ID="mjmobamignjdlgkpmkiijfbknamlbkadhkjcamhp" Version="2.0">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.com</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="gkifgihgclegelojncjfgegcddfncgdaefcjgbod" IssueInstant="2017-12-04T13:47:30Z" Version="2.0">
    <saml:Issuer>https://idp.com</saml:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
      <ds:SignedInfo>
        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
        <ds:Reference URI="#gkifgihgclegelojncjfgegcddfncgdaefcjgbod">
          <ds:Transforms>
            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          </ds:Transforms>
          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
          <ds:DigestValue>nyU3iydIomlY9+D+YO7E6zNyq1A=</ds:DigestValue>
        </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue>1AVSFcmgaPMFZvPHYyZDz1oFWzgiMCHI6yMfe6yCSK1pw6bkbZd/yZys8DuySi3Q75bnu3FmbrJQ
L9eEfoXK7kJEut79f9xrBwScNYQ21AZdYh5Rdzm7jRsbugYuQpfUUWasR6U37+bStVPpsCYEo4+C
Y1arLC/9ujj7aGxF7H+EMk7X0L4059+2v711X7a/3biowx2CyNOgjNRcrri3cyX/0soryyCA6/zH
fO2wcQi4udMXcZwXtZpAsluah7DjGp9MSTS5NInKm3Is4VIS9fN3KmKKTJYYZI27N0lFAxgHGVXc
GPWsh4hAd1CqQvuM0P5YlBfgPBD6Mu6tmZ9VLg==</ds:SignatureValue>
      <ds:KeyInfo>
        <ds:X509IssuerSerial>
          <ds:X509IssuerName>CN=Symantec Class 3 Secure Server CA - G4,OU=Symantec Trust Network,O=Symantec Corporation,C=US</ds:X509IssuerName>
          <ds:X509SerialNumber>142421751065451577073995987482935596892</ds:X509SerialNumber>
        </ds:X509IssuerSerial>
        <ds:X509Data>
          <ds:X509Certificate>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
TyU0Bbc2VGLlcP8=</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </ds:Signature>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified">C229699</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData InResponseTo="63622fa6-9a00-4d39-9c92-791c3a1efc3f" NotOnOrAfter="2017-12-04T13:57:30Z" Recipient="http://my-app.net/saml"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2017-12-04T13:42:30Z" NotOnOrAfter="2017-12-04T13:57:30Z">
      <saml:AudienceRestriction>
        <saml:Audience>http://my-app.net</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2017-12-04T13:47:30Z" SessionIndex="gkifgihgclegelojncjfgegcddfncgdaefcjgbod">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="UserID" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue>D100000</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

要求它是针对 netcoreapp2.0 的手动实现,因此我一直在尝试想出正确的解决方案来验证所提供的签名值。此文档 How to: Verify the Digital Signatures of XML Documents 有助于解释一些过程,但我的 SSO 实施需要额外的验证。

要从 SAML 响应中提取 xml 表单,我有以下块:

var samlResponse = Request.Form["SAMLResponse"];
var toBytes = Convert.FromBase64String(samlResponse);
string decodedString = 
Encoding.UTF8.GetString(toBytes);

只是为了快速参考而不打开上面的代码link这是代码的样子(在适用的地方使用我的示例values/variables):

CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = "XML_DSIG_RSA_KEY";

// Create a new RSA signing key and save it in the container. 
RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider(cspParams);

// Create a new XML document.
XmlDocument xmlDoc = new XmlDocument();

// Load an XML file into the XmlDocument object.
xmlDoc.PreserveWhitespace = true;
xmlDoc.LoadXml(decodedString);

// Verify the signature of the signed XML.
Console.WriteLine("Verifying signature...");
bool result = VerifyXml(xmlDoc, rsaKey);

// Display the results of the signature verification to 
// the console.
if (result)
{
    Console.WriteLine("The XML signature is valid.");
}
else
{
    Console.WriteLine("The XML signature is not valid.");
}

public static Boolean VerifyXml(XmlDocument Doc, RSA Key)
{
    // Check arguments.
    if (Doc == null)
        throw new ArgumentException("Doc");
    if (Key == null)
        throw new ArgumentException("Key");

    // Create a new SignedXml object and pass it
    // the XML document class.
    SignedXml signedXml = new SignedXml(Doc);

    // Find the "Signature" node and create a new
    // XmlNodeList object.
    XmlNodeList nodeList = Doc.GetElementsByTagName("Signature");

    // Throw an exception if no signature was found.
    if (nodeList.Count <= 0)
    {
        throw new CryptographicException("Verification failed: No Signature was found in the document.");
    }

    // This example only supports one signature for
    // the entire XML document.  Throw an exception 
    // if more than one signature was found.
    if (nodeList.Count >= 2)
    {
        throw new CryptographicException("Verification failed: More that one signature was found for the document.");
    }

    // Load the first <signature> node.  
    signedXml.LoadXml((XmlElement)nodeList[0]);

    // Check the signature and return the result.
    return signedXml.CheckSignature(Key);
}

更新:我在 Asp.Net Core 2.0 中手动实施 SAML SSO 的工作解决方案:首先,我使用以下名为“VerifyXml”的方法来验证签名从 SAML 响应表单数据中检索到的 Xml 文档。然后,我在我的 AccountController 代码中验证 X509 证书,因为 @Evk(再次感谢您的帮助)指出,在这种情况下,仅验证签名是不够的,以防止发送和接受任意 SAML 响应。除了验证这两个值之外,在我的例子中,我还需要验证“InResponseTo”参数在合理的时间段内与我的网络应用程序 (SP) 生成的值相匹配。通常,登录不应该花费很长时间,因此您可以创建一个任务来忘记在经过一定时间后由您的网络应用程序生成和发出的 AuthnRequest ID;说出适用于您的一分钟或时间段(未在答案中包含此代码)。

public static bool VerifyXml(XmlDocument Doc)
{
    // Check document isn't null.
    if (Doc == null) 
        throw new ArgumentException("Doc");    
    SignedXml signedXml = new SignedXml(Doc);
    var nsManager = new XmlNamespaceManager(Doc.NameTable);
    nsManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
    var node = Doc.SelectSingleNode("//ds:Signature", nsManager);
    signedXml.LoadXml((XmlElement)node);
    return signedXml.CheckSignature();
 }
/* ******* CONTROLLER CODE ******* */
SignedXml signedXml = new SignedXml(xdoc);
var nsManager = new XmlNamespaceManager(xdoc.NameTable);
nsManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
var certElement = xdoc.SelectSingleNode("//ds:X509Certificate", nsManager);
/* Convert the received X509 Certificate into a new X509Certificate2 object. */
var certReceived = new X509Certificate2(Convert.FromBase64String(certElement.InnerText));
/* Load the pre-shared X509 Certificate from the idP metadata file. I have it stored in a secure database (You DO NOT want this stored in an easily accessible place, especially for production, in the project as it contains sensitive information). */
var loadSafeCert = _context.StoredMetadata.Where(metadata => idPMetadata.Certificate == "Certificate").FirstOrDefault();
/* Create a new X509Certificate2 using the value of the pre-defined certificate. */    
var safeCertificate = new X509Certificate2(Convert.FromBase64String(loadSafeCert.ConfigurationValue));    
/* Compare the received X509 Certificate value vs the pre-defined X509 Certificate value to ensure the validity. */
if (certReceived.GetPublicKeyString() == safeCertificate.GetPublicKeyString())
{ /* Store/get attributes, authenticate user, etc here */ }

尝试像这样验证签名(您没有为我验证,但这可能是由于在此处发布时所做的更改造成的):

public static bool VerifyXml(XmlDocument Doc) {
    if (Doc == null)
        throw new ArgumentException("Doc");
    SignedXml signedXml = new SignedXml(Doc);
    var nsManager = new XmlNamespaceManager(Doc.NameTable);
    nsManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
    var node = Doc.SelectSingleNode("//ds:Signature", nsManager);
    // find signature node
    var certElement = Doc.SelectSingleNode("//ds:X509Certificate", nsManager);
    // find certificate node
    var cert = new X509Certificate2(Convert.FromBase64String(certElement.InnerText));            
    signedXml.LoadXml((XmlElement)node);
    return signedXml.CheckSignature(cert);
}

如果这不起作用,也可以尝试相同的方法,但请致电

return signedXml.CheckSignature();

而不是

return signedXml.CheckSignature(cert);

请注意,仅验证此签名不足以确保响应未被篡改。您使用响应本身提供的密钥 (X509Data) 验证签名,这意味着攻击者可能拦截了响应、提取信息并使用他自己的密钥重新签名,因此签名将有效,但用于签名的密钥将无效.因此,在提取证书后(或者您可以使用 signedXml.CheckSignatureReturningKey 方法获取与签名相关的 public 密钥),您需要验证它是否有效并且它是您期望的证书(例如通过将其哈希与哈希进行比较您期望的证书)。

签名应由身份提供者提供的证书进行验证。从您的更新中,您正在存储来自 IdP 的元数据 xml,然后从中检索证书。

另一种方法是从 IdP 获取证书作为 .cer 文件并将其安装在 Local MachineTrusted Root Certificate Athorities 存储中。安装后,可以通过代码访问它并用于验证签名。这样您就不必担心 XML.

中证书的欺骗

在这种方法中,响应中的证书 XML 用于获取在商店中找到已保存证书的序列号。

建立在@Evk 的回应之上

public static bool VerifyXml(XmlDocument Doc) 
{
    if (Doc == null)
        throw new ArgumentException("Doc");
    SignedXml signedXml = new SignedXml(Doc);
    var nsManager = new XmlNamespaceManager(Doc.NameTable);
    nsManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
    var node = Doc.SelectSingleNode("//ds:Signature", nsManager);
    // find signature node
    var certElement = Doc.SelectSingleNode("//ds:X509Certificate", nsManager);
    // find certificate node
    var cert = new X509Certificate2(Convert.FromBase64String(certElement.InnerText));            
    signedXml.LoadXml((XmlElement)node);

    //Find installed certificate from store
    X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
    store.Open(OpenFlags.ReadOnly);
    X509Certificate2 storeCert = store.Certificates.Find(X509FindType.FindBySerialNumber, cert.SerialNumber, true)[0];

    return signedXml.CheckSignature(storeCert, true);
    //^^^ If certificate is installed in the Root location then 
    //this method returns true after validating it as well
    //In addition to validating the signature
}

还有

    return signedXml.CheckSignature(cert);
    //^^^^ This will not work. 
    //CheckSignature(X509Certificate2 certificate, bool verifySignatureOnly) 
    //needs a boolean flag as well