Real-time CRL validation for X.509 certificates in .NET

This is a continuation of my post on
Avoiding X.509 chain policy caching when using WCF with certificate security.

I wrote the code below to implement certificate validation against a published CRL in real-time. The default mechanism uses a cache / validity period, which might be impractical when you want to deny a revoked certificate at once.

The code looks for a CRL link in the provided certificate and uses it to download the base CRL. It then validates the certificate against the CRL and also retrieves the delta CRL (referenced by the base CRL) and validates against that as well.

I P/Invoked crypto32 API because .NET doesn’t have the necessary classes for handling CRLs.

using System;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

namespace My.IdentityModel
{
    /// <summary>
    /// x509 certificate utilities
    /// Richard Ginzburg - richard@ginzburgconsulting.com
    /// </summary>
    public static class CertUtil
    {
        private const string CERT_CRL_EXTENSION = "2.5.29.31";
        private const string CRL_CRL_EXTENSION = "2.5.29.46";

        public static bool IsCertificateInCrl(X509Certificate2 cert)
        {
            try
            {
                string certCrlUrl = GetBaseCrlUrl(cert);
                return IsCertificateInCrl(cert, certCrlUrl);
            }
            catch
            {
                return false;
            }
        }

        public static bool IsCertificateInCrl(X509Certificate2 cert, string url)
        {
            WebClient wc = new WebClient();
            byte[] rgRawCrl = wc.DownloadData(url);

            IntPtr phCertStore = IntPtr.Zero;
            IntPtr pvContext = IntPtr.Zero;
            GCHandle hCrlData = new GCHandle();
            GCHandle hCryptBlob = new GCHandle();
            try
            {
                hCrlData = GCHandle.Alloc(rgRawCrl, GCHandleType.Pinned);
                WinCrypt32.CRYPTOAPI_BLOB stCryptBlob;
                stCryptBlob.cbData = rgRawCrl.Length;
                stCryptBlob.pbData = hCrlData.AddrOfPinnedObject();
                hCryptBlob = GCHandle.Alloc(stCryptBlob, GCHandleType.Pinned);

                if (!WinCrypt32.CryptQueryObject(
                WinCrypt32.CERT_QUERY_OBJECT_BLOB,
                hCryptBlob.AddrOfPinnedObject(),
                WinCrypt32.CERT_QUERY_CONTENT_FLAG_CRL,
                WinCrypt32.CERT_QUERY_FORMAT_FLAG_BINARY,
                0,
                IntPtr.Zero,
                IntPtr.Zero,
                IntPtr.Zero,
                ref phCertStore,
                IntPtr.Zero,
                ref pvContext
                ))
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                WinCrypt32.CRL_CONTEXT stCrlContext = (WinCrypt32.CRL_CONTEXT)Marshal.PtrToStructure(pvContext, typeof(WinCrypt32.CRL_CONTEXT));
                WinCrypt32.CRL_INFO stCrlInfo = (WinCrypt32.CRL_INFO)Marshal.PtrToStructure(stCrlContext.pCrlInfo, typeof(WinCrypt32.CRL_INFO));

                if (IsCertificateInCrl(cert, stCrlInfo))
                {
                    return true;
                }
                else 
                {
                    url = GetDeltaCrlUrl(stCrlInfo);
                    if (!string.IsNullOrEmpty(url))
                    {
                        return IsCertificateInCrl(cert, url);
                    }
                }
            }
            finally
            {
                if (hCrlData.IsAllocated) hCrlData.Free();
                if (hCryptBlob.IsAllocated) hCryptBlob.Free();
                if (!pvContext.Equals(IntPtr.Zero))
                {
                    WinCrypt32.CertFreeCRLContext(pvContext);
                }
            }

            return false;
        }

        private static bool IsCertificateInCrl(X509Certificate2 cert, WinCrypt32.CRL_INFO stCrlInfo)
        {
            IntPtr rgCrlEntry = stCrlInfo.rgCRLEntry;

            for (int i = 0; i < stCrlInfo.cCRLEntry; i++)
            {
                string serial = string.Empty;
                WinCrypt32.CRL_ENTRY stCrlEntry = (WinCrypt32.CRL_ENTRY)Marshal.PtrToStructure(rgCrlEntry, typeof(WinCrypt32.CRL_ENTRY));

                IntPtr pByte = stCrlEntry.SerialNumber.pbData;
                for (int j = 0; j < stCrlEntry.SerialNumber.cbData; j++)
                {
                    Byte bByte = Marshal.ReadByte(pByte);
                    serial = bByte.ToString("X").PadLeft(2, '0') + serial;
                    pByte = (IntPtr)((Int32)pByte + Marshal.SizeOf(typeof(Byte)));
                }
                if (cert.SerialNumber == serial)
                {
                    return true;
                }
                rgCrlEntry = (IntPtr)((Int32)rgCrlEntry + Marshal.SizeOf(typeof(WinCrypt32.CRL_ENTRY)));
            }
            return false;
        }

