qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: James Bottomley <jejb@linux.ibm.com>
To: Connor Kuehl <ckuehl@redhat.com>, qemu-devel@nongnu.org
Cc: npmccallum@redhat.com, dgilbert@redhat.com
Subject: Re: Interactive launch over QMP socket?
Date: Wed, 10 Feb 2021 10:14:17 -0800	[thread overview]
Message-ID: <ac1a5075878d86b0a46db20cb6e579b5fec74d87.camel@linux.ibm.com> (raw)
In-Reply-To: <47b15088-514a-8174-029d-8d9c4571960a@redhat.com>

On Wed, 2021-02-10 at 12:01 -0600, Connor Kuehl wrote:
> Hello,
> 
> Does QEMU have an internal API which would allow VM construction to
> wait at a *very specific point* until specific data/QMP message(s)
> are supplied via the QMP socket?

Yes, the -S flag tells qemu to pause before starting the VM but after
setting it up.  It's the flag I use for SEV.

> For some additional context: QEMU supports launching AMD SEV-
> protected guests; in short: encrypted virtual machines. Guest owners
> may participate in attestation to cryptographically verify their
> assumptions about the guest's initial state, the host's platform, and
> the host platform owner's identity. If the guest owner is satisfied
> with the attestation process, a secret can be safely injected into
> the guest's address space over a secure channel.
> 
> Attestation is an unavoidably interactive process.
> 
> It appears that QEMU already exposes most of the API required to
> perform this attestation remotely with a guest owner over QMP, with
> only one exception: starting the attestation session. It looks like
> the session  components (policy, session-file, and dh-cert-file) are
> supplied via command line arguments to QEMU and don't have a message
> type in the QMP spec:
> 
> 	-object 
> sev-guest,id=sev0,cbitpos=47,reduced-phys-bits=1,policy=0x1,session-
> file=blah.session,dh-cert-file=guest_owner.cert
> 
> I would like to add a message type to QMP which allows guest owners
> to supply this data over a socket and _not_ require these components
> a priori via command line arguments. In doing so, this would allow
> for a 100% remote attestation process over the socket. However, I'm
> not sure how to express this interactive "waiting" for this data to
> become available with internal APIs (assuming it's not supplied as a
> command  line argument).

Well, I never understood why qemu can't deduce the value of cbitpos ...
it even errors out if you get it wrong.  However, other things like the
policy and the session file have to be present at start of day. 
They're not things that can be passed in after qemu starts building the
machine image because they need to be present to begin building it.

> For example, in order to accomplish a 100% remote attestation:
> 
> Somewhere in between sev_guest_init() and sev_launch_start(), the
> guest owner may send the following messages:
> 
> 1. "query-sev" to collect important information about the platform
> state
> 
> 2. "query-sev-capabilities" to independently verify the platform 
> certificate chain and derive a shared secret for establishing a
> secure channel with the AMD SP.
> 
> 3. "sev-launch-start" this is the only message that I think is
> missing from the QMP message types for remote attestation. This is
> how the guest owner would deliver the session components over the
> socket instead of as command line arguments.

The patch for remote attestation (which was only recently added to the
PSP protocol) is here:

https://lore.kernel.org/kvm/20210105163943.30510-1-brijesh.singh@amd.com/

> Then, sometime before the VM is launched and is running, the guest
> owner may send:
> 
> 4. "query-sev-launch-measure" to compare its measurement against the
> AMD SP's measurement
> 
> 5. "sev-inject-launch-secret" if happy with attestation, securely 
> deliver secrets
> 
> 6. Guest owner could send a "cont" command and the VM can launch
> 
> Any advice on how to accomplish adding this degree of interaction to 
> supplying inputs to specific parts of the launch process this is
> greatly appreciated.

I've attached the python script I use to launch sev guests.  However,
it doesn't include the launch bundle because that has to have already
been passed in when qemu was started.

James

---
#!/usr/bin/python3
##
# Python script to inject a secret disk password into a paused SEV VM
#  (to pause the VM start with -S option)
#
# This assumes you've already created the launch bundle using sev-tool
# from https://github.com/AMDESE/sev-tool.git
#
# sev-tool --generate_launch_blob
#
# creates several files, the only one this script needs is the TIK and TEK
# keys which are stored in tmp_tk.bin
#
# Once TIK/TEK are known, the script will probe the VM for the sev
# parameters needed to calculate the launch measure, retrieve the launch
# measure and verify against the measure calculated from the OVMF hash
# and if that matches create the secret bundle and inject it
#
# Tables and chapters refer to the amd 55766.pdf document
#
# https://www.amd.com/system/files/TechDocs/55766_SEV-KM_API_Specification.pdf
##
import sys
import os 
import base64
import hmac
import hashlib
from argparse import ArgumentParser
from uuid import UUID
from Crypto.Cipher import AES
from Crypto.Util import Counter
from git.qemu.python.qemu import qmp

