views:

2307

answers:

4

Hello all -

Apparently I was asking the wrong question in my earlier post. I have a web service secured with a X.509 certificate, running as a secure web site (https://...). I want to use the client's machine certificate (also X.509) issued by the company's root CA to verify to the server that the client machine is authorized to use the service. In order to do this, I need to inspect the certificate and look for some identifying feature and match that to a value stored in a database (maybe the Thumbprint?).

Here is the code I use to get the certificate from the local certificate store (lifted straight from http://msdn.microsoft.com/en-us/magazine/cc163454.aspx):

 public static class SecurityCertificate
{
    private static X509Certificate2 _certificate = null;

    public static X509Certificate2 Certificate
    {
        get { return _certificate; }
    }

    public static bool LoadCertificate()
    {
        // get thumbprint from app.config
        string thumbPrint = Properties.Settings.Default.Thumbprint;
        if ( string.IsNullOrEmpty( thumbPrint ) )
        {
            // if no thumbprint on file, user must select certificate to use
            _certificate = PickCertificate( StoreLocation.LocalMachine, StoreName.My );
            if ( null != _certificate )
            {
                // show certificate details dialog
                X509Certificate2UI.DisplayCertificate( _certificate );
                Properties.Settings.Default.Thumbprint = _certificate.Thumbprint;
                Properties.Settings.Default.Save();
            }
        }
        else
        {
            _certificate = FindCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, thumbPrint );
        }

        if ( null == _certificate )
        {
            MessageBox.Show( "You must have a valid machine certificate to use STS." );
            return false;
        }

        return true;
    }

    private static X509Certificate2 PickCertificate( StoreLocation location, StoreName name )
    {
        X509Store store = new X509Store( name, location );
        try
        {
            // create and open store for read-only access
            store.Open( OpenFlags.ReadOnly );

            X509Certificate2Collection coll = store.Certificates.Find( X509FindType.FindByIssuerName, STSClientConstants.NBCCA, true );
            if ( 0 == coll.Count )
            {
                MessageBox.Show( "No valid machine certificate found - please contact tech support." );
                return null;
            }

            // pick a certificate from the store
            coll = null;
            while ( null == coll || 0 == coll.Count )
            {
                coll = X509Certificate2UI.SelectFromCollection(
                        store.Certificates, "Local Machine Certificates",
                        "Select one", X509SelectionFlag.SingleSelection );
            }

            // return first certificate found
            return coll[ 0 ];
        }
        // always close the store
        finally { store.Close(); }
    }

    private static X509Certificate2 FindCertificate( StoreLocation location, StoreName name, X509FindType findType, string findValue )
    {
        X509Store store = new X509Store( name, location );
        try
        {
            // create and open store for read-only access
            store.Open( OpenFlags.ReadOnly );

            // search store
            X509Certificate2Collection col = store.Certificates.Find( findType, findValue, true );

            // return first certificate found
            return col[ 0 ];
        }
        // always close the store
        finally { store.Close(); }
    }

Then, I attach the certificate to the outbound stream thusly:

public static class ServiceDataAccess
{    
    private static STSWebService _stsWebService = new STSWebService();

    public static DataSet GetData(Dictionary<string,string> DictParam, string action)
    {
        // add the machine certificate here, the first web service call made by the program (only called once)
        _stsWebService.ClientCertificates.Add( SecurityCertificate.Certificate );
        // rest of web service call here...
    }
}

My question is this -- how do I "get" the certificate in the web service code? Most sample code snippets I have come across that cover how to do custom validation have a GetCertificate() call in there, apparently assuming that part is so easy everyone should know how to do it?

My main class inherits from WebService, so I can use Context.Request.ClientCertificate to get a certificate, but that's an HttpClientCertificate, not an X509Certificate2. HttpContext gives me the same result. Other approaches all use web configuration code to call pre-defined verification code, with no clue as to how to call a custom C# method to do the verification.

I'm sure this is another "stupid question" that somebody will come back and say "look, just do this and it works," but that's OK. I have spent numerous hours trying to get this to work, and my pride is almost nonexistent at this point. Can somebody show me the error of my ways?

Thanks, Dave

A: 

You hook yourself into the ServicePointManager.ServerCertificateValidationCallback see http://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.servercertificatevalidationcallback.aspx

Remus Rusanu
I saw several references to this, as well. This validates the server certificate, not the client certificate.
DaveN59
+3  A: 

I recall doing something similar, its been awhile but, have you tried this in your web service:

X509Certificate2 cert = new X509Certificate2(Context.Request.ClientCertificate.Certificate);
DSO
That works! Now I can finally get to the meat of my problem -- figuring out what value I can use to match with a stored value in the database...Thanks!!
DaveN59
Thumbprint is fine to use if you are treating your database as the trusted authority, I've worked on apps that took this approach. OTOH, if you need a distributed trust model (using CAs), then you need to do additional checks (e.g. verifying cert chain).
DSO
I think I may have acted too soon. Trying to access the Thumbprint throws an exception 'System.Security.Cryptography.CryptographicException' -- so, even though the http Certificate is now of type X509Certificate2, is it really the same certificate sent from the client, or did I just create a new beast that just kinda looks like it?
DaveN59
OK, digging deeper I find the exception message is "m_safeCertContext is an invalid handle." Checking around for that message, it is probably caused by trying to run in the "thin" VS web server (debug mode), rather than IIS. Back to the drawing board!Hey, if it was easy they wouldn't pay us to do it, right?
DaveN59
I don't think you can debug SSL using Visual Studio web server (unless things changed since I last used in in VS 2005). You really should set yourself up to debug using IIS.
DSO
A: 

Can anyone tell me please why i dont get the .PrivateKey (=null) when i instantiate an X509Certificate2 class like this:

X509Certificate2 cert = new X509Certificate2(Context.Request.ClientCertificate.Certificate, "test");

The HttpClientCertificate has an private key, but X509Certificate2 (cert variable) not.

..furthermore i dont even get an CryptographicException when i specify an invalid password

krile
A: 

On the subject of how to tie the certificate back to a user, so assuming the identity of the user associated with the key is good (as the certificate has been verified back to a trusted root and has not been revoked) then you need to tie the identity claimed by the cert to a user. You could just use the LDAP string form of the subject DN and look that up (cn=Username,ou=Department...) to determine the local ID. This is resiliant in the case the user re-generates their key/certificate say because of a card loss or natural expiry of the certificate. This relies on the fact that two CAs won't issue two certs with the same subject name to two different people.

If the certificate was issued by a MS CA it might have a UPN in it that is effectively a domain logon name.

Alternatively if you want to tie the user's identity to an actual certificate the usual method is to store the issuer name and certificate serial number. CAs must issue unique serial numbers for each certificate. Note serial numbers can be large depending on the CA. Not however that using this method then means the cert details in the database must be updated every time the user cert is re-issued.

Excellent information! If I was still working on the project, this is exactly the kind of information I would need to fix the problems I created in the first implementation.However, since I have now moved to a different company with different responsibilities, I'll leave that as an exercise for someone else...Thanks for the feedback!
DaveN59