SignedXml.CheckSignature() is true, but SignedXml.CheckSignature(certificate) is false - saml-2.0

I'm generating a SAML2 token from ADFS, signed by certificate. Now I'm trying to verify that signature, using the same certificate.
X509Certificate2 cert = LoadCert();
XmlDocument token = LoadXmlToken(); //SAML2 token
XmlElement signature = GetSignatureElement(token);
SignedXml signedXml = new SignedXml(token);
signedXml.LoadXml(signature);
bool result1 = signedXml.CheckSignature(); //true
bool result2 = signedXml.CheckSignature(cert, false); //false
CheckSignature() verifies signature against the public key contained in the token.
CheckSignature(cert, [true/false]) verifies signature against the private key from the certificate.
How can it be that one works and the other doesn't?

The method signedXml.CheckSignature() evaluates the xml signature integrity against the certificate contained inside the own signature.
The method SignedXml.CheckSignature(X509Certificate2, Boolean) evaluates the xml signature integrity against the certificate passed as first parameter, and optionally if the second parameter is false it checks also the validity of the certificate in the first parameter.
Probably the second method returns false because you are specifying a wrong certificate: is not the certificate which performs the signature or its state is revoked or expired or it is issued by an untrusted certificate authority.

We had to enable IP address and/or URL's on our outbound firewall for the checksignature method when using the certificate check. In our case it tried to communicate with the root CA and the sub CA's website. With the firewall closed the method failed, but once we identified the URL's being accessed and opened up the firewall it started to work as expected.

The difference is in the second parameter (boolean). If you look at documentation of parameterless CheckSignature method you can find this:
This method also computes the digest of the references and the value of the signature.
The second method has this documentation. If the second parameter is set to
false then verify both the signature and certificate.
To verify certificate this method will probably build whole certificate chain and check revocation information of all certificates in this chain.

Related

Identity Server 4 Extension Grant without a Subject

I created a delegation extension grant the way they did in the docs. (https://identityserver4.readthedocs.io/en/latest/topics/extension_grants.html)
In the example, they get the user's identity from the claims and return the grant validation result like so:
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
My issue is that I don't always have a subject aka user identity when I need to utilize the delegation grant. In my scenario, I have an application listening to messages. When the app gets a message, it calls an API using client_credentials. That API then calls a sub API using the delegation grant type. Since the app is using client_credentials, there is no "sub" in the claims.
I tried checking if the "sub" claim exists and if not, set the subject of the GrantValidationResult to a "magical" guid which the IUserStore's FindByIdAsync would look for and either return null or a newed up empty TUser. In both cases, this causes Microsoft.AspNetCore.Identity to bomb futher down the pipeline.
How can I return a GrantValidationResult with the current claims, but not the subject when it doesn't exist?
I found this override for the GrantValidationResult.
// Summary:
// Initializes a new instance of the IdentityServer4.Validation.GrantValidationResult
// class with no subject. Warning: the resulting access token will only contain
// the client identity.
public GrantValidationResult(Dictionary<string, object> customResponse = null);
Since I don't have any custom responses, if "sub" is null, then I do this:
context.Result = new GrantValidationResult(new Dictionary<string, object>());
Doing it this way still populates the claims with the requested/validated scopes.

Identity Server 4 GetSchemeSupportsSignOutAsync returns incorrect response

I've setup an open id connect provider, Google in this case, using the AddOpenIdConnect extension method in dotnet core. From the discovery document:
https://accounts.google.com/.well-known/openid-configuration
it does not seem that google supports federated sign-out because there is no end_session endpoint. However, in Identity Server 4, the call:
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
returns true. So during Logout it tries to sign out of google using:
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
which throws an exception:
InvalidOperationException: Cannot redirect to the end session endpoint, the configuration may be missing or invalid.
Is this a bug in Identity Server 4 or is there a configuration property that needs to be set when setting up the Oidc provider so that this extension method will pickup that the provider does not support signout?
Doesn't appear to be a bug in Identity Server 4. The code behind this extension calls out to get the underlying authentication scheme handler.
public static async Task<bool> GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme)
{
var provider = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await provider.GetHandlerAsync(context, scheme);
return (handler != null && handler is IAuthenticationSignOutHandler);
}
In this case, your handler will be OpenIdConnectHandler which appears to implement IAuthenticationSignOutHandler so that's why regardless of what is in the discovery document (end session endpoint supported or not), if you use the AddOpenIdConnect(...), it will always register a handler which seemingly supports sign out, but as you have pointed out, does not actually enforce the actual idp validation for that kind of functionality support (link to handler source).
And lastly, worthwhile to mention, that Identity Server 4 check is rightful here as according to Microsoft docs, the IAuthenticationSignOutHandler is indeed basically a marker interface used to determine if a handler supports SignOut.
So I guess you just simply can't use the generic AddOpenIdConnect(...), instead perhaps you should use AddGoogle(...) which does not implement IAuthenticationSignOutHandler so will work as expected with Identity Server 4 (link to source).
As Vidmantas Blazevicius mentioned, using .AddOpenIdConnect will make the extension method default to true because of the interface. I have changed my code to explicity check for the support of an end_session_endpoint by doing:
var discoveryClient = new IdentityModel.Client.DiscoveryClient("https://accounts.google.com/.well-known/openid-configuration")
{
Policy = new IdentityModel.Client.DiscoveryPolicy
{
ValidateEndpoints = false, //this is needed for google, if set to true then will result in error response
ValidateIssuerName = false //this is needed for Microsoft, if set to true then will result in error response
}
};
var discoveryResult = await discoveryClient.GetAsync();
if (!discoveryResult.IsError)
{
if (!String.IsNullOrWhiteSpace(discoveryResult.EndSessionEndpoint))
supportsFederatedSignOut = true;
}
I then save an additional property on the model "SupportsFederatedSignOut" and then use this to determine whether external identity provider signout (SignOut) should be called.

