/* Copyright (c) Citrix Systems Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using XenCenterLib.Archive; using XenCenterLib.Compression; namespace XenOvf { // Provides the properties of a digest for a file in the manifest of an OVF appliance. public class FileDigest { // Construct from a line in the manifest. public FileDigest(string line) { var fields = line.Split(new char[] { '(', ')', '=' }); // Expect the first field to be the security algorithm used to compute the hash of the file. _AlgorithmName = fields[0].Trim(); // Expect the second field to be the name of the file. Match nameMatch = new Regex(@"[\(](.*)[\)][=]").Match(line); _Name = nameMatch.Success ? nameMatch.Groups[1].Value.Trim() : fields[1].Trim(); // Expect the last field to be the digest of the file. _DigestAsString = fields[fields.Length - 1].Trim(); } // Name of the file with the digest. public string Name { get { return _Name; } } private string _Name; // Name of the algorithm to compute the digest. // It must be recognized by HashAlgorithm.Create(). public string AlgorithmName { get { return _AlgorithmName; } } private string _AlgorithmName; // Algorithm to compute the digest. public HashAlgorithm Algorithm { get { var hashAlgorithm = HashAlgorithm.Create(_AlgorithmName); // Validate the algorithm. if (hashAlgorithm == null) throw new ArgumentException(string.Format(Messages.SECURITY_NOT_SUPPORTED, _AlgorithmName)); return hashAlgorithm; } } // Digest as a binary array. public byte[] Digest { get { return ToArray(_DigestAsString); } } private string _DigestAsString; protected string DigestAsString { get { return _DigestAsString; } } public bool WasVerified { get { return _WasVerified; } } private bool _WasVerified = false; // Convert a hex string to a binary array. public static byte[] ToArray(string HexString) { if ((HexString.Length % 2) > 0) { // The length is odd. // Pad the hex string on the left with a space to interpret as a leading zero. HexString = HexString.PadLeft(HexString.Length + 1); } var array = new byte[HexString.Length / 2]; try { for (int i = 0; i < HexString.Length; i += 2) { array[i / 2] = byte.Parse(HexString.Substring(i, 2), System.Globalization.NumberStyles.HexNumber); } } catch { throw new Exception(HexString + " contains an invalid hexadecimal number."); } return array; } // Verify the digest of the file content accessible from the given stream by comparing it to the stored digest. public void Verify(Stream stream) { var computedDigest = Algorithm.ComputeHash(stream); if (!System.Linq.Enumerable.SequenceEqual(Digest, computedDigest)) { throw new Exception(string.Format(Messages.SECURITY_SIGNATURE_FAILED, Name)); } _WasVerified = true; } // Verify the digest, when used as a signature, of the file content accessible from the given stream. public void Verify(Stream stream, RSACryptoServiceProvider key) { var computedDigest = Algorithm.ComputeHash(stream); if (!key.VerifyHash(computedDigest, CryptoConfig.MapNameToOID(_AlgorithmName), Digest)) { throw new Exception(string.Format(Messages.SECURITY_SIGNATURE_FAILED, Name)); } } } // OVF Appliance in the form of a folder. internal class FolderPackage : Package { public FolderPackage(string path) { PackageSourceFile = path; _Folder = path; var extension = Path.GetExtension(path); if (String.Compare(extension, Properties.Settings.Default.ovfFileExtension, true) == 0) _Folder = Path.GetDirectoryName(path); if (_Folder == null) { // Handle a package in the root or current folder. _Folder = ""; } } private string _Folder; public override string DescriptorFileName { get { return Path.GetFileName(PackageSourceFile); } } public override bool HasFile(string fileName) { try { using (var stream = File.OpenRead(Path.Combine(_Folder, fileName))) { return true; } } catch { return false; } } protected override byte[] ReadAllBytes(string fileName) { return File.ReadAllBytes(Path.Combine(_Folder, fileName)); } protected override string ReadAllText(string fileName) { return File.ReadAllText(Path.Combine(_Folder, fileName)); } public override void VerifyManifest() { // Verify the presence of a manifest. var manifest = Manifest; // For a folder package, it is efficient to iterate by the order of files in the manifest. foreach (FileDigest fileDigest in manifest) { using (var stream = File.OpenRead(Path.Combine(_Folder, fileDigest.Name))) { // Verify each manifest item. fileDigest.Verify(stream); } } } } // Exposes a stream of entries within a tar for TarPackage. internal class TarFileStream : IDisposable /*: Stream */ { // Open a file within the tar. public TarFileStream(string tarPath) { _tarStream = File.OpenRead(tarPath); var extension = Path.GetExtension(tarPath); // Check for a compressed archive. if (String.Compare(extension, ".gz", true) == 0) { _compressionStream = CompressionFactory.Reader(CompressionFactory.Type.Gz, _tarStream); _tarInputStream = ArchiveFactory.Reader(ArchiveFactory.Type.Tar, _compressionStream); } else if (String.Compare(extension, ".bz2", true) == 0) { _compressionStream = CompressionFactory.Reader(CompressionFactory.Type.Bz2, _tarStream); _tarInputStream = ArchiveFactory.Reader(ArchiveFactory.Type.Tar, _compressionStream); } else { _tarInputStream = ArchiveFactory.Reader(ArchiveFactory.Type.Tar, _tarStream); } } // An improvement would be to construct an enumerator for use with foreach. public bool FindNextFile(string filePath) { // Emulate the simple pattern matching of Directory.GetFiles(string path, string pattern) var regexPattern = new System.Text.StringBuilder(filePath); // Escape any directory separators first. regexPattern = regexPattern.Replace("\\", "\\\\"); // Escape any extension separator second. regexPattern = regexPattern.Replace(".", "\\."); // Translate any wildcards to their RegEx equivalents. regexPattern = regexPattern.Replace("*", ".*"); regexPattern = regexPattern.Replace("?", ".?"); var regexPatternAsString = regexPattern.ToString(); while (MoveNext()) { if (System.Text.RegularExpressions.Regex.IsMatch(Current, regexPatternAsString, System.Text.RegularExpressions.RegexOptions.IgnoreCase)) return true; } return false; } public byte[] ReadAllBytes() { MemoryStream ms = new MemoryStream(); _tarInputStream.ExtractCurrentFile(ms); if( ms.Length < 1 ) return null; return ms.ToArray(); } // Name of current file in the archive. public string Current { get { return _tarInputStream.CurrentFileName(); } } public bool MoveNext() { return _tarInputStream.HasNext(); } protected void Dispose(Boolean disposing) { if (disposing) { // TarInputStream does not implement Dispose(boolean). _tarInputStream.Dispose(); if (_compressionStream != null) _compressionStream.Dispose(); _tarStream.Dispose(); } } private Stream _tarStream; private Stream _compressionStream; private ArchiveIterator _tarInputStream; public void Close() { Dispose(true); } public void Dispose() { Close(); } } // OVF Appliance in the form of a tape archive (tar). internal class TarPackage : Package { public TarPackage(string path) { PackageSourceFile = path; _DirectoryCache = new Dictionary(); } private Dictionary _DirectoryCache; public override string DescriptorFileName { get { if (_DescriptorFileName != null) return _DescriptorFileName; Load(); if (_DescriptorFileName != null) { return _DescriptorFileName; } // Last resort is to search the whole archive. var ovfFilePattern = "*" + Properties.Settings.Default.ovfFileExtension; using (var stream = OpenRead(ovfFilePattern)) { _DescriptorFileName = stream.Current; } return _DescriptorFileName; } } private string _DescriptorFileName; // Open a file within the tar. protected TarFileStream OpenRead(string fileName) { var stream = new TarFileStream(PackageSourceFile); if (!stream.FindNextFile(fileName)) { stream.Close(); throw new FileNotFoundException(fileName); } return stream; } public override bool HasFile(string fileName) { try { if (_DirectoryCache.ContainsKey(fileName)) return true; using (var stream = OpenRead(fileName)) { return true; } } catch { return false; } } // Wrap calls to TarFileStream.MoveNext() to cache file names for HasFile(). private bool MoveNext(TarFileStream stream) { var haveFile = stream.MoveNext(); if (haveFile) _DirectoryCache[stream.Current] = null; return haveFile; } protected override byte[] ReadAllBytes(string fileName) { byte[] buffer; if ((_DirectoryCache.TryGetValue(fileName, out buffer)) && (buffer != null)) { return buffer; } using (var stream = OpenRead(fileName)) { return (_DirectoryCache[stream.Current] = stream.ReadAllBytes()); } } protected override string ReadAllText(string fileName) { var buffer = ReadAllBytes(fileName); using (var stream = new MemoryStream(buffer)) using (var reader = new StreamReader(stream)) { return reader.ReadToEnd(); } } private void Load() { using (var stream = new TarFileStream(PackageSourceFile)) { while (MoveNext(stream)) { var extension = Path.GetExtension(stream.Current); if ((_DescriptorFileName == null) && (String.Compare(extension, Properties.Settings.Default.ovfFileExtension, true) == 0)) { // Use the first file name with an ovf extension. _DescriptorFileName = stream.Current; } else if (String.Compare(extension, Properties.Settings.Default.manifestFileExtension, true) == 0) { // Skip a manifest file that does not have the base name as the descriptor. if ((_DescriptorFileName != null) && (String.Compare(stream.Current, ManifestFileName, true) != 0)) continue; } else if (String.Compare(extension, Properties.Settings.Default.certificateFileExtension, true) == 0) { // Skip a certificate file that does not have the base name as the descriptor. if ((_DescriptorFileName != null) && (String.Compare(stream.Current, CertificateFileName, true) != 0)) continue; } else { // Stop loading because descriptor, manifest, and certificate files must precede all other files. break; } // Load this file. _DirectoryCache[stream.Current] = stream.ReadAllBytes(); } } } public override void VerifyManifest() { // Verify the presence of a manifest. var manifest = Manifest; int verifiedCount = 0; using (var stream = new TarFileStream(PackageSourceFile)) { // For a tar package, it is more efficient to iterate the files in the order within the archive. while (MoveNext(stream)) { // Find the manifest entry for the current tar entry. FileDigest fileDigest = manifest.Find( delegate(FileDigest aFileDigest) { return (String.Compare(aFileDigest.Name, stream.Current, true) == 0); }); if (fileDigest == null) continue; // The manifest entry was found. // Verify its digest. fileDigest.Verify(new MemoryStream(stream.ReadAllBytes())); verifiedCount++; } foreach (var fileDigest in manifest) { if (!fileDigest.WasVerified) { // A manifest entry was missing. log.ErrorFormat(string.Format(Messages.FILE_MISSING, fileDigest.Name)); } } if (verifiedCount != manifest.Count) { // A manifest entry was missing. throw new Exception(Messages.SECURITY_FILE_MISSING); } } } } // Abstract class to access any time of package. public abstract class Package { protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); // Package factory. public static Package Create(string path) { var extension = Path.GetExtension(path); if (String.Compare(extension, Properties.Settings.Default.ovfFileExtension, true) == 0) { return new FolderPackage(path); } // Assume it is an archive. return new TarPackage(path); } // Hide the constructor because only the factory can create a package. protected Package() { } #region Properties // DescriptorFileName is a special property. // Access to the rest of the package depends on it. // Abstract because the most efficient method to get it depends on the package type. public abstract string DescriptorFileName { get; } // According to the OVF specification, name of the *package* is base name of the descriptor file. public string Name { get { return Path.GetFileNameWithoutExtension(DescriptorFileName); } } // According to the OVF specification, base name of the manifest file must be the same as the descriptor file. public string ManifestFileName { get { return (Name + Properties.Settings.Default.manifestFileExtension); } } // According to the OVF specification, base name of the certificate file must be the same as the descriptor file. public string CertificateFileName { get { return (Name + Properties.Settings.Default.certificateFileExtension); } } // Contents of the OVF file. public virtual string Descriptor { get { if (_Descriptor != null) return _Descriptor; return (_Descriptor = ReadAllText(DescriptorFileName)); } } // Cache this property because it is expensive to get. private string _Descriptor; public List Manifest { get { var manifest = new List(); using (var stream = new MemoryStream(RawManifest)) using (StreamReader reader = new StreamReader(stream)) { while (!reader.EndOfStream) { manifest.Add(new FileDigest(reader.ReadLine())); } } return manifest; } } protected byte[] RawManifest { get { if (_RawManifest != null) return _RawManifest; return (_RawManifest = ReadAllBytes(ManifestFileName)); } } private byte[] _RawManifest; public X509Certificate2 Certificate { get { return (new X509Certificate2(RawCertificate)); } } protected byte[] RawCertificate { get { if (_RawCertificate != null) return _RawCertificate; return (_RawCertificate = ReadAllBytes(CertificateFileName)); } } private byte[] _RawCertificate; public string PackageSourceFile { get; protected set; } #endregion public virtual bool HasManifest() { return ((_RawManifest != null) || (HasFile(ManifestFileName))); } public virtual bool HasSignature() { return ((_RawCertificate != null) || (HasFile(CertificateFileName))); } public virtual void VerifySignature() { // Verify the certificate used to sign the package. var certificate = Certificate; if (!certificate.Verify()) { throw new Exception(Messages.CERTIFICATE_IS_INVALID); } // Get the package signature from the certificate file. FileDigest fileDigest = null; using (Stream stream = new MemoryStream(RawCertificate)) using (StreamReader reader = new StreamReader(stream)) { // The package signature is the digest of the file identified on the first line in the certificate file. fileDigest = new FileDigest(reader.ReadLine()); } // Compare the stored signature to the computed signature. // Do this independently to minimize the number of files opened concurrently. using (Stream stream = new MemoryStream(RawManifest)) { // Verify the signature using the public key. fileDigest.Verify(stream, (RSACryptoServiceProvider)certificate.PublicKey.Key); } } #region Package methods to specialize public abstract bool HasFile(string name); protected abstract byte[] ReadAllBytes(string fileName); protected abstract string ReadAllText(string fileName); // Abstract because the file enumerator method varies by package type. public abstract void VerifyManifest(); #endregion } }