views:

695

answers:

6

We are developing a number of WCF services. requests will cross a domain boundry; that is, the clients are running in one domain and the servers handling the requests are in a different (production) domain. I know how to secure this link with SSL and certificates. We will aks the users for their usernames and passwords on the production domain and pass those in the SOAP headers.

My problem is what to do during development and "beta" testing. I know that I can get a temporary certificate and use that during development. I am wondering what my alternatives to this approach are. What have others done in this situation?

Update: I am not sure that I have gotten a "good" answer to my question. I am part of a large team (50+) of developers. The organization is fairly agile. Any one of the developers could end up working on the project that is using WCF. In fact several of the other projects are doing something similar but for different web sites and services. What I was looking for was a way that I could have anyone come in and work on this particular project for a few days without having to jump through a number of hoops. Installing the development certificate is one of those hoops. I fully understand that "dogfooding" the WCF structure during development is the best practice. Most the answers gave that as the answer. I wanted to know what, if anything, made sense that was other than "get a test certificate (or two) and install it on all of the developer boxes."

Jon

A: 

How about changing the configuration between development and production?

John Saunders
A: 

My suggestion would be to consider a couple of different approaches:

For development -> There are ways to generate an SSL certificate locally so that tests with https can be done in an environment that you have total control over.

For "beta" testing -> Consider getting a second certificate for this as there may be a continuous need to do some beta testing between releases so it likely can be used over and over again.

JB King
+2  A: 

Really you want your development environment to match production as much as possible. WCF will check revocation lists during transport negotiation or signature checking and self signed certificates, or faked certifications using makecert do not support CRLs.

If you have a spare machine you could use Windows Certificate Services (free with Server 2003 and 2008). This provides a CA and you can request certificates (SSL or client) from it. It needs to be a spare machine as it sets itself up under the default web site and completely messes up if you have already tweaked that. It also publishes CRLs. All you would need to do is to install the root certificate for the CA on your development boxes and away you go.

blowdart
+1  A: 

You have the option to either generate a certificate to use in development, or disabling the use of certificates through the configuration file. I would recommend actually using a certificate also in development.

baretta
+5  A: 

Only recommended for use in Development of course, not production, but there is a low-friction way to generate X.509 certificates (without resorting to makecert.exe).

If you have access to CryptoAPI on Windows, the idea is that you use CryptoAPI calls to generate RSA public and private keys, sign and encode a new X.509 certificate, put it in a memory-only certificate store, and then use PFXExportCertStore() to generate the .pfx bytes which you can then pass to X509Certificate2 constructor.

Once you have an X509Certificate2 instance, you can set it as a property of the appropriate WCF objects, and things just start working.

I have some example code I've written, no guarantees of any sort of course, and you'll need a bit of C experience to write the bits that have to be unmanaged (it would be a lot more painful to write the P/Invoke for all CryptoAPI calls than to have that part be in C/C++).

Example C# code that uses the unmanaged helper function:

    public X509Certificate2 GenerateSelfSignedCertificate(string issuerCommonName, string keyPassword)
    {
        int pfxSize = -1;
        IntPtr pfxBufferPtr = IntPtr.Zero;
        IntPtr errorMessagePtr = IntPtr.Zero;

        try
        {
            if (!X509GenerateSelfSignedCertificate(KeyContainerName, issuerCommonName, keyPassword, ref pfxSize, ref pfxBufferPtr, ref errorMessagePtr))
            {
                string errorMessage = null;

                if (errorMessagePtr != IntPtr.Zero)
                {
                    errorMessage = Marshal.PtrToStringUni(errorMessagePtr);
                }

                throw new ApplicationException(string.Format("Failed to generate X.509 server certificate. {0}", errorMessage ?? "Unspecified error."));
            }
            if (pfxBufferPtr == IntPtr.Zero)
            {
                throw new ApplicationException("Failed to generate X.509 server certificate. PFX buffer not initialized.");
            }
            if (pfxSize <= 0)
            {
                throw new ApplicationException("Failed to generate X.509 server certificate. PFX buffer size invalid.");
            }

            byte[] pfxBuffer = new byte[pfxSize];
            Marshal.Copy(pfxBufferPtr, pfxBuffer, 0, pfxSize);
            return new X509Certificate2(pfxBuffer, keyPassword);
        }
        finally
        {
            if (pfxBufferPtr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(pfxBufferPtr);
            }
            if (errorMessagePtr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(errorMessagePtr);
            }
        }
    }