        private static string GetBaseCrlUrl(X509Certificate2 cert)
        {
            try
            {
                return (from X509Extension extension in cert.Extensions
                        where extension.Oid.Value.Equals(CERT_CRL_EXTENSION)
                        select GetCrlUrlFromExtension(extension)).Single();
            }
            catch
            {
                return null;
            }
        }

        private static string GetDeltaCrlUrl(WinCrypt32.CRL_INFO stCrlInfo)
        {
            IntPtr rgExtension = stCrlInfo.rgExtension;
            X509Extension deltaCrlExtension = null;

            for (int i = 0; i < stCrlInfo.cExtension; i++)
            {
                WinCrypt32.CERT_EXTENSION stCrlExt = (WinCrypt32.CERT_EXTENSION)Marshal.PtrToStructure(rgExtension, typeof(WinCrypt32.CERT_EXTENSION));

                if (stCrlExt.Value.pbData != IntPtr.Zero && stCrlExt.pszObjId == CRL_CRL_EXTENSION)
                {
                    byte[] rawData = new byte[stCrlExt.Value.cbData];
                    Marshal.Copy(stCrlExt.Value.pbData, rawData, 0, rawData.Length);
                    deltaCrlExtension = new X509Extension(stCrlExt.pszObjId, rawData, stCrlExt.fCritical);
                    break;
                }

                rgExtension = (IntPtr)((Int32)rgExtension + Marshal.SizeOf(typeof(WinCrypt32.CERT_EXTENSION)));
            }
            if (deltaCrlExtension == null)
            {
                return null;
            }
            return GetCrlUrlFromExtension(deltaCrlExtension);
        }

        private static string GetCrlUrlFromExtension(X509Extension extension)
        {
            try
            {
                Regex rx = new Regex("http://.*crl");
                string raw = new AsnEncodedData(extension.Oid, extension.RawData).Format(false);
                return rx.Match(raw).Value;
            }
            catch
            {
                return null;
            }
        }
    }
}

The custom validator is below

using System;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace My.IdentityModel
{
    /// <summary>
    /// Custom x509 certificate validator
    /// Richard Ginzburg - richard@ginzburgconsulting.com
    /// </summary>
    public class MyX509Validator : X509CertificateValidator
    {
        public override void Validate(X509Certificate2 certificate)
        {
            var myChainPolicy = new X509ChainPolicy
                                    {
                                        RevocationMode = X509RevocationMode.Online,
                                        RevocationFlag = X509RevocationFlag.EntireChain,
                                        VerificationFlags = X509VerificationFlags.NoFlag,
                                        UrlRetrievalTimeout = new TimeSpan(0, 0, 10),
                                        VerificationTime = DateTime.Now
                                    };
            var chain = new X509Chain(true) {ChainPolicy = myChainPolicy};

            try
            {
                if (!chain.Build(certificate))
                    throw new SecurityTokenValidationException("Certificate validation failed when building chain");
                if (CertUtil.IsCertificateInCrl(certificate))
                    throw new SecurityTokenValidationException("Certificate is revoked by CRL");
            }
            catch (CryptographicException e)
            {
                throw new SecurityTokenValidationException("Certificate validation failed when building chain, " + e);
            }
        }
    }
}

