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