Introduction
Handling sensitive data is de jour in modern programming and doing so in a consistent method should be part of any secure programming best practices. In many cases, the requirements of regulatory or industry standards bodies must also be met. For example, the data security standards of the credit card industry go so far as to define expectations on data-in-flight as well as data-at-rest, including data-at-rest transiently in memory. .NET has System.SecureString
to help with the latter, and WCF provides both Transport and Message encryption to deal with the former. However, the two cannot be simply married in a DataContract
because SecureString
is deliberately non-serializable. This article describes a wrapper for SecureString
that is able to participate as a DataMember
using a preconfigured X.509 certificate to protect the data content while serialized, thus allowing DataContracts
of the form:
[DataContract]
public class SecureDataContract
{
:
[DataMember]
public SerializableSecureString SecureContent { get; private set; }
}
The programmatic simplicity of this methodology was the chief motivator for this work. Other than binding the DataContract
(via constructor injection) to the correct X.509 certificate, developers have no other cryptographic concerns with secure data storage or movement; they simply interact with a Type in a normal manner. By disentangling the data storage problem from the data consumption problem, the scope of secure data handling concerns is limited to areas in the code where the sensitive data is extracted to clear text for the implementation of business problems. The scope limitation facilitates the development of best practices, data handling guidelines, and perhaps even automated checking for possible exposures.
The Basics
SerializableSecureString
wraps a private
member variable, Content
. The wrapper implements both IXmlSerializable
, to hook the serialization process, and IDisposable
, to housekeep Content
, as is best practice for SecureString
. A few helper methods allow consuming code access to the underlying Content
or its properties.
[Serializable]
public class SerializableSecureString : IXmlSerializable, IDisposable
{
:
SecureString Content = new SecureString();
:
#region Content helpers
public void Initialize(string clearText)
{
Content.Clear();
Append(clearText);
}
public void Append(string clearText)
{
if (clearText != null)
{
if (Content.IsReadOnly()) throw new ReadOnlyContentException();
foreach (char t in clearText)
{
Content.AppendChar(t);
}
}
}
public string Extract()
{
IntPtr bstr = Marshal.SecureStringToBSTR(Content);
string copiedText = Marshal.PtrToStringAuto(bstr);
Marshal.ZeroFreeBSTR(bstr);
return copiedText;
}
public void MakeReadOnly()
{
Content.MakeReadOnly();
}
public bool IsReadOnly()
{
return Content.IsReadOnly();
}
public SecureString CloneData()
{
return Content.Copy();
}
#endregion
:
}
Two other helpers provide access to the configured certificate store. These are useful in implementing DataContract
constructors that initialize the SerializableSecureString
s as shown later:
[Serializable]
public class SerializableSecureString : IXmlSerializable, IDisposable
{
:
public IEnumerable<X509Certificate2> Find(string thumbPrint)
{
return Find(thumbPrint, CertificateStore);
}
public static IEnumerable<X509Certificate2> Find(string thumbPrint, X509Store store)
{
store.Open(OpenFlags.ReadOnly);
IEnumerable<X509Certificate2> results =
store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false)
.Cast<X509Certificate2>();
store.Close();
return results;
}
:
}
IXmlSerializable
Implementation of IXmlSerializable
begins by defining the representational post-serialization schema.
<SecureStringEnvelope>
<EncryptionContext>
<ClearText />
<IsReadOnly>false</IsReadOnly>
<CertificateContext>
<ThumbPrint />
<Store />
<Location />
</CertificateContext>
</EncryptionContext>
</SecureStringEnvelope>"
The relevant inner elements of this schema are:
ClearText
, the value of Content
at the time of serialization IsReadOnly
, the read only state of Content
at time of serialization ThumbPrint
, the thumbprint of an X.509 certificate used to encrypt ClearText
during serialization Store
, the name of the certificate store in which the certificate specified by ThumbPrint
is installed Location
, the location of the Store
, either CurrentUser
or LocalMachine
These schema elements have related or backing properties in the wrapper which play vital roles during serialization.
public class SerializableSecureString : IXmlSerializable, IDisposable
{
:
{
:
#region IXmlSerializable
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
var doc = new XmlDocument();
reader.MoveToContent();
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element
&& reader.LocalName == _Envelope)
{
break;
}
}
doc.LoadXml(reader.ReadOuterXml());
Value = doc;
}
public void WriteXml(XmlWriter writer)
{
Value.Save(writer);
}
#endregion
}
}
XML Encryption
The W3C's recommendation on encrypting XML describes a process for the encryption of one or more elements in an XML document. The basic procedure replaces a fragment of the document with an encrypted equivalent on output and reverses the process on input. In .NET, this standard is implemented by the EncryptedXml
class (System.Security.Cryptography.Xml). While a variety of cryptographic mechanisms are supported, the most straight forward use of EncryptedXml
is provided when an X.509 certificate is used to specify the encryption processes. The wrapper's private Value
property accessors leverage EncryptedXml
to protect the ClearText
once it is extracted from the SecureString
.
private XmlDocument Value
{
get
{
var doc = new XmlDocument();
doc.LoadXml(SerializationXsi);
XmlNode elmt = doc.DocumentElement.FirstChild.FirstChild;
elmt.InnerText = Extract();
var xmlEnc = new EncryptedXml(doc);
EncryptedData encrData = xmlEnc.Encrypt(elmt as XmlElement, Certificate);
EncryptedXml.ReplaceElement(elmt as XmlElement, encrData, false);
LoadCertificateContext(doc);
return doc;
}
set
{
XmlDocument doc = value;
bool bReadOnly = UnloadCertificateContext(doc);
var encrXml = new EncryptedXml(doc);
encrXml.DecryptDocument();
Initialize(doc.DocumentElement.FirstChild.FirstChild.InnerText);
if (bReadOnly) Content.MakeReadOnly();
}
}
The get
accessor loads an instance of the serialization schema with the current ClearText
value of Content
, invokes EncryptedXml
to encrypt the ClearText
node, then populates the remainder of the schema from current member variables. The set
accessor reverses the process, starting with deserializing the current member variables, then decrypting ClearText
and reloading Content
. Content
’s IsReadOnly()
state is preserved. Note that the set
accessor throws a CertificateException
if the certificate specified in the incoming serialization instance is not installed in the specified location and certificate store. The current implementation demands that both sender and receiver use the same certificate, and that both sender and receiver have access to the certificate’s private key. A before and after picture of the encryption effects are shown below:
Before | After |
| |
Herein lies the problem with any field based encryption strategy: payload bloat, especially with X.509 certificates. The bloat is, however, offset by several factors that make the solution viable:
- The data is highly compressible on the wire.
- The programmatic implementation is relatively simple due in part to the deep support for certificates within the base class library.
DataContract
syntax is familiar and straight forward when working with the certificates - Experience indicates that there are few (one to two) of encrypted fields per
DataContract
- There is significant administrative ease in using X.509 certificates to convey cryptographic details in field deployments.
DataContract Recommendations
When implementing a DataContract
for a class which contains SerializableSecureString
, the following steps are recommended:
- Provide a
private
default constructor. A default constructor is required by the serializer, but does not have to be public
. - Make the
set
accessor of the SerializableSecureString
properties private
. - Implement one or more parameterized constructors that create the
SerializableSecureString
members with the appropriate certificate.
The sample below illustrates these points.
[DataContract]
public class SecureDataContract
{
private SecureDataContract()
{
}
public SecureDataContract(X509Certificate2 cert, X509Store store)
{
SecureContent = new SerializableSecureString(cert, store);
}
public SecureDataContract(SerializableSecureString secureString)
{
SecureContent = secureString;
}
public SecureDataContract(string thumbPrint, X509Store store)
{
X509Certificate2 cert = SerializableSecureString.Find(
thumbPrint, store).FirstOrDefault();
SecureContent = new SerializableSecureString(cert, store);
}
[DataMember]
public SerializableSecureString SecureContent { get; private set; }
}
}
Points Of Interest
Working with X.509 certificates in unit test environments is a best trying, especially when private keys are involved as here. To avoid the tedium, a certificate generator was developed to provide dynamically created X.509 certificates. The generator leverages the BouncyCastle crypto library to produce the inputs to create an X509Certificate2
instances for use in the unit tests. X509Certificate2
provides a convenient API for working with the public and private keys of a certificate, including persisting or destroying the private key’s backing store in the Cryptographic Service Provider. The generator is packaged as a separate assembly, X509Generators
.
namespace X509Generators
{
:
public static X509Certificate2 GenerateCertificate(StoreLocation location,
string subjectName,
SecureString password,
ExtendedKeyUsageEnum purpose =
ExtendedKeyUsageEnum.EmailProtection)
{
GeneratedCertificate bouncyCert = GenerateCertificate(subjectName, purpose);
var cert = new X509Certificate2(bouncyCert.Certificate.GetEncoded(), password,
X509KeyStorageFlags.Exportable);
:
return cert;
}
:
}
In the DEBUG configuration, SerializedSecureString
exposes a testing constructor that initializes itself from a dynamically generated certificate. The _autogenerated
flags the use of the dynamic generator.
public class SerializableSecureString : IXmlSerializable, IDisposable
{
:
:#if DEBUG
public SerializableSecureString(
ExtendedKeyUsageEnum purpose = ExtendedKeyUsageEnum.EmailProtection)
{
CertificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
Content = new SecureString();
Certificate = X509Generation.GenerateCertificate(CertificateStore.Location, GetType().Name,
new SecureString());
_autoGenerated = true;
ThumbPrint = Certificate.Thumbprint;
SetCertificateInStore(Certificate);
}#endif
:
}
#endif
The _autogenerated
flag is used in the IDisposable
implementation to detect a dynamic certificate and delete the backing private key when the certificate is Disposed
.
public SerializableSecureString(SerializableSecureString instance, bool bCopyData = false)
{
:
protected virtual void Dispose(bool isDisposing)
{
if (!_disposed)
{
if (isDisposing)
{
Content.Dispose();
if (Certificate != null && _autoGenerated)
{
if (Certificate.HasPrivateKey)
{
var pvKey = Certificate.PrivateKey as RSACryptoServiceProvider;
pvKey.PersistKeyInCsp = false;
pvKey.Clear();
}
CertificateStore.Open(OpenFlags.ReadWrite);
CertificateStore.Remove(Certificate);
CertificateStore.Close();
}
}
_disposed = true;
}
}
:
}
A build condition in the csproj causes the necessary reference to the X509Generators
project to be made. This has been hand edited into the csproj as shown:
<Choose>
<When Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<ItemGroup>
<ProjectReference Include="X509Generators\X509Generators.csproj">
<Project>{483bd6f8-b1f8-402d-aca4-9ea13a173baf}</Project>
<Name>X509Generators</Name>
</ProjectReference>
</ItemGroup>
</When>
</Choose>">
Thus the X509Generator
assembly is not required for the Release build.
Using the Code
The code is delivered as a single, VS2012 solution with three projects containing:
SerializableSecureString
, the implementation X509Generators
, the dynamic certificate generators SerialzableSecureStringTests
, xUnit tests to exercise code
Code can be exercised by running the unit tests following a successful build.
History
- 19th October, 2013: Initial version