Skip to content

Commit 4296c22

Browse files
committed
Added integrity check to cf-remote remote installation
Ticket: ENT-13005 Signed-off-by: Victor Moene <victor.moene@northern.tech>
1 parent 82392ef commit 4296c22

5 files changed

Lines changed: 211 additions & 24 deletions

File tree

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ include cf_remote/nt-discovery.sh
22
include cf_remote/Vagrantfile
33
include cf_remote/default_provision.sh
44
include cf_remote/demo.sql
5+
include cf_remote/remote-download.sh
6+

cf_remote/nt-discovery.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ run_command "command -v apt" "APT" "Cannot find apt"
7474
run_command "command -v pkg" "PKG" "Cannot find pkg"
7575
run_command "command -v zypper" "ZYPPER" "Cannot find zypper"
7676
run_command "command -v curl" "CURL" "Cannot find curl"
77+
run_command "command -v wget" "WGET" "Cannot find wget"
78+
run_command "command -v sha256sum" "SHA256SUM" "Cannot find sha256sum"
7779

7880
# ip
7981

cf_remote/remote-download.sh

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
OPTS=$(getopt -o IChc: --long insecure,curl,help,checksum: -n "$0" -- "$@")
6+
if [ $? != 0 ]; then
7+
exit 1;
8+
fi
9+
10+
usage() {
11+
cat << EOF
12+
Usage: $0 [OPTIONS] <package_url>
13+
A script to download packages from url securely.
14+
15+
Options:
16+
-c, --checksum VAL Provide a SHA256 checksum to verify package integrity
17+
-C, --curl Use curl instead of default wget
18+
-I, --insecure Skip package integrity verification
19+
EOF
20+
exit 0
21+
}
22+
23+
# Arg parsing
24+
eval set -- "$OPTS"
25+
26+
INSECURE=0
27+
USE_CURL=0
28+
CHECKSUM=""
29+
30+
while true; do
31+
case "$1" in
32+
-h|--help) usage ;;
33+
-I|--insecure) INSECURE=1; shift ;;
34+
-C|--curl) USE_CURL=1; shift ;;
35+
-c|--checksum) CHECKSUM="$2"; shift 2 ;;
36+
--) shift; break ;;
37+
*) echo "Error"; exit 1 ;;
38+
esac
39+
done
40+
41+
shift $((OPTIND - 1))
42+
43+
PACKAGE=$1
44+
if [ -z "$PACKAGE" ]; then
45+
usage
46+
fi
47+
48+
# temp file
49+
50+
tmpfile=$(mktemp)
51+
cleanup() {
52+
rm -f "$tmpfile"
53+
}
54+
trap cleanup EXIT QUIT TERM INT
55+
56+
# Download
57+
58+
if [ "$USE_CURL" -eq 1 ]; then
59+
curl --fail -sS -o "$tmpfile" "$PACKAGE"
60+
else
61+
wget -nv -O "$tmpfile" "$PACKAGE"
62+
fi
63+
64+
# Checksum
65+
66+
filename="$(basename "$PACKAGE")"
67+
68+
if [ -n "$CHECKSUM" ]; then
69+
hash="$(sha256sum "$tmpfile" | awk '{print $1}')"
70+
71+
if [[ "$CHECKSUM" != "$hash" ]]; then
72+
if [ "$INSECURE" -eq 0 ]; then
73+
echo "Package '$PACKAGE' doesn't match the expected checksum '$CHECKSUM'. Run with --insecure to skip"
74+
exit 1
75+
fi
76+
echo "Package '$PACKAGE' doesn't match the expected checksum '$CHECKSUM'. Continuing due to insecure flag"
77+
fi
78+
fi
79+
80+
mv "$tmpfile" "$filename"
81+

cf_remote/remote.py

Lines changed: 125 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
import sys
33
import re
4+
45
from os.path import basename, dirname, join, exists
56
from collections import OrderedDict
67
from typing import Union
@@ -276,7 +277,17 @@ def get_info(host, *, users=None, connection=None):
276277
data["role"] = "hub" if discovery.get("NTD_CFHUB") else "client"
277278

