CA-349836: Reworked handling of manifest and signature for appliance import/export.

- Moved the code to the ExportApplianceAction so it's easier to modify the
  action's description.
- Fixed issue where the user's option for adding signature/manifest to a plain
  OVF package was ignored.

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
This commit is contained in:
Konstantina Chremmou 2020-12-05 23:52:24 +00:00
parent 28c0f1d62a
commit e325548a75
10 changed files with 167 additions and 275 deletions

View File

@ -33,6 +33,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using XenAdmin.Core;
using XenAdmin.Network;
@ -75,6 +76,8 @@ namespace XenAdmin.Actions.OvfActions
m_eulas = eulas;
m_signAppliance = signAppliance;
m_createManifest = createManifest;
if (m_signAppliance && m_certificate == null)
throw new ArgumentNullException(nameof(m_certificate));
m_certificate = certificate;
m_encryptFiles = encryptFiles;
m_encryptPassword = encryptPassword;
@ -134,7 +137,6 @@ namespace XenAdmin.Actions.OvfActions
CheckForCancellation();
var ovfPath = Path.Combine(appFolder, appFile);
Description = String.Format(Messages.CREATING_FILE, appFile);
OVF.SaveAs(env, ovfPath);
PercentComplete = 85;
@ -142,12 +144,12 @@ namespace XenAdmin.Actions.OvfActions
if (m_createOVA)
{
log.Info($"Archiving OVF package {m_applianceFileName} into OVA");
Description = String.Format(Messages.CREATING_FILE, String.Format("{0}.ova", m_applianceFileName));
ManifestAndSign(appFolder, appFile);
ManifestAndSign(env, appFolder, appFile);
PercentComplete = 90;
log.Info($"Archiving OVF package {m_applianceFileName} into OVA");
Description = string.Format(Messages.CREATING_OVA_FILE, string.Format("{0}.ova", m_applianceFileName));
try
{
OVF.ConvertOVFtoOVA(env, ovfPath, () => Cancelling, m_compressOVFfiles);
@ -172,25 +174,110 @@ namespace XenAdmin.Actions.OvfActions
}
PercentComplete = 95;
ManifestAndSign(appFolder, appFile);
ManifestAndSign(env, appFolder, appFile);
}
else
{
ManifestAndSign(env, appFolder, appFile);
}
PercentComplete = 100;
Description = Messages.COMPLETED;
}
private void ManifestAndSign(string appFolder, string appFile)
private void ManifestAndSign(EnvelopeType ovfEnv, string appFolder, string appFile)
{
if (m_signAppliance || m_createManifest)
Manifest(ovfEnv, appFolder, appFile);
if (m_signAppliance)
{
Description = Messages.SIGNING_APPLIANCE;
OVF.Sign(m_certificate, appFolder, appFile);
}
else if (m_createManifest)
Sign(m_certificate, appFolder, appFile);
}
private void Manifest(EnvelopeType ovfEnv, string pathToOvf, string ovfFileName)
{
var manifestFiles = new List<string> {ovfFileName};
var files = ovfEnv?.References?.File;
if (files != null)
manifestFiles.AddRange(files.Select(f => f.href).ToList());
var fileDigests = new List<FileDigest>();
foreach (var mf in manifestFiles)
{
Description = Messages.CREATING_MANIFEST;
OVF.Manifest(appFolder, appFile);
CheckForCancellation();
var mfPath = Path.Combine(pathToOvf, mf);
if (!File.Exists(mfPath))
continue;
Description = string.Format(Messages.CREATING_MANIFEST_CHECKSUM, mf);
log.Info($"Calculating checksum for file {mf}");
using (FileStream stream = new FileStream(mfPath, FileMode.Open, FileAccess.Read))
using (var hasher = HashAlgorithm.Create(FileDigest.DEFAULT_HASHING_ALGORITHM))
{
var hash = hasher?.ComputeHash(stream);
fileDigests.Add(new FileDigest(Path.GetFileName(mf), hash));
}
}
CheckForCancellation();
Description = Messages.CREATING_MANIFEST;
string manifest = Path.Combine(pathToOvf, $"{Path.GetFileNameWithoutExtension(ovfFileName)}{Package.MANIFEST_EXT}");
using (var stream = new FileStream(manifest, FileMode.Create, FileAccess.Write))
using (var sw = new StreamWriter(stream))
{
foreach (var fileDigest in fileDigests)
sw.WriteLine(fileDigest.ToManifestLine());
sw.Flush();
}
log.Info($"Created manifest for package {ovfFileName}");
}
private void Sign(X509Certificate2 certificate, string pathToOvf, string ovfFileName)
{
Description = Messages.SIGNING_APPLIANCE;
var packageName = Path.GetFileNameWithoutExtension(ovfFileName);
string manifestFileName = packageName + Package.MANIFEST_EXT;
string manifestPath = Path.Combine(pathToOvf, manifestFileName);
CheckForCancellation();
byte[] signedHash = null;
using (FileStream stream = new FileStream(manifestPath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var hasher = HashAlgorithm.Create(FileDigest.DEFAULT_HASHING_ALGORITHM))
{
var hash = hasher?.ComputeHash(stream);
if (hash != null)
{
using (var csp = (RSACryptoServiceProvider)certificate.PrivateKey)
signedHash = csp.SignHash(hash, CryptoConfig.MapNameToOID(FileDigest.DEFAULT_HASHING_ALGORITHM));
}
}
var fileDigest = new FileDigest(manifestFileName, signedHash);
string signatureFileName = packageName + Package.CERTIFICATE_EXT;
string signaturePath = Path.Combine(pathToOvf, signatureFileName);
using (FileStream stream = new FileStream(signaturePath, FileMode.Create, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine(fileDigest.ToManifestLine());
// Export the certificate encoded in Base64 using DER
string b64Cert = Convert.ToBase64String(certificate.Export(X509ContentType.SerializedCert));
writer.WriteLine("-----BEGIN CERTIFICATE-----");
writer.WriteLine(b64Cert);
writer.WriteLine("-----END CERTIFICATE-----");
writer.Flush();
}
log.Info($"Digitally signed package {ovfFileName}");
}
protected override void CleanOnError()

View File

@ -78,36 +78,40 @@ namespace XenAdmin.Actions.OvfActions
protected override void RunCore()
{
// The appliance has a signature and the user asked to verify it.
if (m_verifySignature)
{
Description = Messages.VERIFYING_SIGNATURE;
try
{
// The appliance is known to have a signature and the user asked to verify it.
m_package.VerifySignature();
// If the appliance has a signature, then it has a manifest.
// Always verify the manifest after verifying the signature.
m_package.VerifyManifest();
log.Info($"Verified signature for package {m_package.Name}");
}
catch (Exception e)
{
log.Error($"Signature verification failed for package {m_package.Name}", e);
throw new Exception(String.Format(Messages.VERIFYING_SIGNATURE_ERROR, e.Message));
}
}
else if (m_verifyManifest)
// The appliance has
// - a signature (in which case it also has a manifest that should be verified AFTER the signature); or
// - a manifest without a signature
// and the user asked to verify it
if (m_verifySignature || m_verifyManifest)
{
Description = Messages.VERIFYING_MANIFEST;
try
{
// The appliance had a manifest without a signature and the user asked to verify it.
// VerifyManifest() throws an exception when verification fails for any reason.
m_package.VerifyManifest();
log.Info($"Verified manifest for package {m_package.Name}");
}
catch (Exception e)
{
log.Error($"Manifest verification failed for package {m_package.Name}", e);
throw new Exception(String.Format(Messages.VERIFYING_MANIFEST_ERROR, e.Message));
}
}

View File

@ -11080,15 +11080,6 @@ namespace XenAdmin {
}
}
/// <summary>
/// Looks up a localized string similar to Archiving appliance files into single package file &apos;{0}&apos;....
/// </summary>
public static string CREATING_FILE {
get {
return ResourceManager.GetString("CREATING_FILE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Creating manifest file....
/// </summary>
@ -11098,6 +11089,15 @@ namespace XenAdmin {
}
}
/// <summary>
/// Looks up a localized string similar to Calculating checksum for file &apos;{0}&apos;....
/// </summary>
public static string CREATING_MANIFEST_CHECKSUM {
get {
return ResourceManager.GetString("CREATING_MANIFEST_CHECKSUM", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Creating new pool &apos;{0}&apos; with master &apos;{1}&apos;.
/// </summary>
@ -11125,6 +11125,15 @@ namespace XenAdmin {
}
}
/// <summary>
/// Looks up a localized string similar to Archiving appliance files into single package file &apos;{0}&apos;....
/// </summary>
public static string CREATING_OVA_FILE {
get {
return ResourceManager.GetString("CREATING_OVA_FILE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Creating vApp &apos;{0}&apos;....
/// </summary>

View File

@ -3955,12 +3955,12 @@ For optimal performance and reliability during VM migration, ensure that the net
<data name="CREATING_DISKS" xml:space="preserve">
<value>Creating disks</value>
</data>
<data name="CREATING_FILE" xml:space="preserve">
<value>Archiving appliance files into single package file '{0}'...</value>
</data>
<data name="CREATING_MANIFEST" xml:space="preserve">
<value>Creating manifest file...</value>
</data>
<data name="CREATING_MANIFEST_CHECKSUM" xml:space="preserve">
<value>Calculating checksum for file '{0}'...</value>
</data>
<data name="CREATING_NAMED_POOL_WITH_MASTER" xml:space="preserve">
<value>Creating new pool '{0}' with master '{1}'</value>
</data>
@ -3970,6 +3970,9 @@ For optimal performance and reliability during VM migration, ensure that the net
<data name="CREATING_NEW_FOLDERS" xml:space="preserve">
<value>Creating new folders...</value>
</data>
<data name="CREATING_OVA_FILE" xml:space="preserve">
<value>Archiving appliance files into single package file '{0}'...</value>
</data>
<data name="CREATING_VMSS" xml:space="preserve">
<value>Creating snapshot schedule '{0}'...</value>
</data>

View File

@ -49,6 +49,8 @@ namespace XenOvf
/// </summary>
public class FileDigest
{
public const string DEFAULT_HASHING_ALGORITHM = "SHA256";
/// <summary>
/// Creates a new instance from a line in the manifest.
/// </summary>
@ -69,6 +71,14 @@ namespace XenOvf
Digest = ToArray(DigestAsString);
}
public FileDigest(string fileName, byte[] digest, string hashingAlgorithm = DEFAULT_HASHING_ALGORITHM)
{
AlgorithmName = hashingAlgorithm;
Name = fileName;
Digest = digest;
DigestAsString = Digest == null ? "" : string.Join("", Digest.Select(b => $"{b:x2}"));
}
/// <summary>
/// Name of the file with the digest.
/// </summary>
@ -83,6 +93,11 @@ namespace XenOvf
public byte[] Digest{ get; }
public string ToManifestLine()
{
return $"{AlgorithmName}({Name})= {DigestAsString}";
}
/// <summary>
/// Convert a hex string to a binary array.
/// </summary>
@ -271,12 +286,12 @@ namespace XenOvf
{
_DescriptorFileName = _archiveIterator.CurrentFileName();
}
else if (string.Compare(extension, Properties.Settings.Default.manifestFileExtension, true) == 0)
else if (string.Compare(extension, MANIFEST_EXT, true) == 0)
{
if (_DescriptorFileName == null || string.Compare(_archiveIterator.CurrentFileName(), ManifestFileName, true) != 0)
continue;
}
else if (string.Compare(extension, Properties.Settings.Default.certificateFileExtension, true) == 0)
else if (string.Compare(extension, CERTIFICATE_EXT, true) == 0)
{
if (_DescriptorFileName == null || string.Compare(_archiveIterator.CurrentFileName(), CertificateFileName, true) != 0)
continue;
@ -357,8 +372,8 @@ namespace XenOvf
while (_archiveIterator.HasNext())
{
var extension = Path.GetExtension(_archiveIterator.CurrentFileName());
if (string.Compare(extension, Properties.Settings.Default.manifestFileExtension, true) == 0 ||
string.Compare(extension, Properties.Settings.Default.certificateFileExtension, true) == 0)
if (string.Compare(extension, MANIFEST_EXT, true) == 0 ||
string.Compare(extension, CERTIFICATE_EXT, true) == 0)
continue;
var fileDigest = manifest.Find(fd => string.Compare(fd.Name, _archiveIterator.CurrentFileName(), true) == 0);
@ -417,6 +432,9 @@ namespace XenOvf
{
protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public const string MANIFEST_EXT = ".mf";
public const string CERTIFICATE_EXT = ".cert";
// Cache these properties because they are expensive to get
private string _descriptorXml;
private byte[] _RawManifest;
@ -449,12 +467,12 @@ namespace XenOvf
/// <summary>
/// According to the OVF specification, base name of the manifest file must be the same as the descriptor file.
/// </summary>
protected string ManifestFileName => Name + Properties.Settings.Default.manifestFileExtension;
protected string ManifestFileName => Name + MANIFEST_EXT;
/// <summary>
/// According to the OVF specification, base name of the certificate file must be the same as the descriptor file.
/// </summary>
protected string CertificateFileName => Name + Properties.Settings.Default.certificateFileExtension;
protected string CertificateFileName => Name + CERTIFICATE_EXT;
/// <summary>
/// Contents of the OVF file.
@ -551,6 +569,7 @@ namespace XenOvf
return RawCertificate != null;
}
/// <exception cref="Exception">Thrown when verification fails for any reason</exception>>
public void VerifySignature()
{
using (var certificate = new X509Certificate2(RawCertificate))
@ -594,6 +613,7 @@ namespace XenOvf
protected abstract string ReadAllText(string fileName);
/// <exception cref="Exception">Thrown when verification fails for any reason</exception>>
public abstract void VerifyManifest();
public abstract bool HasFile(string fileName);

View File

@ -12,7 +12,7 @@ namespace XenOvf.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.5.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@ -330,33 +330,6 @@ namespace XenOvf.Properties {
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute(".mf")]
public string manifestFileExtension {
get {
return ((string)(this["manifestFileExtension"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute(".cert")]
public string certificateFileExtension {
get {
return ((string)(this["certificateFileExtension"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("SHA1")]
public string securityAlgorithm {
get {
return ((string)(this["securityAlgorithm"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Schemas\\secext-1.0.xsd")]

View File

@ -104,15 +104,6 @@
<Setting Name="knownFileExtensions" Type="System.String" Scope="Application">
<Value Profile="(Default)">.vhd,.pvp,.vmdk,.mf,.cert,.xva,.ovf,.wim,.vdi,.sdi,.iso,.gz</Value>
</Setting>
<Setting Name="manifestFileExtension" Type="System.String" Scope="Application">
<Value Profile="(Default)">.mf</Value>
</Setting>
<Setting Name="certificateFileExtension" Type="System.String" Scope="Application">
<Value Profile="(Default)">.cert</Value>
</Setting>
<Setting Name="securityAlgorithm" Type="System.String" Scope="Application">
<Value Profile="(Default)">SHA1</Value>
</Setting>
<Setting Name="wsseSchemaLocation" Type="System.String" Scope="Application">
<Value Profile="(Default)">Schemas\secext-1.0.xsd</Value>
</Setting>

View File

@ -397,167 +397,6 @@ namespace XenOvf
}
#endregion
#region SIGNATURES
/// <summary>
/// Create a Manifest for the OVF
/// </summary>
/// <param name="pathToOvf">Absolute path to the OVF files</param>
/// <param name="ovfFileName">OVF file name (file.ovf)</param>
public static void Manifest(string pathToOvf, string ovfFileName)
{
List<ManifestFileEntry> mfes = new List<ManifestFileEntry>();
SHA1 sha1 = SHA1.Create();
EnvelopeType ovfenv;
try
{
using (FileStream stream = new FileStream(Path.Combine(pathToOvf, ovfFileName), FileMode.Open, FileAccess.Read))
{
ManifestFileEntry mfe = new ManifestFileEntry();
mfe.Algorithm = Properties.Settings.Default.securityAlgorithm;
mfe.Filename = ovfFileName;
mfe.Digest = sha1.ComputeHash(stream);
mfes.Add(mfe);
stream.Position = 0;
using (StreamReader sr = new StreamReader(stream))
ovfenv = Tools.Deserialize<EnvelopeType>(sr.ReadToEnd());
}
}
catch (Exception ex)
{
log.ErrorFormat("OVF.Security.Manifest: {0}", ex.Message);
throw;
}
if (ovfenv != null && ovfenv.References != null && ovfenv.References.File != null && ovfenv.References.File.Length > 0)
{
File_Type[] files = ovfenv.References.File;
foreach (File_Type file in files)
{
string currentfile = Path.Combine(pathToOvf, file.href);
if (!File.Exists(currentfile))
continue;
ManifestFileEntry mfe = new ManifestFileEntry();
using (FileStream computestream = new FileStream(currentfile, FileMode.Open, FileAccess.Read))
{
mfe.Algorithm = Properties.Settings.Default.securityAlgorithm;
mfe.Filename = file.href;
mfe.Digest = sha1.ComputeHash(computestream);
mfes.Add(mfe);
}
}
}
string manifest = Path.Combine(pathToOvf, string.Format("{0}{1}", Path.GetFileNameWithoutExtension(ovfFileName), Properties.Settings.Default.manifestFileExtension));
File.Delete(manifest); //no exception is thrown if file does not exist, so no need to check
using (FileStream stream = new FileStream(manifest, FileMode.CreateNew, FileAccess.Write))
{
using (StreamWriter sw = new StreamWriter(stream))
{
foreach (ManifestFileEntry mf in mfes)
sw.WriteLine(mf.ToString());
sw.Flush();
}
}
log.Debug("OVF.Manifest completed");
}
/// <summary>
/// Digitaly Sign the OVF
/// </summary>
/// <param name="x509">Signing Certificate</param>
/// <param name="pathToOvf">Absolute path to the OVF files</param>
/// <param name="ovfFileName">OVF file name (file.ovf)</param>
public static void Sign(X509Certificate2 Certificate, string PackageFolder, string PackageFileName)
{
if (Certificate == null)
{
throw new ArgumentException(Messages.CERTIFICATE_IS_INVALID);
}
string PackageName = PackageNameFromFileName(PackageFileName);
string ManifestPath = Path.Combine(PackageFolder, PackageName) + Properties.Settings.Default.manifestFileExtension;
// Create the manifest if it doesn't exist.
if (!File.Exists(ManifestPath))
{
Manifest(PackageFolder, PackageFileName);
}
// Compute the SHA1 hash of the manifest.
byte[] hash = null;
using (FileStream stream = new FileStream(ManifestPath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (SHA1 sha1 = SHA1.Create())
{
hash = sha1.ComputeHash(stream);
}
// Describe the file to sign.
ManifestFileEntry signed = new ManifestFileEntry();
signed.Algorithm = Properties.Settings.Default.securityAlgorithm;
signed.Filename = Path.GetFileName(ManifestPath);
// Compute the signature.
try
{
RSACryptoServiceProvider csp = (RSACryptoServiceProvider)Certificate.PrivateKey;
signed.Digest = csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
}
catch (Exception exception)
{
string message = exception.Message;
}
// Create the signature file.
string SignaturePath = Path.Combine(PackageFolder, PackageName) + Properties.Settings.Default.certificateFileExtension;
if (File.Exists(SignaturePath))
File.Delete(SignaturePath);
using (FileStream stream = new FileStream(SignaturePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(stream))
{
// Describe the signed file.
writer.WriteLine(signed.ToString());
// Export the certificate encoded in Base64 using DER.
writer.WriteLine("-----BEGIN CERTIFICATE-----");
string b64Cert = Convert.ToBase64String(Certificate.Export(X509ContentType.SerializedCert));
writer.WriteLine(b64Cert);
writer.WriteLine("-----END CERTIFICATE-----");
writer.WriteLine("\r\n");
writer.Flush();
}
}
public static string PackageNameFromFileName(string FileName)
{
// Always drop the last extension.
string PackageName = Path.GetFileNameWithoutExtension(FileName);
if (Path.HasExtension(PackageName) && Path.GetExtension(PackageName).ToLower().EndsWith("ova"))
{
// Drop any .ova extension.
PackageName = Path.GetFileNameWithoutExtension(PackageName);
}
return PackageName;
}
#endregion
#region PRIVATE
private static void CryptoFileWrapper(EnvelopeType env, string ovffilename, string password, bool encrypt)
{
@ -1006,31 +845,5 @@ namespace XenOvf
iv[loop - key.Length] = result[loop];
}
#endregion
internal class ManifestFileEntry
{
public string Algorithm = null;
public string Filename = null;
public byte[] Digest = null;
public override string ToString()
{
if (Algorithm != null && Filename != null && Digest != null)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in Digest)
{
sb.AppendFormat("{0:x2}", b);
}
// same as:
//string bc = BitConverter.ToString(Digest).Replace("-","");;
return string.Format("{0}({1})= {2}", Algorithm, Filename, sb.ToString());
}
else
{
throw new ArgumentNullException("NULL data inside ManifestFileEntry");
}
}
}
}
}

View File

@ -207,7 +207,7 @@ namespace XenOvf
foreach (File_Type file in files)
{
string ext = Path.GetExtension(file.href).ToLower();
if (ext == Properties.Settings.Default.manifestFileExtension || ext == Properties.Settings.Default.certificateFileExtension)
if (ext == Package.MANIFEST_EXT || ext == Package.CERTIFICATE_EXT)
continue;
if (package.HasFile(file.href))

View File

@ -109,15 +109,6 @@
<setting name="knownFileExtensions" serializeAs="String">
<value>.vhd,.pvp,.vmdk,.mf,.cert,.xva,.ovf,.wim,.vdi,.sdi,.iso,.gz</value>
</setting>
<setting name="manifestFileExtension" serializeAs="String">
<value>.mf</value>
</setting>
<setting name="certificateFileExtension" serializeAs="String">
<value>.cert</value>
</setting>
<setting name="securityAlgorithm" serializeAs="String">
<value>SHA1</value>
</setting>
<setting name="wsseSchemaLocation" serializeAs="String">
<value>Schemas\secext-1.0.xsd</value>
</setting>
@ -229,7 +220,8 @@
</setting>
<setting name="knownVirtualSystemTypes" serializeAs="Xml">
<value>
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<string>xen-3.0-unknown</string>
<string>xen-3.0-x32</string>
<string>xen-3.0-x86</string>