using System; using System.IO; using System.Security.Cryptography; using System.Text; using System.Xml; namespace Framework.Support { /// Signs XML document with a hash code for tamper resistance public static class XmlSigner { /// Name of the hash attribute that will be added to the root element public static readonly string HashName = "Hash"; /// Signs an xml document in a memory stream in-place /// /// Memory stream containing the XML document to be signed /// public static void Sign(MemoryStream memoryStream) { var document = new XmlDocument(); document.PreserveWhitespace = true; document.Load(memoryStream); XmlElement rootElement = document.DocumentElement; // Set the hash code to all zeroes (that's the state we're hashing against) rootElement.SetAttribute(HashName, new string('0', 40)); memoryStream.Seek(0, SeekOrigin.Begin); document.Save(memoryStream); // Hash it HashAlgorithm sha1Hasher = SHA1.Create(); memoryStream.Seek(0, SeekOrigin.Begin); byte[] sha1Hash1 = sha1Hasher.ComputeHash(memoryStream); rootElement.SetAttribute(HashName, hexStringFromBytes(sha1Hash1)); // Write the document again with the actual hash memoryStream.Seek(0, SeekOrigin.Begin); document.Save(memoryStream); } /// Signs an xml document /// Stream containing the XML document to be signed /// /// Loads the entire stream into memory, does the signing operations and writes /// it back into the original stream. /// public static void Sign(Stream stream) { using(var modifiedStream = makeMemoryCopy(stream)) { Sign(modifiedStream); // Copy the modified stream back to the file stream (or whatever it is) modifiedStream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Current); copyStream(modifiedStream, stream); } } /// Verifies the hash code of an XML document /// Stream containing the XML document that will be verified /// True if the document's hash code matched its contents public static bool Verify(MemoryStream memoryStream) { var document = new XmlDocument(); document.PreserveWhitespace = true; document.Load(memoryStream); XmlElement rootElement = document.DocumentElement; // Obtain the hash code currently present in the document string actualHash = rootElement.GetAttribute(HashName); if(string.IsNullOrEmpty(actualHash)) { return false; // No hash present, document did not pass verification } // Set the hash code to all zeroes (that's the state we're hashing against) rootElement.SetAttribute(HashName, new string('0', 40)); memoryStream.Seek(0, SeekOrigin.Begin); document.Save(memoryStream); // Hash it HashAlgorithm sha1Hasher = SHA1.Create(); memoryStream.Seek(0, SeekOrigin.Begin); byte[] sha1Hash1 = sha1Hasher.ComputeHash(memoryStream); string expectedHash = hexStringFromBytes(sha1Hash1); // If the hash matches the one we read at the beginning, the document passes bool ignoreCase = true; return (string.Compare(actualHash, expectedHash, ignoreCase) == 0); } /// Verifies the hash code of an XML document /// Stream containing the XML document that will be verified /// True if the document's hash code matched its contents public static bool Verify(Stream stream) { using(var copiedStream = makeMemoryCopy(stream)) { return Verify(copiedStream); } } /// Turnes an array of bytes into a hexadecimal string /// Bytes that will be turned into a hexadecimal string /// The hexadecimal string of the provided byte array private static string hexStringFromBytes(byte[] bytes) { var hexStringBuilder = new StringBuilder(bytes.Length * 2); for(int index = 0; index < bytes.Length; ++index) { hexStringBuilder.Append(bytes[index].ToString("X2")); } return hexStringBuilder.ToString(); } /// Creates an in-memory copy of the specified stream /// Stream of which an in-memory copy will be created /// The in-memory copy of the provided stream private static MemoryStream makeMemoryCopy(Stream stream) { int length = (int)stream.Length; var copy = new MemoryStream(length); try { copyStream(stream, copy); copy.Seek(0, SeekOrigin.Begin); } catch(Exception) { copy.Dispose(); throw; } return copy; } /// Copies one stream to another /// Stream that will be copied /// Stream into which the source stream will be copied private static void copyStream(Stream source, Stream target) { long oldPosition = source.Position; source.Seek(0, SeekOrigin.Begin); try { int length = Math.Min((int)source.Length, 65536); var buffer = new byte[length]; for(;;) { int readByteCount = source.Read(buffer, 0, length); if(readByteCount == 0) { return; } target.Write(buffer, 0, readByteCount); } } finally { source.Position = oldPosition; } } } } // namespace Framework.Support