The X509GenerateSelfSignedCertificate function implementation could go something like this (you need WinCrypt.h):

BOOL X509GenerateSelfSignedCertificate(LPCTSTR keyContainerName, LPCTSTR issuerCommonName, LPCTSTR keyPassword, DWORD *pfxSize, BYTE **pfxBuffer, LPTSTR *errorMessage)
{
    // Constants
#define CERT_DN_ATTR_COUNT    1
#define SIZE_SERIALNUMBER     8
#define EXPIRY_YEARS_FROM_NOW 2
#define MAX_COMMON_NAME       8192
#define MAX_PFX_SIZE          65535

    // Declarations
    HCRYPTPROV hProv = NULL;
    BOOL result = FALSE;

    // Sanity

    if (pfxSize != NULL)
    {
     *pfxSize = -1;
    }
    if (pfxBuffer != NULL)
    {
     *pfxBuffer = NULL;
    }
    if (errorMessage != NULL)
    {
     *errorMessage = NULL;
    }

    if (keyContainerName == NULL || _tcslen(issuerCommonName) <= 0)
    {
     SetOutputErrorMessage(errorMessage, _T("Key container name must not be NULL or an empty string."));
     return FALSE;
    }
    if (issuerCommonName == NULL || _tcslen(issuerCommonName) <= 0)
    {
     SetOutputErrorMessage(errorMessage, _T("Issuer common name must not be NULL or an empty string."));
     return FALSE;
    }
    if (keyPassword == NULL || _tcslen(keyPassword) <= 0)
    {
     SetOutputErrorMessage(errorMessage, _T("Key password must not be NULL or an empty string."));
     return FALSE;
    }

    // Start generating
    USES_CONVERSION;

    if (CryptAcquireContext(&hProv, keyContainerName, MS_DEF_RSA_SCHANNEL_PROV, PROV_RSA_SCHANNEL, CRYPT_MACHINE_KEYSET) ||
     CryptAcquireContext(&hProv, keyContainerName, MS_DEF_RSA_SCHANNEL_PROV, PROV_RSA_SCHANNEL, CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET))
    {
     HCRYPTKEY hKey = NULL;

     // Generate 1024-bit RSA keypair.
     if (CryptGenKey(hProv, AT_KEYEXCHANGE, CRYPT_EXPORTABLE | RSA1024BIT_KEY, &hKey))
     {
      DWORD pkSize = 0;
      PCERT_PUBLIC_KEY_INFO pkInfo = NULL;

      // Export public key for use by certificate signing.
      if (CryptExportPublicKeyInfo(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING, NULL, &pkSize) &&
       (pkInfo = (PCERT_PUBLIC_KEY_INFO)LocalAlloc(0, pkSize)) &&
       CryptExportPublicKeyInfo(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING, pkInfo, &pkSize))
      {
       CERT_RDN_ATTR certDNAttrs[CERT_DN_ATTR_COUNT];
       CERT_RDN certDN[CERT_DN_ATTR_COUNT] = {{1, &certDNAttrs[0]}};
       CERT_NAME_INFO certNameInfo = {CERT_DN_ATTR_COUNT, &certDN[0]};
       DWORD certNameSize = -1;
       BYTE *certNameData = NULL;

       certDNAttrs[0].dwValueType = CERT_RDN_UNICODE_STRING;
       certDNAttrs[0].pszObjId = szOID_COMMON_NAME;
       certDNAttrs[0].Value.cbData = (DWORD)(_tcslen(issuerCommonName) * sizeof(WCHAR));
       certDNAttrs[0].Value.pbData = (BYTE*)T2W((LPTSTR)issuerCommonName);

       // Encode issuer name into certificate name blob.
       if (CryptEncodeObject(X509_ASN_ENCODING, X509_NAME, &certNameInfo, NULL, &certNameSize) &&
        (certNameData = (BYTE*)LocalAlloc(0, certNameSize)) &&
           CryptEncodeObject(X509_ASN_ENCODING, X509_NAME, &certNameInfo, certNameData, &certNameSize))
       {
        CERT_NAME_BLOB issuerName;
        CERT_INFO certInfo;
        SYSTEMTIME systemTime;
        FILETIME notBefore;
        FILETIME notAfter;
        BYTE serialNumber[SIZE_SERIALNUMBER];
        DWORD certSize = -1;
        BYTE *certData = NULL;

        issuerName.cbData = certNameSize;
        issuerName.pbData = certNameData;

        // Certificate should be valid for a decent window of time.
        ZeroMemory(&certInfo, sizeof(certInfo));
        GetSystemTime(&systemTime);
        systemTime.wYear -= 1;
        SystemTimeToFileTime(&systemTime, &notBefore);
        systemTime.wYear += EXPIRY_YEARS_FROM_NOW;
        SystemTimeToFileTime(&systemTime, &notAfter);

        // Generate a throwaway serial number.
        if (CryptGenRandom(hProv, SIZE_SERIALNUMBER, serialNumber))
        {
         certInfo.dwVersion = CERT_V3;
         certInfo.SerialNumber.cbData = SIZE_SERIALNUMBER;
         certInfo.SerialNumber.pbData = serialNumber;
         certInfo.SignatureAlgorithm.pszObjId = szOID_RSA_MD5RSA;
         certInfo.Issuer = issuerName;
         certInfo.NotBefore = notBefore;
         certInfo.NotAfter = notAfter;
         certInfo.Subject = issuerName;
         certInfo.SubjectPublicKeyInfo = *pkInfo;

         // Now sign and encode it.
         if (CryptSignAndEncodeCertificate(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING, X509_CERT_TO_BE_SIGNED, (LPVOID)&certInfo, &(certInfo.SignatureAlgorithm), NULL, NULL, &certSize) &&
          (certData = (BYTE*)LocalAlloc(0, certSize)) &&
          CryptSignAndEncodeCertificate(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING, X509_CERT_TO_BE_SIGNED, (LPVOID)&certInfo, &(certInfo.SignatureAlgorithm), NULL, certData, &certSize))
         {
          HCERTSTORE hCertStore = NULL;

          // Open a new temporary store.
          if ((hCertStore = CertOpenStore(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING, NULL, CERT_STORE_CREATE_NEW_FLAG, NULL)))
          {
           PCCERT_CONTEXT certContext = NULL;

           // Add to temporary store so we can use the PFX functions to export a store + private keys in PFX format.
           if (CertAddEncodedCertificateToStore(hCertStore, X509_ASN_ENCODING, certData, certSize, CERT_STORE_ADD_NEW, &certContext))
           {
            CRYPT_KEY_PROV_INFO keyProviderInfo;

            // Link keypair to certificate (without this the keypair gets "lost" on export).
            ZeroMemory(&keyProviderInfo, sizeof(keyProviderInfo));
            keyProviderInfo.pwszContainerName = T2W((LPTSTR)keyContainerName);
            keyProviderInfo.pwszProvName = MS_DEF_RSA_SCHANNEL_PROV_W; /* _W used intentionally. struct hardcodes LPWSTR. */
            keyProviderInfo.dwProvType = PROV_RSA_SCHANNEL;
            keyProviderInfo.dwFlags = CRYPT_MACHINE_KEYSET;
            keyProviderInfo.dwKeySpec = AT_KEYEXCHANGE;

            // Finally, export to PFX and provide to caller.
            if (CertSetCertificateContextProperty(certContext, CERT_KEY_PROV_INFO_PROP_ID, 0, (LPVOID)&keyProviderInfo))
            {
             CRYPT_DATA_BLOB pfxBlob;
             DWORD pfxExportFlags = EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY;

             // Calculate size required.
             ZeroMemory(&pfxBlob, sizeof(pfxBlob));
             if (PFXExportCertStore(hCertStore, &pfxBlob, T2CW(keyPassword), pfxExportFlags))
             {
              pfxBlob.pbData = (BYTE *)LocalAlloc(0, pfxBlob.cbData);

              if (pfxBlob.pbData != NULL)
              {
               // Now export.
               if (PFXExportCertStore(hCertStore, &pfxBlob, T2CW(keyPassword), pfxExportFlags))
               {
                if (pfxSize != NULL)
                {
                 *pfxSize = pfxBlob.cbData;
                }
                if (pfxBuffer != NULL)
                {
                 // Caller must free this.
                 *pfxBuffer = pfxBlob.pbData;
                }
                else
                {
                 // Caller did not provide target pointer to receive buffer, free ourselves.
                 LocalFree(pfxBlob.pbData);
                }

                result = TRUE;
               }
               else
               {
                SetOutputErrorMessage(errorMessage, _T("Failed to export certificate in PFX format (0x%08x)."), GetLastError());
               }
              }
              else
              {
               SetOutputErrorMessage(errorMessage, _T("Failed to export certificate in PFX format, buffer allocation failure (0x%08x)."), GetLastError());
              }
             }
             else
             {
              SetOutputErrorMessage(errorMessage, _T("Failed to export certificate in PFX format, failed to calculate buffer size (0x%08x)."), GetLastError());
             }
            }
            else
            {
             SetOutputErrorMessage(errorMessage, _T("Failed to set certificate key context property (0x%08x)."), GetLastError());
            }
           }
           else
           {
            SetOutputErrorMessage(errorMessage, _T("Failed to add certificate to temporary certificate store (0x%08x)."), GetLastError());
           }

           CertCloseStore(hCertStore, 0);
           hCertStore = NULL;
          }
          else
          {
           SetOutputErrorMessage(errorMessage, _T("Failed to create temporary certificate store (0x%08x)."), GetLastError());
          }
         }
         else
         {
          SetOutputErrorMessage(errorMessage, _T("Failed to sign/encode certificate or out of memory (0x%08x)."), GetLastError());
         }

         if (certData != NULL)
         {
          LocalFree(certData);
          certData = NULL;
         }
        }
        else
        {
         SetOutputErrorMessage(errorMessage, _T("Failed to generate certificate serial number (0x%08x)."), GetLastError());
        }
       }
       else
       {
        SetOutputErrorMessage(errorMessage, _T("Failed to encode X.509 certificate name into ASN.1 or out of memory (0x%08x)."), GetLastError());
       }

       if (certNameData != NULL)
       {
        LocalFree(certNameData);
        certNameData = NULL;
       }
      }
      else
      {
       SetOutputErrorMessage(errorMessage, _T("Failed to export public key blob or out of memory (0x%08x)."), GetLastError());
      }

      if (pkInfo != NULL)
      {
       LocalFree(pkInfo);
       pkInfo = NULL;
      }
      CryptDestroyKey(hKey);
      hKey = NULL;
     }
     else
     {
      SetOutputErrorMessage(errorMessage, _T("Failed to generate public/private keypair for certificate (0x%08x)."), GetLastError());
     }

     CryptReleaseContext(hProv, 0);
     hProv = NULL;
    }
    else
    {
     SetOutputErrorMessage(errorMessage, _T("Failed to acquire cryptographic context (0x%08x)."), GetLastError());
    }

    return result;
}