if __name__ == "__main__":
    parser = ArgumentParser(description='Inject secret into SEV')
    parser.add_argument('--tiktek-file',
                        help='file where sev-tool stored the TIK/TEK combination, defaults to tmp_tk.bin',
                        default='tmp_tk.bin')
    parser.add_argument('--passwd',
                        help='Disk Password',
                        required=True)
    parser.add_argument('--ovmf-hash',
                        help='hash of OVMF firmware blob in hex')
    parser.add_argument('--ovmf-file',
                        help='location of OVMF file to calculate hash from')
    parser.add_argument('--socket',
                        help='Socket to connect to QMP on, defaults to localhost:6550',
                        default='localhost:6550')
    args = parser.parse_args()

    if (args.ovmf_file):
        fh = open (args.ovmf_file, 'rb')
        h = hashlib.sha256(fh.read())
        ovmf_hash = h.digest()
    elif (args.ovmf_hash):
        ovmf_hash = bytearray.fromhex(args.ovmf_hash)
    else:
        parser.error('one of --ovmf-hash or -ovmf-file must be specified')

    if (args.socket[0] == '/'):
        socket = args.socket
    elif (':' in args.socket):
        s = args.socket.split(':')
        socket = (s[0], int(s[1]))
    else:
        parse.error('--socket must be <host>:<port> or /path/to/unix')

    fh=open(args.tiktek_file, 'rb')
    tiktek=bytearray(fh.read())
    fh.close()

    ##
    #  tiktek file is just two binary aes128 keys
    ##
    TEK=tiktek[0:16]
    TIK=tiktek[16:32]

    disk_secret = args.passwd

    Qmp = qmp.QEMUMonitorProtocol(address=socket);
    Qmp.connect()
    caps = Qmp.command('query-sev')
    print('SEV query found API={api-major}.{api-minor} build={build-id} policy={policy}'.format(**caps))
    h = hmac.new(TIK, digestmod='sha256');

    ##
    # calculated per section 6.5.2
    ##
    h.update(bytes([0x04]))
    h.update(caps['api-major'].to_bytes(1,byteorder='little'))
    h.update(caps['api-minor'].to_bytes(1,byteorder='little'))
    h.update(caps['build-id'].to_bytes(1,byteorder='little'))
    h.update(caps['policy'].to_bytes(4,byteorder='little'))
    h.update(ovmf_hash)

    print('\nGetting Launch Measurement')
    meas = Qmp.command('query-sev-launch-measure')
    launch_measure = base64.b64decode(meas['data'])

    ##
    # returned data per Table 52. LAUNCH_MEASURE Measurement Buffer
    ##
    nonce = launch_measure[32:48]
    h.update(nonce)
    measure = launch_measure[0:32]

    print('Measure:   ', measure.hex())
    print('should be: ', h.digest().hex())
    print('')

    if (measure != h.digest()):
        sys.exit('Measurement doesn\'t match')

    print('Measurement matches, Injecting Secret')

    ##
    # construct the secret table: two guids + 4 byte lengths plus string
    # and zero terminator
    #
    # Secret layout is  guid, len (4 bytes), data
    # with len being the length from start of guid to end of data
    #
    # The table header covers the entire table then each entry covers
    # only its local data
    #
    # our current table has the header guid with total table length
    # followed by the secret guid with the zero terminated secret 
    ##
    
    # total length of table: header plus one entry with trailing \0
    l = 16 + 4 + 16 + 4 + len(disk_secret) + 1
    # SEV-ES requires rounding to 16
    l = (l + 15) & ~15
    secret = bytearray(l);
    secret[0:16] = UUID('{1e74f542-71dd-4d66-963e-ef4287ff173b}').bytes_le
    secret[16:20] = len(secret).to_bytes(4, byteorder='little')
    secret[20:36] = UUID('{736869e5-84f0-4973-92ec-06879ce3da0b}').bytes_le
    secret[36:40] = (16 + 4 + len(disk_secret) + 1).to_bytes(4, byteorder='little')
    secret[40:40+len(disk_secret)] = disk_secret.encode()
    
    ##
    # encrypt the secret table with the TEK in ctr mode using a random IV
    ##
    IV=os.urandom(16)
    # -EKNUCKLEHEADS in python crypto don't understand CTR mode
    e = AES.new(TEK, AES.MODE_CTR, counter=Counter.new(128,initial_value=int.from_bytes(IV, byteorder='big')));
    encrypted_secret = e.encrypt(bytes(secret))

    ##
    # ultimately needs to be an argument, but there's only
    # compressed and no real use case
    ##
    FLAGS = 0

    ##
    # Table 55. LAUNCH_SECRET Packet Header Buffer
    ##
    header=bytearray(52);
    header[0:4]=FLAGS.to_bytes(4,byteorder='little')
    header[4:20]=IV
    h = hmac.new(TIK, digestmod='sha256');
    h.update(bytes([0x01]))
    # FLAGS || IV
    h.update(header[0:20])
    h.update(l.to_bytes(4, byteorder='little'))
    h.update(l.to_bytes(4, byteorder='little'))
    h.update(encrypted_secret)
    h.update(measure)
    header[20:52]=h.digest()

    Qmp.command('sev-inject-launch-secret',
                **{'packet-header': base64.b64encode(header).decode(),
                   'secret': base64.b64encode(encrypted_secret).decode()
                })

    print('\nSecret Injection Successful, starting VM')

    Qmp.command('cont')




  reply	other threads:[~2021-02-10 18:15 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-02-10 18:01 Interactive launch over QMP socket? Connor Kuehl
2021-02-10 18:14 ` James Bottomley [this message]
2021-02-10 18:46   ` Connor Kuehl
2021-02-10 19:06     ` James Bottomley
2021-02-10 20:39       ` Connor Kuehl
2021-02-11  9:11         ` Dr. David Alan Gilbert
2021-02-22 11:40 ` Kevin Wolf
2021-02-22 15:39   ` Daniel P. Berrangé
2021-02-22 16:23     ` Kevin Wolf
2021-02-22 12:18 ` Daniel P. Berrangé
2021-02-22 15:00   ` Connor Kuehl
2021-02-22 15:36     ` Daniel P. Berrangé

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ac1a5075878d86b0a46db20cb6e579b5fec74d87.camel@linux.ibm.com \
    --to=jejb@linux.ibm.com \
    --cc=ckuehl@redhat.com \
    --cc=dgilbert@redhat.com \
    --cc=npmccallum@redhat.com \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).