# Copyright 2013-2014 Eucalyptus Systems, Inc.
#
# Redistribution and use of this software 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
# OWNER 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.
import binascii
import hashlib
import logging
import os.path
import subprocess
import lxml.etree
import lxml.objectify
import euca2ools.bundle
class BundleManifest(object):
def __init__(self, loglevel=None):
self.log = logging.getLogger(self.__class__.__name__)
if loglevel is not None:
self.log.level = loglevel
self.image_arch = None
self.kernel_id = None
self.ramdisk_id = None
self.block_device_mappings = {} # virtual -> device
self.product_codes = []
self.image_name = None
self.account_id = None
self.image_type = None
self.image_digest = None
self.image_digest_algorithm = None
self.image_size = None
self.bundled_image_size = None
self.enc_key = None
self.enc_iv = None
self.enc_algorithm = None
self.image_parts = []
@classmethod
def read_from_file(cls, manifest_filename, privkey_filename=None):
with open(manifest_filename) as manifest_fileobj:
return cls.read_from_fileobj(manifest_fileobj, privkey_filename)
@classmethod
def read_from_fileobj(cls, manifest_fileobj, privkey_filename=None):
xml = lxml.objectify.parse(manifest_fileobj).getroot()
manifest = cls()
mconfig = xml.machine_configuration
manifest.image_arch = mconfig.architecture.text.strip()
if hasattr(mconfig, 'kernel_id'):
manifest.kernel_id = mconfig.kernel_id.text.strip()
if hasattr(mconfig, 'ramdisk_id'):
manifest.ramdisk_id = mconfig.ramdisk_id.text.strip()
if hasattr(mconfig, 'block_device_mappings'):
for xml_mapping in mconfig.block_device_mappings.iter(
tag='block_device_mapping'):
device = xml_mapping.device.text.strip()
virtual = xml_mapping.virtual.text.strip()
manifest.block_device_mappings[virtual] = device
if hasattr(mconfig, 'productcodes'):
for xml_pcode in mconfig.productcodes.iter(tag='product_code'):
manifest.product_codes.append(xml_pcode.text.strip())
manifest.image_name = xml.image.name.text.strip()
manifest.account_id = xml.image.user.text.strip()
manifest.image_type = xml.image.type.text.strip()
manifest.image_digest = xml.image.digest.text.strip()
manifest.image_digest_algorithm = xml.image.digest.get('algorithm')
manifest.image_size = int(xml.image.size.text.strip())
manifest.bundled_image_size = int(xml.image.bundled_size.text.strip())
if privkey_filename is not None:
try:
manifest.enc_key = _decrypt_hex(
xml.image.user_encrypted_key.text.strip(),
privkey_filename)
except (AttributeError, ValueError):
manifest.enc_key = _decrypt_hex(
xml.image.ec2_encrypted_key.text.strip(), privkey_filename)
manifest.enc_algorithm = xml.image.user_encrypted_key.get(
'algorithm')
try:
manifest.enc_iv = _decrypt_hex(
xml.image.user_encrypted_iv.text.strip(), privkey_filename)
except (AttributeError, ValueError):
manifest.enc_iv = _decrypt_hex(
xml.image.ec2_encrypted_iv.text.strip(), privkey_filename)
manifest.image_parts = [None] * int(xml.image.parts.get('count'))
for xml_part in xml.image.parts.iter(tag='part'):
index = int(xml_part.get('index'))
manifest.image_parts[index] = euca2ools.bundle.BundlePart(
xml_part.filename.text.strip(), xml_part.digest.text.strip(),
xml_part.digest.get('algorithm'))
for index, part in enumerate(manifest.image_parts):
if part is None:
raise ValueError('part {0} must not be None'.format(index))
return manifest
def dump_to_str(self, privkey_filename, user_cert_filename,
ec2_cert_filename, pretty_print=False):
if self.enc_key is None:
raise ValueError('enc_key must not be None')
if self.enc_iv is None:
raise ValueError('enc_iv must not be None')
ec2_fp = euca2ools.bundle.util.get_cert_fingerprint(ec2_cert_filename)
self.log.info('creating manifest for EC2 service with fingerprint %s',
ec2_fp)
self.log.debug('EC2 certificate: %s', ec2_cert_filename)
self.log.debug('user certificate: %s', user_cert_filename)
self.log.debug('user private key: %s', privkey_filename)
xml = lxml.objectify.Element('manifest')
# Manifest version
xml.version = '2007-10-10'
# Our version
xml.bundler = None
xml.bundler.name = 'euca2ools'
xml.bundler.version = euca2ools.__version__
xml.bundler.release = 0
# Target hardware
xml.machine_configuration = None
mconfig = xml.machine_configuration
assert self.image_arch is not None
mconfig.architecture = self.image_arch
if self.image_type == 'machine':
if self.block_device_mappings:
mconfig.block_device_mapping = None
for virtual, device in sorted(
self.block_device_mappings.items()):
xml_mapping = lxml.objectify.Element('mapping')
xml_mapping.virtual = virtual
xml_mapping.device = device
mconfig.block_device_mapping.append(xml_mapping)
if self.product_codes:
mconfig.product_codes = None
for code in self.product_codes:
xml_code = lxml.objectify.Element('product_code')
mconfig.product_codes.append(xml_code)
mconfig.product_codes.product_code[-1] = code
# kernel_id and ramdisk_id are normally meaningful only for machine
# images, but eucalyptus also uses them to indicate kernel and ramdisk
# images using the magic string "true", so their presence cannot be
# made contingent on whether the image is a machine image or not. Be
# careful not to create invalid kernel or ramdisk manifests because of
# this.
if self.kernel_id:
mconfig.kernel_id = self.kernel_id
if self.ramdisk_id:
mconfig.ramdisk_id = self.ramdisk_id
# Image info
xml.image = None
assert self.image_name is not None
xml.image.name = self.image_name
assert self.account_id is not None
xml.image.user = self.account_id
# xml.image.type must appear immediately after xml.image.user
# for EC2 compatibility.
assert self.image_type is not None
xml.image.type = self.image_type
assert self.image_digest is not None
xml.image.digest = self.image_digest
assert self.image_digest_algorithm is not None
xml.image.digest.set('algorithm', self.image_digest_algorithm)
assert self.image_size is not None
xml.image.size = self.image_size
assert self.bundled_image_size is not None
xml.image.bundled_size = self.bundled_image_size
# Bundle encryption keys (these are cloud-specific)
assert self.enc_key is not None
assert self.enc_iv is not None
assert self.enc_algorithm is not None
xml.image.ec2_encrypted_key = _public_encrypt(self.enc_key,
ec2_cert_filename)
xml.image.ec2_encrypted_key.set('algorithm', self.enc_algorithm)
if user_cert_filename:
xml.image.user_encrypted_key = _public_encrypt(self.enc_key,
user_cert_filename)
else:
# Absence results in 400 (InvalidManifest)
xml.image.user_encrypted_key = None
xml.image.user_encrypted_key.set('algorithm', self.enc_algorithm)
xml.image.ec2_encrypted_iv = _public_encrypt(self.enc_iv,
ec2_cert_filename)
if user_cert_filename:
xml.image.user_encrypted_iv = _public_encrypt(self.enc_iv,
user_cert_filename)
else:
# Absence results in 400 (InvalidManifest)
xml.image.user_encrypted_iv = None
# Bundle parts
xml.image.parts = None
xml.image.parts.set('count', str(len(self.image_parts)))
for index, part in enumerate(self.image_parts):
if part is None:
raise ValueError('part {0} must not be None'.format(index))
part_elem = lxml.objectify.Element('part')
part_elem.set('index', str(index))
part_elem.filename = os.path.basename(part.filename)
part_elem.digest = part.hexdigest
part_elem.digest.set('algorithm', part.digest_algorithm)
# part_elem.append(lxml.etree.Comment(
# ' size: {0} '.format(part.size)))
xml.image.parts.append(part_elem)
# Cleanup for signature
lxml.objectify.deannotate(xml, xsi_nil=True)
lxml.etree.cleanup_namespaces(xml)
if privkey_filename:
to_sign = (lxml.etree.tostring(xml.machine_configuration) +
lxml.etree.tostring(xml.image))
signature = _rsa_sha1_sign(to_sign, privkey_filename)
else:
# Absence yields 400 (InvalidManifest)
# Empty contents yield 500 (InternalError)
signature = 'UNSIGNED'
xml.signature = signature
self.log.debug('hex-encoded signature: %s', signature)
lxml.objectify.deannotate(xml, xsi_nil=True)
lxml.etree.cleanup_namespaces(xml)
self.log.debug('-- manifest content --\n', extra={'append': True})
pretty_manifest = lxml.etree.tostring(xml, pretty_print=True).strip()
self.log.debug('%s', pretty_manifest, extra={'append': True})
self.log.debug('-- end of manifest content --')
return lxml.etree.tostring(xml, xml_declaration=True,
pretty_print=pretty_print).strip()
def dump_to_file(self, manifest_file, privkey_filename,
user_cert_filename, ec2_cert_filename,
pretty_print=False):
manifest_file.write(self.dump_to_str(
privkey_filename, user_cert_filename, ec2_cert_filename,
pretty_print=pretty_print))
def _decrypt_hex(hex_encrypted_key, privkey_filename):
popen = subprocess.Popen(['openssl', 'rsautl', '-decrypt', '-pkcs',
'-inkey', privkey_filename],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
binary_encrypted_key = binascii.unhexlify(hex_encrypted_key)
(decrypted_key, _) = popen.communicate(binary_encrypted_key)
try:
# Make sure it might actually be an encryption key.
# This isn't perfect, but it's still better than nothing.
int(decrypted_key, 16)
return decrypted_key
except ValueError:
pass
raise ValueError("Failed to decrypt the bundle's encryption key. "
"Ensure the key supplied matches the one used for "
"bundling.")
def _public_encrypt(content, cert_filename):
popen = subprocess.Popen(['openssl', 'rsautl', '-encrypt', '-pkcs',
'-inkey', cert_filename, '-certin'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(stdout, _) = popen.communicate(content)
return binascii.hexlify(stdout)
def _rsa_sha1_sign(content, privkey_filename):
digest = hashlib.sha1()
digest.update(content)
popen = subprocess.Popen(['openssl', 'pkeyutl', '-sign', '-inkey',
privkey_filename, '-pkeyopt', 'digest:sha1'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(stdout, _) = popen.communicate(digest.digest())
return binascii.hexlify(stdout)