Applying a digital signature to data involves computing a hash code for the data and then encrypting the hash code with an asymmetric algorithm and a private key. This results in a small amount of ciphertext. Typically, the number of bits of ciphertext is equal to the number of bits selected for the encryption key. Because data you might wish to sign in the real world can be very large, encrypting the hash code of the data rather than the data itself is more efficient, though it provides no confidentiality protection over the data because the signed data is not encrypted itself.
When you receive signed data, it must contain at least a copy of the original data and the ciphertext version of the hash code of that data. If you know in advance what key to use to decrypt the ciphertext and you know what hash algorithm was used on the digitally signed data, then you could verify the signature without difficulty. In the real world, we anticipate not knowing such things in advance. It's possible that we won't even know in advance what the data format is of the original data (often referred to in crypto documentation as the message), which could cause a real problem as we attempt to figure out where the plaintext data of the message stops and where the ciphertext data of its digital signature begins. If we were to create digital signatures in their most basic form as just described, we might attach ciphertext of the data's encrypted hash code to the beginning or the end of the plaintext message data so that it becomes a part of the message. There are times when this approach works well, but it is also possible that we'll need to store the signature as a separate detached data block so that we don't corrupt the original data format.
Alternatively, we could design a digital signature envelope standard and write up complex contingency rules that tell us how to apply the envelope standard in various scenarios where a simple attached or detached envelope structure would corrupt the data being signed or create some other failure mode. RSA's Public Key Cryptography Standards (PKCS) define just such envelope structures and their encoding ruleswhile leaving room to adapt and extend algorithms, identifiers, language and character sets, and so forth, so that an organization's Public Key Infrastructure (PKI) will function for everyone, everywhere, at all times, even across organizational boundaries. The PKCS standards are a big part of the reason that PKI is no fun (see http://www.rsasecurity.com/rsalabs/pkcs).
Since XML offers improvements over fixed data structures, such as the ability
to add arbitrary branches and embeddings without adversely impacting any properly
written XML application, naturally we'll want a consistent way to attach digital
signatures to XML data. The W3C XMLDSIG specification serves this purpose (see
http://www.w3.org/Signature). Microsoft's
.NET Framework provides a class called SignedXML
in the System.Security.Cryptography.Xml
namespace that implements XMLDSIG for you. The SignedXML
class can, and
should, be used as a general purpose XML signature creation and verification
mechanism because it attempts to conform to the XMLDSIG standard. Compatibility
with other XMLDSIG-based systems is, therefore, supposed to be automatic, although
differences in encoding and formatting, including things like how whitespace
is handled (or ignored) by different XMLDSIG implementations, still causes incompatibilities
in practice.
Using XMLDSIG through the SignedXML
class gives you a standards-based
solution for attaching and later parsing an XML-encoded digital signature so
that it can be verified or removed from the signed data. Detached signature
envelope files are also easy to create with SignedXML
so that the signature
data can be stored apart from the data that was signed. However, the real power
of the XML-based signature is its ability to become a part of the data without
causing problems for any software already in existence that expects XML data
without attached signatures. The XMLDSIG standard defines the way in which signature
verification public key and cryptographic-algorithm information is stored, along
with the ciphertext of the signed data's encrypted hash code.
Creating XMLDSIG Signatures
To create and attach a signature to an XML data block using the SignedXML
class requires only a few steps. First, populate a new XmlDocument
with
the XML to be signed. Next, create an instance of the SignedXml
class
and create or load an RSA key pair using the RSA class. Set the SigningKey
property of the signedXML
object and write the XmlDocument
to
a DataObject
. Finally, add the object and an intradocument URI reference
for it to the SignedXML
object, along with an instance of the KeyInfo
class containing the signature key pair to the SignedXML
object, and
you're ready to compute and display the XMLDSIG signature wrapped around the
plaintext message. Listing One shows these steps:
Listing One
XmlDocument xmldoc = new XmlDocument(); xmldoc.PreserveWhitespace = true; XmlNode msgnode = xmldoc.CreateNode(XmlNodeType.Element,"Plaintext","msg"); msgnode.InnerText = "This is the plaintext message."; xmldoc.AppendChild(msgnode); System.Security.Cryptography.Xml.SignedXml XMLsig = new SignedXml(); RSA keypair = RSA.Create(); XMLsig.SigningKey = keypair; System.Security.Cryptography.Xml.DataObject message = new DataObject("Message","","",xmldoc.DocumentElement); XMLsig.AddObject(message); System.Security.Cryptography.Xml.Reference msgURI = new Reference(); msgURI.Uri = "#Message"; XMLsig.AddReference(msgURI); System.Security.Cryptography.Xml.KeyInfo ki = new KeyInfo(); ki.AddClause(new RSAKeyValue(keypair)); XMLsig.KeyInfo = ki; XMLsig.ComputeSignature(); Console.Out.WriteLine(XMLsig.GetXml().OuterXml);