and finally some crypt32 API

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace My.IdentityModel
{
    public static class WinCrypt32
    {
        #region APIs

        [DllImport("CRYPT32.DLL", EntryPoint = "CryptQueryObject", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern Boolean CryptQueryObject(
            Int32 dwObjectType,
            IntPtr pvObject, 
            Int32 dwExpectedContentTypeFlags,
            Int32 dwExpectedFormatTypeFlags,
            Int32 dwFlags,
            IntPtr pdwMsgAndCertEncodingType,
            IntPtr pdwContentType,
            IntPtr pdwFormatType,
            ref IntPtr phCertStore,
            IntPtr phMsg,
            ref IntPtr ppvContext
            );

        [DllImport("CRYPT32.DLL", EntryPoint = "CertFreeCRLContext", SetLastError = true)]
        public static extern Boolean CertFreeCRLContext(
            IntPtr pCrlContext
        );

        [DllImport("CRYPT32.DLL", EntryPoint = "CertNameToStr", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern Int32 CertNameToStr(
            Int32 dwCertEncodingType,
            ref CRYPTOAPI_BLOB pName,
            Int32 dwStrType,
            StringBuilder psz,
            Int32 csz
        );

        [DllImport("CRYPT32.DLL", EntryPoint = "CertFindExtension", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr CertFindExtension(
            [MarshalAs(UnmanagedType.LPStr)]String pszObjId,
            Int32 cExtensions,
            IntPtr rgExtensions
        );

        [DllImport("CRYPT32.DLL", EntryPoint = "CryptFormatObject", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern Boolean CryptFormatObject(
            Int32 dwCertEncodingType,
            Int32 dwFormatType,
            Int32 dwFormatStrType,
            IntPtr pFormatStruct,
            [MarshalAs(UnmanagedType.LPStr)]String lpszStructType,
            IntPtr pbEncoded,
            Int32 cbEncoded,
            StringBuilder pbFormat,
            ref Int32 pcbFormat
        );

        #endregion APIs

        #region Structs
        [StructLayout(LayoutKind.Sequential)]
        public struct CRYPT_OBJID_BLOB
        {
            public uint cbData;
            [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
            public byte[] pbData;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct CERT_PUBLIC_KEY_INFO
        {
            public CRYPT_ALGORITHM_IDENTIFIER Algorithm;
            public CRYPTOAPI_BLOB PublicKey;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct CERT_EXTENSION
        {
            [MarshalAs(UnmanagedType.LPStr)]
            public string pszObjId;
            public bool fCritical;
            public CRYPTOAPI_BLOB Value;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CERT_CONTEXT
        {
            public uint dwCertEncodingType;
            public IntPtr pbCertEncoded;
            public uint cbCertEncoded;
            public IntPtr pCertInfo;
            public IntPtr hCertStore;
        }

        public struct CERT_INFO
        {
            public int dwVersion;
            public CRYPTOAPI_BLOB SerialNumber;
            public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm;
            public CRYPTOAPI_BLOB Issuer;
            public FILETIME NotBefore;
            public FILETIME NotAfter;
            public CRYPTOAPI_BLOB Subject;
            public CERT_PUBLIC_KEY_INFO SubjectPublicKeyInfo;
            public CRYPTOAPI_BLOB IssuerUniqueId;
            public CRYPTOAPI_BLOB SubjectUniqueId;
            public int cExtension;
            public CERT_EXTENSION rgExtension;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CRL_CONTEXT
        {
            public Int32 dwCertEncodingType;
            public IntPtr pbCrlEncoded;
            public Int32 cbCrlEncoded;
            public IntPtr pCrlInfo;
            public IntPtr hCertStore;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CRL_INFO
        {
            public Int32 dwVersion;
            public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm;
            public CRYPTOAPI_BLOB Issuer;
            public FILETIME ThisUpdate;
            public FILETIME NextUpdate;
            public Int32 cCRLEntry;
            public IntPtr rgCRLEntry;
            public Int32 cExtension;
            public IntPtr rgExtension;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CRYPT_ALGORITHM_IDENTIFIER
        {
            [MarshalAs(UnmanagedType.LPStr)]
            public String pszObjId;
            public CRYPTOAPI_BLOB Parameters;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CRYPTOAPI_BLOB
        {
            public Int32 cbData;
            public IntPtr pbData;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct FILETIME
        {
            public Int32 dwLowDateTime;
            public Int32 dwHighDateTime;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct CRL_ENTRY
        {
            public CRYPTOAPI_BLOB SerialNumber;
            public FILETIME RevocationDate;
            public Int32 cExtension;
            public IntPtr rgExtension;
        }

        #endregion Structs

        #region Consts

        public const Int32 CERT_QUERY_OBJECT_FILE = 0x00000001;
        public const Int32 CERT_QUERY_OBJECT_BLOB = 0x00000002;
        public const Int32 CERT_QUERY_CONTENT_CRL = 3;
        public const Int32 CERT_QUERY_CONTENT_FLAG_CRL = 1 << CERT_QUERY_CONTENT_CRL;
        public const Int32 CERT_QUERY_FORMAT_BINARY = 1;
        public const Int32 CERT_QUERY_FORMAT_BASE64_ENCODED = 2;
        public const Int32 CERT_QUERY_FORMAT_ASN_ASCII_HEX_ENCODED = 3;
        public const Int32 CERT_QUERY_FORMAT_FLAG_BINARY = 1 << CERT_QUERY_FORMAT_BINARY;
        public const Int32 CERT_QUERY_FORMAT_FLAG_BASE64_ENCODED = 1 << CERT_QUERY_FORMAT_BASE64_ENCODED;
        public const Int32 CERT_QUERY_FORMAT_FLAG_ASN_ASCII_HEX_ENCODED = 1 << CERT_QUERY_FORMAT_ASN_ASCII_HEX_ENCODED;
        public const Int32 CERT_QUERY_FORMAT_FLAG_ALL = CERT_QUERY_FORMAT_FLAG_BINARY | CERT_QUERY_FORMAT_FLAG_BASE64_ENCODED | CERT_QUERY_FORMAT_FLAG_ASN_ASCII_HEX_ENCODED;

        public const Int32 X509_ASN_ENCODING = 0x00000001;
        public const Int32 PKCS_7_ASN_ENCODING = 0x00010000;

        public const Int32 X509_NAME = 7;

        public const Int32 CERT_SIMPLE_NAME_STR = 1;
        public const Int32 CERT_OID_NAME_STR = 2;
        public const Int32 CERT_X500_NAME_STR = 3;

        public const String szOID_CRL_REASON_CODE = "2.5.29.21";

        public enum Disposition : uint
        {
            CERT_STORE_ADD_NEW = 1,
            CERT_STORE_ADD_USE_EXISTING = 2,
            CERT_STORE_ADD_REPLACE_EXISTING = 3,
            CERT_STORE_ADD_ALWAYS = 4,
            CERT_STORE_ADD_REPLACE_EXISTING_INHERIT_PROPERTIES = 5,
            CERT_STORE_ADD_NEWER = 6,
            CERT_STORE_ADD_NEWER_INHERIT_PROPERTIES = 7,
        }

        [Flags]
        public enum FindFlags : int
        {
            CRL_FIND_ISSUED_BY_AKI_FLAG = 0x1,
            CRL_FIND_ISSUED_BY_SIGNATURE_FLAG = 0x2,
            CRL_FIND_ISSUED_BY_DELTA_FLAG = 0x4,
            CRL_FIND_ISSUED_BY_BASE_FLAG = 0x8,
        }

        public enum FindType : int
        {
            CRL_FIND_ANY = 0,
            CRL_FIND_ISSUED_BY = 1,
            CRL_FIND_EXISTING = 2,
            CRL_FIND_ISSUED_FOR = 3
        }

        #endregion
    }
}

Fixing “Value does not fall within the expected range” error in MOSS page settings

I wrote this script to correct the error I got from MOSS when trying to edit page settings. Our site uses an internal url (wwwint.mysite.com) and an external one (www.mysite.com). The page layout seemed to have been reset to use the internal url instead of the external one, which generated the errors.

I first tried to fix the error using the gl-fixpublishingpagespagelayouturl command from Gary Lapointe’s stsadm commands, but that didn’t work, so I wrote my own custom script.

I’m posting the script here as a reference.

[ArgumentException: Value does not fall within the expected range.]
   Microsoft.SharePoint.Library.SPRequestInternalClass.GetFileAndMetaInfo(String bstrUrl, Byte bPageView, Byte bPageMode, Byte bGetBuildDependencySet, String bstrCurrentFolderUrl, Boolean& pbCanCustomizePages, Boolean& pbCanPersonalizeWebParts, Boolean& pbCanAddDeleteWebParts, Boolean& pbGhostedDocument, Boolean& pbDefaultToPersonal, String& pbstrSiteRoot, Guid& pgSiteId, UInt32& pdwVersion, String& pbstrTimeLastModified, String& pbstrContent, Byte& pVerGhostedSetupPath, UInt32& pdwPartCount, Object& pvarMetaData, Object& pvarMultipleMeetingDoclibRootFolders, String& pbstrRedirectUrl, Boolean& pbObjectIsList, Guid& pgListId, UInt32& pdwItemId, Int64& pllListFlags, Boolean& pbAccessDenied, Guid& pgDocId, Byte& piLevel, UInt64& ppermMask, Object& pvarBuildDependencySet, UInt32& pdwNumBuildDependencies, Object& pvarBuildDependencies, String& pbstrFolderUrl, String& pbstrContentTypeOrder) +0
   Microsoft.SharePoint.Library.SPRequest.GetFileAndMetaInfo(String bstrUrl, Byte bPageView, Byte bPageMode, Byte bGetBuildDependencySet, String bstrCurrentFolderUrl, Boolean& pbCanCustomizePages, Boolean& pbCanPersonalizeWebParts, Boolean& pbCanAddDeleteWebParts, Boolean& pbGhostedDocument, Boolean& pbDefaultToPersonal, String& pbstrSiteRoot, Guid& pgSiteId, UInt32& pdwVersion, String& pbstrTimeLastModified, String& pbstrContent, Byte& pVerGhostedSetupPath, UInt32& pdwPartCount, Object& pvarMetaData, Object& pvarMultipleMeetingDoclibRootFolders, String& pbstrRedirectUrl, Boolean& pbObjectIsList, Guid& pgListId, UInt32& pdwItemId, Int64& pllListFlags, Boolean& pbAccessDenied, Guid& pgDocId, Byte& piLevel, UInt64& ppermMask, Object& pvarBuildDependencySet, UInt32& pdwNumBuildDependencies, Object& pvarBuildDependencies, String& pbstrFolderUrl, String& pbstrContentTypeOrder) +215
   Microsoft.SharePoint.SPWeb.GetWebPartPageContent(Uri pageUrl, PageView requestedView, HttpContext context, Boolean forRender, Boolean includeHidden, Boolean mainFileRequest, Boolean fetchDependencyInformation, Boolean& ghostedPage, Byte& verGhostedPage, String& siteRoot, Guid& siteId, Int64& bytes, Guid& docId, UInt32& docVersion, String& timeLastModified, Byte& level, Object& buildDependencySetData, UInt32& dependencyCount, Object& buildDependencies, SPWebPartCollectionInitialState& initialState, Object& oMultipleMeetingDoclibRootFolders, String& redirectUrl, Boolean& ObjectIsList, Guid& listId) +1533
   Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.FetchWebPartPageInformationForInit(HttpContext context, SPWeb spweb, Boolean mainFileRequest, String path, Boolean impersonate, Boolean& fGhostedPage, Byte& verGhostedPage, Guid& docId, UInt32& docVersion, String& timeLastModified, SPFileLevel& spLevel, String& masterPageUrl, String& customMasterPageUrl, String& webUrl, String& siteUrl, Guid& siteId, Object& buildDependencySetData, SPWebPartCollectionInitialState& initialState, String& siteRoot, String& redirectUrl, Object& oMultipleMeetingDoclibRootFolders, Boolean& objectIsList, Guid& listId, Int64& bytes) +691
   Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.FetchWebPartPageInformation(HttpContext context, String path, Boolean impersonate, Boolean& fGhostedPage, Byte& verGhostedPage, Guid& docId, UInt32& docVersion, String& timeLastModified, SPFileLevel& level, String& masterpageUrl, String& customMasterPageUrl, String& webUrl, String& siteUrl, Guid& siteId, Object& buildDependencySetData) +132
   Microsoft.SharePoint.ApplicationRuntime.SPRequestModuleData.GetWebPartPageData(HttpContext context, String path, Boolean throwIfFileNotFound) +773
   Microsoft.SharePoint.ApplicationRuntime.SPVirtualFile.GetFile(String virtualPath, Boolean fetchContent) +78
   Microsoft.SharePoint.ApplicationRuntime.SPVirtualFile.GetFile(String virtualPath) +30
   Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider.GetFile(String virtualPath) +171
   System.Web.Hosting.VirtualPathProvider.GetFile(String virtualPath) +18
   System.Web.Hosting.VirtualPathProvider.GetFileWithCheck(String virtualPath) +11
   System.Web.FormatterWithFileInfo.GetSourceFileLines(String fileName, Encoding encoding, String sourceCode, Int32 lineNumber) +229
   System.Web.DynamicCompileErrorFormatter.get_MiscSectionContent() +926
   System.Web.ErrorFormatter.GetHtmlErrorMessage(Boolean dontShowSensitiveInfo) +837
   System.Web.HttpResponse.WriteErrorMessage(Exception e, Boolean dontShowSensitiveErrors) +820
   System.Web.HttpResponse.ReportRuntimeError(Exception e, Boolean canThrow, Boolean localExecute) +560
   System.Web.HttpRuntime.FinishRequest(HttpWorkerRequest wr, HttpContext context, Exception e) +333
# Updates page layout (wwwint -> www)

[System.Reflection.Assembly]::LoadWithPartialName(“Microsoft.SharePoint”)
[System.Reflection.Assembly]::LoadWithPartialName(“Microsoft.SharePoint.Publishing”)

$site = new-Object Microsoft.SharePoint.SPSite("http://www.mysite.com/myweb")
$web = $site.OpenWeb("subweb")
$pweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($web)
$global:page = $pweb.GetPublishingPages()["Pages/mypage.aspx"]
$layout = $global:page.ListItem[[Microsoft.SharePoint.Publishing.FieldId]::PageLayout]
$newlayout = $layout.replace("wwwint", "www")

$global:page.ListItem[[Microsoft.SharePoint.Publishing.FieldId]::PageLayout] = $newlayout
$global:page.ListItem.SystemUpdate()

Avoiding X.509 chain policy caching when using WCF with certificate security

A while ago my customer came to me with an issue that was a bit tricky to find a solution to.

Their system uses a Windows forms client and a WCF service running on IIS7 on a Windows 2008 R2 server. The users are provided with personal certificates stored on smart cards. Those certificates are then used to authenticate the users with the WCF service using their AD accounts.

The issue my customer was facing was:
1. New employees could not log on to the system until the application pool of the web site running the WCF service was recycled.
2. Employees who got their certificates revokeed COULD log on to the system the application pool of the web site running the WCF service was recycled.

Obviously a (totally unwanted) certificate cache of some sort was causing this behavior, so after digging around a bit I found out that System.IdentityModel was caching the X.509 chain policy in memory.

The relevant web.config settings for the WCF service looked like this:

<clientCertificate>
	<authentication mapClientCertificateToWindowsAccount="true" certificateValidationMode="ChainTrust" revocationMode="Online"/>
</clientCertificate>

To solve this issue I decided to implement a custom X.509 validator that builds a X509Chain manually to prevent the policy from being cached.

I changed the above web.config lines to

<clientCertificate>
	<authentication mapClientCertificateToWindowsAccount="true" certificateValidationMode="Custom" customCertificateValidatorType="My.IdentityModel.MyX509Validator, My.IdentityModel" />
</clientCertificate>

and implemented the custom validator like this:

using System;
using System.IO;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace My.IdentityModel
{
    /// <summary>
    /// Custom x509 certificate validator
    /// Richard Ginzburg - richard@ginzburgconsulting.com
    /// </summary>
    public class MyX509Validator : X509CertificateValidator
    {
        public override void Validate(X509Certificate2 certificate)
        {
            if (certificate == null)
            {
                throw new ArgumentNullException("certificate", "Certificate validation failed, no certificate provided");
            }

            X509ChainPolicy myChainPolicy = new X509ChainPolicy
                                                {
                                                    RevocationMode = X509RevocationMode.Online,
                                                    RevocationFlag = X509RevocationFlag.EntireChain,
                                                    VerificationFlags = X509VerificationFlags.NoFlag,
                                                    UrlRetrievalTimeout = new TimeSpan(0, 0, 10),
                                                    VerificationTime = DateTime.Now
                                                };
            X509Chain chain = new X509Chain(true) {ChainPolicy = myChainPolicy};

            try
            {
                bool ok = chain.Build(certificate);
                if(!ok)
                {
                    foreach (var status in chain.ChainStatus)
                    {
                        Logging.Log("MyX509Validator: Validation failed - " + status.StatusInformation);
                    }
                    throw new SecurityTokenValidationException("Certificate validation failed when building chain");
                }
            }
            catch (CryptographicException e)
            {
                throw new SecurityTokenValidationException("Certificate validation failed when building chain, " + e);
            }
        }
    }
}

This resolved the issue, now the changes in certificate validity take effect immediately.

Some documentation on certificate validity checking can be found here: http://technet.microsoft.com/en-us/library/bb457027.aspx .

Field not found: CatalogResourceMajorVersion from Commerce Server 2007

So I was trying to troubleshoot this Biztalk 2006 R2 orchestration that includes importing some data into a Commerce Server 2007 instance. CS doesn’t really want to play ball, so I decide to upgrade it to the latest service pack as that’s supposed to resolve some of the issues. Download SP2, install, migrate, restart IIS… BOOM! Exception:
MissingFieldException: Field not found: 'Microsoft.CommerceServer.Catalog.Internal.Constants.CatalogResourceMajorVersion'.]
Microsoft.CommerceServer.Catalog.CatalogContext.InternalCreate(CatalogSiteAgent catalogSiteAgent, DebugContext debugContext, CacheConfiguration cacheConfiguration) +708
Microsoft.CommerceServer.Runtime.Catalog.CommerceCatalogModule.CreateCatalogContext() +293
Microsoft.CommerceServer.Runtime.Catalog.CommerceCatalogModule.get_CatalogContext() +138
Microsoft.CommerceServer.Runtime.Catalog.CommerceCatalogModule.OnBeginRequest(Object sender, EventArgs e) +24
System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean&amp; completedSynchronously) +171

Google is, for once, silent… So I check the GAC and compare the version Microsoft.CommerceServer.Catalog.dll to the other CS dlls – bingo, the version of the dll is 6.0.3724.0. A quick search leads you to this MSDN blog post which tells you which version corresponds to which SP…

Apparently the dll failed to register during the sp2 install which led to the weirdness described above.

As I couldn’t find the right dll anywhere and was not able to reinstall sp2 I had to install sp1 and THEN sp2. This finally took care of the problem.

NULL values in SOAP requests

Question: how to pass a NULL value in a SOAP envelope?
Answer: Use the xsi:nil=”true” attribute on the element, and don’t forget the xsi namespace in the envelope.

Example SOAP request:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <soapenv:Header/>
   <soapenv:Body>
      <TestService>
         <request>
            <PassNullHere xsi:nil="true" />
         </request>
      </TestService>
   </soapenv:Body>
</soapenv:Envelope>

Listing subcategories for a category in Commerce Server 2007 using a SQL query

As promised, here is the follow-up to the last post. This query gets us the names of all subcategories of a particular category in Commerce Server 2007.

DECLARE @categoryOid AS INT
SET @categoryOid = 937 ;

WITH    Cats2 ( oid, child_oid, hierarchy_level )
          AS ( SELECT   oid ,
                        child_oid ,
                        1 AS hierarchy_level
               FROM     dbo.ConsumerShop_CatalogHierarchy
               WHERE    oid = @categoryOid
                        AND CatalogName = 'ConsumerStaging'
               UNION ALL
               SELECT   h.oid ,
                        h.child_oid ,
                        c.hierarchy_level + 1 AS hierarchy_level
               FROM     dbo.ConsumerShop_CatalogHierarchy h
                        JOIN Cats2 c ON h.oid = c.child_oid
                                        AND h.CatalogName = 'ConsumerStaging'
             )
    SELECT  CategoryName
    FROM    dbo.ConsumerShop_CatalogProducts
    WHERE   oid IN ( SELECT DISTINCT ( oid ) 
					 FROM   Cats2 )

Listing categories for a product in Commerce Server 2007 using a SQL query

Commerce Server API is not exactly known for being lightning-fast, so sometimes you want to skip it altogether and just use plain old SQL to get the data.

This time I needed to find all categories (at a particular level) that a product belongs to. This was solved by the script below, which is using a recursive CTE to walk upwards through the tree.

Just set the @productId and @hierarchyLevel below and voilá!

DECLARE @hierarchyLevel INT
DECLARE @productId VARCHAR(10)
SET @hierarchyLevel = 3
SET @productId = '25853' ;

WITH Cats (oid, child_oid, hierarchy_level) AS
	(
		SELECT h.oid, h.child_oid, 1 AS hierarchy_level
		FROM dbo.ConsumerShop_CatalogHierarchy h 
		JOIN dbo.ConsumerShop_CatalogProducts p ON h.child_oid = p.oid
		WHERE p.ProductID = @productId
		AND h.CatalogName = 'ConsumerStaging'

		UNION ALL 

		SELECT h.oid, h.child_oid, c.hierarchy_level + 1 AS hierarchy_level
		FROM dbo.ConsumerShop_CatalogHierarchy h
		JOIN Cats c ON c.oid = h.child_oid
		WHERE h.CatalogName = 'ConsumerStaging'
	)
    SELECT  CategoryName
    FROM    Cats
            JOIN dbo.ConsumerShop_CatalogProducts p ON Cats.oid = p.oid
    WHERE   hierarchy_level = @hierarchyLevel
            AND BaseCatalogName = 'ConsumerStaging'

In my next post I will explain how to accomplish the opposite, i.e. find all subcategories of a category.

List does not exits in Sharepoint 2007 when accessing cache settings

When trying to access the cache settings in Sharepoint 2007 (Site Collection Administration section, Site collection output cache link).

The error I got was:

List does not exist
The page you selected contains a list that does not exist. It may have been deleted by another user.
at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32&amp; plRecycleBinCount) at Microsoft.SharePoint.Library.SPRequest.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32&amp; plRecycleBinCount

After a bit of searching and asking around I found out that the culpit is the Office Sharepoint Server Publishing Infrastructure feature.

Apparently caching control breaks when a site is upgraded and this feature has to be deactivated and then reactivated to resolve the issue. The feature can be found in site collection features section.

Dumping / restoring SQL Server data using Powershell

Sometimes you need to transfer data between two SQL Server databases. This Powershell 2.0 script might come in handy. It dumps one or all tables from the specified database to .DAT files and later restores them to another database with the same schema. Basically a convenient wrapper for bcp.

param([string]$action, [string]$srv, [string]$db, [string]$src)

$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$fileName = Split-Path $MyInvocation.MyCommand.Path -Leaf

$sqlroot = "C:Program FilesMicrosoft SQL Server90"
$bcp = "$sqlrootToolsbinnbcp.exe"
$sqlcmd = "$sqlrootToolsbinnsqlcmd.exe"

function Dump-All
{
	Write-Host "Dumping all tables from $db..." -Fore Cyan
	[void][reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
	$smosrv = New-Object Microsoft.SqlServer.Management.Smo.Server $srv
	$smodb = $smosrv.databases[$db]
	$smodb.Tables | % { 
		Dump $_.Name
	}
	Write-Host "All tables dumped" -Fore Green
}

function Dump($tbl)
{
	Write-Host "Dumping table $db.dbo.$tbl to $tbl.dat..." -Fore Cyan
	$args = "$db.dbo.$tbl", "out", "$tbl.dat", "-S$srv", "-T", "-w", "-E"
	& $bcp $args
	Write-Host "Dump completed" -Fore Green
}

function Restore-All
{
	Write-Host "Restoring all tables from $src..." -Fore Cyan
	(Get-ChildItem "$src*.DAT") | % { 
		Restore $_
	}
	Write-Host "All tables restored" -Fore Green
}

function Restore($datfile)
{
	if($datfile.Name)
	{
		$table = $datfile.Name.ToLower().Replace(".dat", "")
		$file = $datfile.FullName
	}
	else
	{
		$file = (Split-Path $datfile -Leaf)
		$table = $file.ToLower().Replace(".dat", "")
	}
	Write-Host "Restoring table $db.dbo.$table from $file..." -Fore Cyan

	& $sqlcmd -S $srv -E -d $db -Q `"TRUNCATE TABLE $table`"

	$args = "$db.dbo.$table", "in", "$file", "-S$srv", "-T", "-w", "-E"
	& $bcp $args
	Write-Host "Restore completed" -Fore Green
}

function Print-Help
{
	Write-Host "USAGE:" -Fore White
	Write-Host
	Write-Host "  .DbDumper out <servername> <database> <table | ALL>" -Fore White
	Write-Host "  Exports data from specified table to a file named after the table with a .DAT extension." -Fore White
	Write-Host "  If ALL is specified instead of table name, all tables will be dumped." -Fore White
	Write-Host "  Example: .DbDumper.ps1 out . APO_Profiles UserObject" -Fore White
	Write-Host
	Write-Host "  .DbDumper in <servername> <database> <filename | path>" -Fore White
	Write-Host "  Imports data from specified file to specified database." -Fore White
	Write-Host "  The file should have the same name as the table with a .DAT extension." -Fore White
	Write-Host "  If a path is specified, ALL files will be imported." -Fore White
	Write-Host "  Example: .DbDumper.ps1 in . APO_Profiles UserObject.DAT" -Fore White
	Write-Host
}

function main
{
	if($action -eq "out")
	{
		if($src -eq "ALL")
		{
			Dump-All
		}
		else
		{
			Dump $src
		}
	}
	elseif($action -eq "in")
	{
		Write-Host "WARNING!!! This will DELETE all data in the affected tables!!! ESC to abort." -Fore Yellow
		$key = $host.ui.RawUI.ReadKey("NoEcho,IncludeKeyUp,IncludeKeyDown")
        if ($key.VirtualKeyCode -eq 27) # ESCAPE
		{
			return;
		}

		if((Test-Path $src -PathType Container))
		{
			Restore-All
		}
		else
		{
			Restore $src
		}
	}
	else
	{
		Print-Help
	}
}

main