How to get next certificate in chain

I want to get parent certificate (or all certificates in chain for that matter) from Windows Certificate Store (assuming I know the location of the end certificate). I need to get each one in order to build my own custom X509_STORE (using OpenSSL).
I think the proper course of action would be:
obtain first certificate using CertFindCertificateInStore (done)
get the certificate chain using CertGetCertificateChain (done)
extract the certificates from chain (?)
for each certificate in chain, convert it using d2i_X509 (done)
or
obtain first certificate using CertFindCertificateInStore (done)
get the parent certificate (if exists) (?)
convert it using d2i_X509, go to 2. (done)
Then create the store.
The question to answer is then - how to get the parent certificate or all certificates in chain using Windows Certificate Store? I'm probably missing some more or less occult function here.
as the CertFindCertificateInStore outputs a chain context, one can access it's members using the beautiful construction:
chainContext->rgpChain[0]->rgpElement[iCertIndex]->pCertContext->pbCertEncoded
where iCertIndex is between 0 (end-certificate) and chainSize -1 (self-signed root certificate).

xmlsec library - obtain subject of signing key

I am using the xmlsec library to verify the signature of an SAML assertion. My code is almost identical to the verify4.c example provided on the xmlsec web page.
I am linking against the xmlsec-openssl lib, so using openssl as the crypto engine.
I was expecting that xmlsec would consider the signature valid only if it was signed with one of the specific certificate(s) I had loaded into the key manager.
However, the signature seems to be considered valid if signed with ANY certificate that can be verified by openssl. This means that someone could forge an SAML response just by buying a certificate from a trusted root CA and using that to sign whatever response they want.
Not only that, but the xmlsec1 command line tool provided with the library seems to do the same thing:
xmlsec1 --verify --dtd-file saml.dtd --pubkey-cert-pem my_cert.cer sample_saml_assertion.xml
...
OK
SignedInfo References (ok/all): 1/1
Manifests References (ok/all): 0/0
Actually, in an ideal world, I would be happy for any valid signing key to be used as long as I could identify the subject of the key and therefore confirm it was signed by the entity I expected. That would simplify matters when the sender of the SAML responses changed their signing key. But I have not been able to find a simple way to extract the details of the cert that was used to verify the signature.
Failing that, can I make it accept only the certificate(s) I have specified when verifying the signature?
While writing the question I realized I had not tried the --print-debug option for xmlsec1. When I tried that I found that it does indeed print the subject and issuer of the cert that was used to verify the signature.
That led me to realize that the information must be present, so it is a question of how to access it. Tracing through the code, I was able to write this little snippet which does the trick:
/* If signature is valid, then the list dsigCtx->signKey contains
the signing key, data dsigCtx->signKey->dataList contains the certificate */
xmlSecPtrListPtr keyDataList = dsigCtx->signKey->dataList;
/* Iterate through the data list to find the X509 cert */
xmlSecSize n = xmlSecPtrListGetSize(keyDataList);
xmlSecSize i;
for (i=0; i<n; i++) {
xmlSecKeyDataPtr item = xmlSecPtrListGetItem(keyDataList, i);
if (xmlSecKeyDataIsValid(item) && xmlSecKeyDataCheckId(item, xmlSecOpenSSLKeyDataX509Id)) {
/* Extract openssl cert */
X509* cert = xmlSecOpenSSLKeyDataX509GetKeyCert(item);
char cn_buff[256];
if(cert != NULL) {
/* Get the CN */
X509_NAME * subject_name = X509_get_subject_name(cert);
int nid_cn = OBJ_txt2nid("CN");
X509_NAME_get_text_by_NID(subject_name, nid_cn, cn_buff, 255);
/* Here you would compare it to the expected certificate */
fprintf(stdout, "CN=%s\n", cn_buff);
} else {
fprintf(stdout, "Failed to obtain signing key cert\n");
}
}
}
This seems a very complex way to obtain something so fundamental, so I am sure there must be an easier way.

libldap openldap setting option fails for simple SSL connection

I am using openLdap (libldap) on Linux for building a simple SSL connection client.
I get the following error:
ldap_set_option(pLdap, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)
returns -1. Has anyone else seen this problem?
I am initializing ldap with
ldap_initialize (&pLdap,"ldaps://localhost:10636");
Please, do not redirect me to MSDN site for using wldap32.lib with
ldap_set_option(pLdapConnection, LDAP_OPT_SERVER_CERTIFICATE, &VerifyCert);
and a callback function. That works, but only on windows.
Furthermore, I can't change the server configuration.
pass the parameter in an enclosing value, not as the value itself.
int invalue = LDAP_OPT_X_TLS_NEVER;
err = ldap_set_option(0, LDAP_OPT_X_TLS_REQUIRE_CERT, &invalue);
Note that we can use 0 for the ldap handle because this option is for all connections.

Resources