Thursday, August 25, 2016

SignedXml::CheckSignature and dreadful “The profile for the user is a temporary profile” on .NET 4.6.2

Sometimes, out of nowhere, your well-tested and working-for-years code just stops working just because something changes somewhere you can really have control over.

This post documents one of such cases. The case is about checking digital signatures of signed XML documets. For years we’ve been creating and validating documents with self-contained signatures. I’ve blogged on that in a three part tutorial. In our scenario, IIS hosted ASP.NET applications were receiving signed XML documents and processing them.

This worked as a charm until .NET 4.6.2. In .NET 4.6.2 the SignedXml::CheckSignature( X509Certificate2 ) method we’ve been using for years doesn’t work anymore in our scenario, it throws “The profie for the user is a temporary profile” exception.

What is a temporary profile? You see, when an app pool is configued in IIS to use a custom account (a domain account in our case) and the Load User Profile is turned on, the ASP.NET worker process is supposed to load the profile of the app pool account. The existence of the profile changes some details on how temporary paths are resolved and how keys are imported to cert stores. IIS has an unfortunate way of handling this requirement – instead of creating a regular profile, it creates a temporary profile. People often ask what these profiles are and how to turn them off.

Unfortunately, there is no easy way to change this behavior of creating temporary profiles rather than regular profiles (or we don’t know how to do it). A workaround exists – you can just log into the server as the app pool account with an interactive session (a direct login or a remote desktop session). A local profile is then created at the server and further loading the profile by ASP.NET use the newly created, valid profile rather than a temporary one (e.g. the workaround is mentioned here at Paul Stovell blog). But suppose you have a farm where servers are cloned on demand (we use the Web Farm Framework). Suppose also you have multiple apps. We have like ~150 servers and ~50 apps on each, this means logging 150*50 times to different servers only to have profiles created correctly.

Unfortunately also, to be able to use cryptographic API, you often just have to turn on loading user profiles, otherwise the crypto system just doesn’t work sometimes (private keys cannot be accessed for certs that are loaded from local files). In our case, turning off the profile raises an instant exception “Object is in invalid state”.

What all it means is that .NET 4.6.2 changes the way SignedXml::CheckSignature( X509Certificate2 ) works and changes it in a fundamental way. Before 4.6.2 the method works always, regardless of whether the profile is a temporary profile or not, as long as the profile is loaded (Load User Profile = true). In .NET 4.6.2 the method doesn’t work if the profile is a temporary profile.

One of our first attempts was to follow the previously mentioned workaround and somehow automate the profile creation so that profiles are there when IIS requests them. This however doesn’t solve the issue on already existing servers.

But then another approach was tested. Because we check signatures on self-contained signed documents, X509Certificate2 instances we pass to the method are retrieved from the SignedXml:

// Create a new SignedXml object and pass it  // the XML document class.
SignedXml signedXml = new SignedXml( xd );
 
// Load the first <signature> node.   signedXml.LoadXml( (XmlElement)messageSignatureNodeList[0] );

// load certificate 
foreach ( KeyInfoClause clause in signedXml.KeyInfo ) 

    if ( clause is KeyInfoX509Data ) 
    { 
        if ( ( (KeyInfoX509Data)clause ).Certificates.Count > 0 ) 
        { 
            certificate = (X509Certificate2)( (KeyInfoX509Data)clause ).Certificates[0]; 
        } 
    } 
}

if ( certificate != null ) 

    // Check the signature and return the result. Throws the “user profile is a temporary profile” 
    return signedXml.CheckSignature( certificate, true ); 

else 
    return false;

The exception possibly is related to the possible leakage of private keys when temporary profiles are involved. Maybe one of .NET BCL developers was just oversensitive here, implementing a bunch of new features.

There are no private keys, however, in our certificates! Since certificates are here to verify signatures, private keys are not included, we only have public keys. How about using the overload of the CheckSignature that only expects an AsymmetricAlgorithm?

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

// Load the first  node.  
signedXml.LoadXml( (XmlElement)messageSignatureNodeList[0] );

AsymmetricAlgorithm rsa = null;

// load certificate
foreach ( KeyInfoClause clause in signedXml.KeyInfo )
{
	if ( clause is KeyInfoX509Data )
	{
        	if ( ( (KeyInfoX509Data)clause ).Certificates.Count > 0 )
	        {
        	      certificate = ((KeyInfoX509Data)clause).Certificates[0] as X509Certificate2;
	        }
        }
}

if ( certificate == null )
{
	Message = "No KeyInfoX509Data clause in the signature";
        return false;
}

if ( certificate.PublicKey == null || certificate.PublicKey.Key == null )
{
	Message = "The KeyInfoX509 clause doesn't contain any valid public key";
        return false;
}

rsa = certificate.PublicKey.Key;

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

Yes, as you can expect, this works correctly, again regardless on whether the profile is a regular profile or a temporary one.

No comments: