Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add support for encrypted images #2297

Draft
wants to merge 12 commits into
base: criu-dev
Choose a base branch
from
Draft
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ task:
ln -sf /usr/include/google/protobuf/descriptor.proto images/google/protobuf/descriptor.proto
dnf config-manager --set-enabled crb # Same as CentOS 8 powertools
dnf -y install epel-release epel-next-release
dnf -y install --allowerasing asciidoc gcc git gnutls-devel libaio-devel libasan libcap-devel libnet-devel libnl3-devel libbsd-devel libselinux-devel make protobuf-c-devel protobuf-devel python-devel python-PyYAML python-protobuf python-junit_xml python3-importlib-metadata xmlto libdrm-devel
dnf -y install --allowerasing asciidoc gcc git gnutls-devel libaio-devel libasan libcap-devel libnet-devel libnl3-devel libbsd-devel libselinux-devel make protobuf-c-devel protobuf-devel python-devel python-PyYAML python-protobuf python-junit_xml python3-importlib-metadata python3-cryptography xmlto libdrm-devel
# The image has a too old version of nettle which does not work with gnutls.
# Just upgrade to the latest to make the error go away.
dnf -y upgrade nettle nettle-devel
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/encrypted-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Encrypted Images Test

on: [push, pull_request]

# Cancel any preceding run on the pull request.
concurrency:
group: encrypted-images-test-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/criu-dev' }}

jobs:
build:
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v2
- name: Run CRIU Encrypted Images Test
run: sudo -E make -C scripts/ci local ENCRYPTED_IMAGES_TEST=1
1 change: 1 addition & 0 deletions Documentation/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ endif
FOOTER := footer.txt
SRC1 += crit.txt
SRC1 += criu-ns.txt
SRC1 += criu-keygen.txt
SRC1 += compel.txt
SRC1 += criu-amdgpu-plugin.txt
SRC8 += criu.txt
Expand Down
29 changes: 29 additions & 0 deletions Documentation/criu-keygen.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CRIU-KEYGEN(1)
==============
include::footer.txt[]

NAME
----
criu-keygen - criu encryption key utility

SYNOPSIS
--------
*criu-keygen* [<options>]

DESCRIPTION
-----------
The *criu-keygen* command generates and manages encryption keys for CRIU.
*criu-keygen* can create keys for use by CRIU. The type of key to be
generated is specified with the *-t* option. If invoked without any arguments,
*criu-keygen* will generate an RSA keys.

A system administrator wishing to use CRIU with encryption, would run *criu-keygen*
once to create a certficicate and private key in '/etc/pki/criu/'.

SEE ALSO
--------
criu(8)

AUTHOR
------
The CRIU team
8 changes: 8 additions & 0 deletions Documentation/criu.txt
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,14 @@ By default the option is set to *fpu* and *ins*.
option is intended for post-copy (lazy) migration and should be
used in conjunction with *restore* with appropriate options.

*-e*, *--encrypt*::
Encrypt the contents of the image files. The encryption key is
loaded from an X.509 certificate, which can be specified using
the *--tls-cert* option. The *restore* operation automatically
detects if the image files are encrypted and loads a private key
from a corresponding PEM file, which can be specified using the
*--tls-key* option.

*--file-validation* ['mode']::
Set the method to be used to validate open files. Validation is done
to ensure that the version of the file being restored is the same
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ ruff:
lib/pycriu/images/images.py \
scripts/criu-ns \
test/others/criu-ns/run.py \
scripts/criu-keygen \
crit/*.py \
crit/crit/*.py \
scripts/uninstall_module.py \
Expand Down
109 changes: 91 additions & 18 deletions crit/crit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
import sys
import json
import os
import base64

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

import pycriu
from . import __version__


CRIU_KEY = '/etc/pki/criu/private/key.pem'


def inf(opts):
if opts['in']:
return open(opts['in'], 'rb')
Expand Down Expand Up @@ -36,11 +44,56 @@
return open(os.path.join(opts['dir'], name), mode='rb')


def get_cipher_token(opts):
Fixed Show fixed Hide fixed
"""
get_cipher_token returns the decrypted cipher token.
"""
plaintext_token = None
if 'in' in opts:
dir_path = os.path.dirname(os.path.realpath(opts['in']))
elif 'dir' in opts:
dir_path = os.path.realpath(opts['dir'])
else:
raise TypeError("Invalid input")
cipher_img_path = os.path.join(dir_path, 'cipher.img')

# We assume that when this image is not present,
# the checkpoint images are not encrypted. Thus,
# here we continue with normal decode if 'cipher.img'
# doesn't exist.
if not os.path.exists(cipher_img_path):
return None

with open(cipher_img_path, mode='rb') as cipher_img_file:
cipher_img = pycriu.images.load(cipher_img_file)
# Validate the content of the cipher.img
if ('entries' not in cipher_img or
len(cipher_img['entries']) != 1 or
'token' not in cipher_img['entries'][0]):
raise TypeError("Invalid cipher image")

encrypted_token = base64.b64decode(cipher_img['entries'][0]['token'])

priv_key_file = opts['tls_key']
with open(priv_key_file, "rb") as f:
priv_key = serialization.load_pem_private_key(f.read(), None, default_backend())
if not isinstance(priv_key, rsa.RSAPrivateKey):
raise TypeError("Only RSA private keys are supported.")

# GnuTLS uses the PKCS#1 v1.5 padding scheme by default.
plaintext_token = priv_key.decrypt(encrypted_token, padding.PKCS1v15())
return plaintext_token


def decode(opts):
indent = None
token = None

if opts['in'] and os.path.basename(opts['in']) not in ['cipher.img', 'stats-dump', 'stats-restore']:
token = get_cipher_token(opts)

try:
img = pycriu.images.load(inf(opts), opts['pretty'], opts['nopl'])
img = pycriu.images.load(inf(opts), opts['pretty'], opts['nopl'], token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
except pycriu.images.MagicException as exc:
print("Unknown magic %#x.\n"
"Maybe you are feeding me an image with "
Expand Down Expand Up @@ -101,10 +154,11 @@

def explore_ps(opts):
pss = {}
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'))
token = get_cipher_token(opts)
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
for p in ps_img['entries']:
core = pycriu.images.load(
dinf(opts, 'core-%d.img' % get_task_id(p, 'pid')))
dinf(opts, 'core-%d.img' % get_task_id(p, 'pid')), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
ps = ps_item(p, core['entries'][0])
pss[ps.pid] = ps

Expand All @@ -128,10 +182,11 @@

def ftype_find_in_files(opts, ft, fid):
global files_img
token = get_cipher_token(opts)

if files_img is None:
try:
files_img = pycriu.images.load(dinf(opts, "files.img"))['entries']
files_img = pycriu.images.load(dinf(opts, "files.img"), token=token)['entries']

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
except Exception:
files_img = []

Expand All @@ -146,6 +201,7 @@


def ftype_find_in_image(opts, ft, fid, img):
token = get_cipher_token(opts)
f = ftype_find_in_files(opts, ft, fid)
if f:
if ft['field'] in f:
Expand All @@ -154,7 +210,7 @@
return None

if ft['img'] is None:
ft['img'] = pycriu.images.load(dinf(opts, img))['entries']
ft['img'] = pycriu.images.load(dinf(opts, img), token=token)['entries']

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
for f in ft['img']:
if f['id'] == fid:
return f
Expand Down Expand Up @@ -218,18 +274,19 @@


def explore_fds(opts):
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'))
token = get_cipher_token(opts)
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
for p in ps_img['entries']:
pid = get_task_id(p, 'pid')
idi = pycriu.images.load(dinf(opts, 'ids-%s.img' % pid))
idi = pycriu.images.load(dinf(opts, 'ids-%s.img' % pid), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
fdt = idi['entries'][0]['files_id']
fdi = pycriu.images.load(dinf(opts, 'fdinfo-%d.img' % fdt))
fdi = pycriu.images.load(dinf(opts, 'fdinfo-%d.img' % fdt), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.

print("%d" % pid)
for fd in fdi['entries']:
print("\t%7d: %s" % (fd['fd'], get_file_str(opts, fd)))

fdi = pycriu.images.load(dinf(opts, 'fs-%d.img' % pid))['entries'][0]
fdi = pycriu.images.load(dinf(opts, 'fs-%d.img' % pid), token=token)['entries'][0]

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
print("\t%7s: %s" %
('cwd', get_file_str(opts, {
'type': 'REG',
Expand Down Expand Up @@ -258,11 +315,12 @@


def explore_mems(opts):
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'))
token = get_cipher_token(opts)
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
vids = vma_id()
for p in ps_img['entries']:
pid = get_task_id(p, 'pid')
mmi = pycriu.images.load(dinf(opts, 'mm-%d.img' % pid))['entries'][0]
mmi = pycriu.images.load(dinf(opts, 'mm-%d.img' % pid), token=token)['entries'][0]

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.

print("%d" % pid)
print("\t%-36s %s" % ('exe',
Expand Down Expand Up @@ -311,12 +369,13 @@


def explore_rss(opts):
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'))
token = get_cipher_token(opts)
ps_img = pycriu.images.load(dinf(opts, 'pstree.img'), token=token)

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
for p in ps_img['entries']:
pid = get_task_id(p, 'pid')
vmas = pycriu.images.load(dinf(opts, 'mm-%d.img' %
pid))['entries'][0]['vmas']
pms = pycriu.images.load(dinf(opts, 'pagemap-%d.img' % pid))['entries']
vmas = pycriu.images.load(
dinf(opts, 'mm-%d.img' % pid), token=token)['entries'][0]['vmas']

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.
pms = pycriu.images.load(dinf(opts, 'pagemap-%d.img' % pid), token=token)['entries']

Check warning

Code scanning / CodeQL

File is not always closed Warning

File is opened but is not closed.

print("%d" % pid)
vmi = 0
Expand Down Expand Up @@ -385,6 +444,10 @@
'-o',
'--out',
help='where to put criu image in json format (stdout by default)')
decode_parser.add_argument(
'--tls-key', default=CRIU_KEY,
help=f'path to private key in PEM format used to decrypt images (default: {CRIU_KEY})'
)
decode_parser.set_defaults(func=decode, nopl=False)

# Encode
Expand All @@ -409,15 +472,25 @@
x_parser = subparsers.add_parser('x', help='explore image dir')
x_parser.add_argument('dir')
x_parser.add_argument('what', choices=['ps', 'fds', 'mems', 'rss'])
x_parser.add_argument(
'--tls-key', default=CRIU_KEY,
help=f'path to private key in PEM format used to decrypt images (default: {CRIU_KEY})'
)
x_parser.set_defaults(func=explore)

# Show
show_parser = subparsers.add_parser(
'show', help="convert criu image from binary to human-readable json")
show_parser.add_argument("in")
show_parser.add_argument('--nopl',
help='do not show entry payload (if exists)',
action='store_true')
show_parser.add_argument(
'--nopl',
help='do not show entry payload (if exists)',
action='store_true'
)
show_parser.add_argument(
'--tls-key', default=CRIU_KEY,
help=f'path to private key in PEM format used to decrypt images (default: {CRIU_KEY})'
)
show_parser.set_defaults(func=decode, pretty=True, out=None)

opts = vars(parser.parse_args())
Expand Down
3 changes: 3 additions & 0 deletions criu/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,15 @@ install: $(obj)/criu
$(Q) install -m 755 scripts/systemd-autofs-restart.sh $(DESTDIR)$(LIBEXECDIR)/criu/scripts
$(E) " INSTALL " scripts/criu-ns
$(Q) install -m 755 scripts/criu-ns $(DESTDIR)$(SBINDIR)
$(E) " INSTALL " scripts/criu-keygen
$(Q) install -m 755 scripts/criu-keygen $(DESTDIR)$(SBINDIR)
.PHONY: install

uninstall:
$(E) " UNINSTALL" criu
$(Q) $(RM) $(addprefix $(DESTDIR)$(SBINDIR)/,criu)
$(Q) $(RM) $(addprefix $(DESTDIR)$(SBINDIR)/,criu-ns)
$(Q) $(RM) $(addprefix $(DESTDIR)$(SBINDIR)/,criu-keygen)
$(Q) $(RM) $(addprefix $(DESTDIR)$(INCLUDEDIR)/criu/,$(notdir $(UAPI_HEADERS)))
$(Q) $(RM) $(addprefix $(DESTDIR)$(LIBEXECDIR)/criu/scripts/,systemd-autofs-restart.sh)
.PHONY: uninstall
Expand Down
2 changes: 1 addition & 1 deletion criu/action-scripts.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ static int run_shell_scripts(const char *action)
list_for_each_entry(script, &scripts, node) {
int err;
pr_debug("\t[%s]\n", script->path);
err = cr_system(-1, -1, -1, script->path, (char *[]){ script->path, NULL }, 0);
err = cr_system(-1, -1, -1, script->path, (char *[]){ script->path, NULL }, 0, TLS_MODE_NONE);
if (err)
pr_err("Script %s exited with %d\n", script->path, err);
retval |= err;
Expand Down
Loading
Loading