Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.Net Core SAML Response Signature Validation

I'm working on a web application that needs to implement a SAML SSO using a third party idP (SP-initiated). I've reached the point where I am receiving the SAMLResponse from the idP which looks like this:

<?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>

It's a requirement that it's a manual implementation targeting netcoreapp2.0 so I have been trying to come up with the proper solution for validating the Signature value that's being provided. This doc, How to: Verify the Digital Signatures of XML Documents, was helpful in explaining some of the process but my implementation of SSO requires additional validations.

To pull the xml form from the SAML Response I have the following chunk:

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

Just for a quick reference without opening the above link this is what the code looks like (using my sample values/variables where applicable):

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);
}

UPDATE: Working solution for my manual implementation of SAML SSO in Asp.Net Core 2.0: First I have the below method named "VerifyXml" to verify the signature of the Xml document that is retrieved from the SAML Response form data. I then verify the X509 Certificate in my AccountController code as @Evk (thanks again for the help) points out that simply verifying the signature is not enough, in this case, to prevent arbitrary SAML Responses from being sent and accepted. On top of verifying those two values, in my case, I also need to validate the "InResponseTo" parameter matches the value generated by my web app (the SP) within a reasonable period of time. Generally, a login shouldn't take a long time so you could, for instance, create a task to forget AuthnRequest IDs that were generated and issued by your web app after a certain amount of time that's passed; say a minute or a time period that's applicable for you (Didn't include this code in the answer).

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 */ }
like image 883
Jared Avatar asked Dec 05 '17 20:12

Jared


2 Answers

Try to verify signature like this (your does not verify for me, but that might be caused by changes made while posting it 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);
}

If that doesn't work, also try the same but call

return signedXml.CheckSignature();

instead of

return signedXml.CheckSignature(cert);

Note that just verifying this signature is not enough to ensure that response has not been tampered with. You verify signature using key provided in response itself (X509Data), which means attacker could have intercepted response, extracted information and resigned it with his own key, so signature will be valid, but key it was signed with will be not. So after extracting certificate (or you can use signedXml.CheckSignatureReturningKey method to get public key related to signature) you need to verify that it's valid and that it's certificate you are expecting (for example by comparing its hash with hash of certificate you expect).

like image 165
Evk Avatar answered Oct 16 '22 11:10

Evk


The signature should be validated by a certificate provided by the Identity Provider. From your update, you are storing the metadata xml from the IdP and then retrieving the certificate from it.

Another alternative is to acquire the certificate from the IdP as a .cer file and install it in the Trusted Root Certificate Athorities store of the Local Machine. Once installed, this can be accessed in code and be used to verify the signature. This way you don't have to be concerned about spoofing of the certificate in the XML.

In this approach, the certificate in the response XML is used to acquire the Serial Number by which the saved certificate is found in the store.

Building on top of @Evk 's response

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
}

Also

    return signedXml.CheckSignature(cert);
    //^^^^ This will not work. 
    //CheckSignature(X509Certificate2 certificate, bool verifySignatureOnly) 
    //needs a boolean flag as well
like image 43
Yadu Avatar answered Oct 16 '22 10:10

Yadu