278279
data["bin"] = {}
279-
for bin in ["dpkg", "rpm", "yum", "apt", "pkg", "zypper", "curl"]:
280+
for bin in [
281+
"dpkg",
282+
"rpm",
283+
"yum",
284+
"apt",
285+
"pkg",
286+
"zypper",
287+
"curl",
288+
"wget",
289+
"sha256sum",
290+
]:
280291
path = discovery.get("NTD_{}".format(bin.upper()))
281292
if path:
282293
data["bin"][bin] = path
@@ -432,7 +443,7 @@ def bootstrap_host(host_data, policy_server, *, connection=None, trust_server=Tr
432443
def _package_from_list(tags, extension, packages):
433444
artifacts = [Artifact(None, p) for p in packages]
434445
artifact = filter_artifacts(artifacts, tags, extension)[-1]
435-
return artifact.url
446+
return artifact.url, artifact
436447

437448

438449
def _package_from_releases(
@@ -445,7 +456,7 @@ def _package_from_releases(
445456
release = releases.pick_version(version)
446457
if release is None:
447458
print("Could not find a release for version {}".format(version))
448-
return None
459+
return None, None
449460

450461
release.init_download()
451462

@@ -455,7 +466,7 @@ def _package_from_releases(
455466
version, edition
456467
)
457468
)
458-
return None
469+
return None, None
459470

460471
artifacts = release.find(tags, extension)
461472
if not artifacts:
@@ -464,13 +475,16 @@ def _package_from_releases(
464475
"hub" if "hub" in tags else "client"
465476
)
466477
)
467-
return None
478+
return None, None
468479
artifact = artifacts[-1]
469480
if remote_download:
470-
return artifact.url
481+
return artifact.url, artifact
471482
else:
472-
return download_package(
473-
artifact.url, checksum=artifact.checksum, insecure=insecure
483+
return (
484+
download_package(
485+
artifact.url, checksum=artifact.checksum, insecure=insecure
486+
),
487+
artifact,
474488
)
475489

476490

@@ -514,13 +528,103 @@ def get_package_from_host_info(
514528
tags.extend(tag for tag in package_tags if tag != "msi")
515529

516530
if packages is None: # No command line argument given
517-
package = _package_from_releases(
531+
package, artifact = _package_from_releases(
518532
tags, extension, version, edition, remote_download, insecure
519533
)
520534
else:
521-
package = _package_from_list(tags, extension, packages)
535+
package, artifact = _package_from_list(tags, extension, packages)
536+
537+
return package, artifact
538+
522539

523-
return package
540+
def _remote_download(
541+
host, package, artifact, avalaible_binaries, insecure=False, connection=None
542+
):
543+
"""
544+
Remotely download package on host from url.
545+
546+
This function checks for avalaible binaries, copies remote-download.sh to the host, then runs it to download the package from its url.
547+
The script ensures package integrity by comparing checksums.
548+
549+
:host str hostname
550+
:package str package url
551+
:artifact Artifact artifact corresponding to the package that is downloaded
552+
:avalaible_binaries Dict[str:str] dictionary containing of program_name:path on the host
553+
"""
554+
555+
if not avalaible_binaries:
556+
log.error("No binary could be found on host '{}'".format(host))
557+
return None
558+
559+
if "sha256sum" not in avalaible_binaries:
560+
if not insecure:
561+
log.error(
562+
"Cannot check file integrity. sha256sum is not installed on host '{}'. Run with --insecure to skip".format(
563+
host
564+
)
565+
)
566+
return None
567+
log.warning(
568+
"Cannot check file integrity. sha256sum is not installed on host '{}'. Continuing due to insecure flag".format(
569+
host
570+
)
571+
)
572+
573+
if not any(bin in avalaible_binaries for bin in ("wget", "curl")):
574+
log.error(
575+
"Cannot download remotely. wget and curl are not installed on host '{}'".format(
576+
host
577+
)
578+
)
579+
return None
580+
use_curl = "wget" not in avalaible_binaries
581+
582+
if not (artifact and artifact.checksum):
583+
if not insecure:
584+
log.error(
585+
"Cannot check file integrity on '{}'. No artifact associated with package '{}' found. Run with --insecure to skip".format(
586+
host, package
587+
)
588+
)
589+
return None
590+
log.warning(
591+
"Cannot check file integrity on '{}'. No artifact associated with package '{}' found. Continuing due to insecure flag".format(
592+
host, package
593+
)
594+
)
595+
596+
cf_remote_dir = dirname(__file__)
597+
script_path = join(cf_remote_dir, "remote-download.sh")
598+
if not exists(script_path):
599+
sys.exit("%s does not exist" % script_path)
600+
scp(
601+
script_path,
602+
host,
603+
connection,
604+
hide=True,
605+
)
606+
607+
args = ""
608+
if insecure:
609+
args += "-I "
610+
if use_curl:
611+
args += "-C "
612+
if artifact:
613+
args += "-c {} ".format(artifact.checksum)
614+
args += package
615+
616+
ret = ssh_cmd(connection, "bash remote-download.sh {}".format(args), errors=True)
617+
618+
if ret is None:
619+
return None
620+
if insecure:
621+
warning = re.search(r"Package.*", ret)
622+
if warning:
623+
log.warning(warning.group(0))
624+
625+
log.debug("Package '{}' downloaded successfully on host '{}'".format(package, host))
626+
627+
return basename(package)
524628

525629

526630
@auto_connect
@@ -552,9 +656,10 @@ def install_host(
552656
elif packages and len(packages) == 1:
553657
package = packages[0]
554658

659+
artifact = None
555660
if not package:
556661
try:
557-
package = get_package_from_host_info(
662+
package, artifact = get_package_from_host_info(
558663
data.get("package_tags"),
559664
data.get("bin"),
560665
data.get("arch"),
@@ -574,19 +679,16 @@ def install_host(
574679
return 1
575680

576681
if remote_download:
577-
if ("bin" not in data) or ("curl" not in data["bin"]):
578-
log.error(
579-
"Couldn't download remotely. Curl is not installed on host '%s'" % host
580-
)
581-
return 1
582-
583-
print("Downloading '%s' on '%s' using curl" % (package, host))
584-
r = ssh_cmd(
585-
cmd="curl --fail -O {}".format(package), connection=connection, errors=True
682+
package = _remote_download(
683+
host,
684+
package,
685+
artifact,
686+
data.get("bin"),
687+
connection=connection,
688+
insecure=insecure,
586689
)
587-
if r is None:
690+
if package is None:
588691
return 1
589-
package = basename(package)
590692
elif not getattr(connection, "is_local", False):
591693
scp(package, host, connection=connection)
592694
package = basename(package)

cf_remote/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ def parse_envfile(text):
314314
if not key:
315315
return error_and_none("Invalid env file format: Key missing")
316316

317-
if not re.fullmatch(r"([A-Z]+\_?)+", key):
317+
if not re.fullmatch(r"[A-Z][A-Z0-9]*(\_[A-Z0-9]+)*", key):
318318
return error_and_none("Invalid env file format: Invalid key")
319319

320320
if not (val.startswith('"') and val.endswith('"')):

0 commit comments

Comments
 (0)