void
SetOutputErrorMessage(LPTSTR *errorMessage, LPCTSTR formatString, ...)
{
#define MAX_ERROR_MESSAGE 1024
    va_list va;

    if (errorMessage != NULL)
    {
     size_t sizeInBytes = (MAX_ERROR_MESSAGE * sizeof(TCHAR)) + 1;
     LPTSTR message = (LPTSTR)LocalAlloc(0, sizeInBytes);

     va_start(va, formatString);
     ZeroMemory(message, sizeInBytes);
     if (_vstprintf_s(message, MAX_ERROR_MESSAGE, formatString, va) == -1)
     {
      ZeroMemory(message, sizeInBytes);
      _tcscpy_s(message, MAX_ERROR_MESSAGE, _T("Failed to build error message"));
     }

     *errorMessage = message;

     va_end(va);
    }
}

We've used this to generate SSL certificates on startup, which is fine when you only want to test encryption and not verify trust/identity, and it takes only about 2-3 seconds to generate.

Leon Breedt
A: 

Extending Leon Breedt's answer, to generate the in-memory x509 certifcate you can use the source code from Keith Elder's SelfCert.

using (CryptContext ctx = new CryptContext())
{
    ctx.Open();

    var cert = ctx.CreateSelfSignedCertificate(
        new SelfSignedCertProperties
        {
            IsPrivateKeyExportable = true,
            KeyBitLength = 4096,
            Name = new X500DistinguishedName("cn=InMemoryTestCert"),
            ValidFrom = DateTime.Today.AddDays(-1),
            ValidTo = DateTime.Today.AddYears(5),
        });

    var creds = new ServiceCredentials();
    creds.UserNameAuthentication.CustomUserNamePasswordValidator = new MyUserNamePasswordValidator();
    creds.ServiceCertificate.Certificate = cert;
    serviceHost.Description.Behaviors.Add(creds);
}
Paul Stovell