TT Lab
Get started
Learn Learning paths Courses

Air-Gapped Mirrors and a Private CA

An intake bundle: from request to signature verification

Continue in TT Lab

Goal

Starting from an import request, download a bundle, sign a relative-path hash list, move it on media without the private key, and on the air-gapped side verify signature first and hash next, then install from the bundle alone and leave a record.

Why it matters

A USB drive that carries only a hash list catches damage in transit but not substitution — whoever changed the files just edits the list too. Only when you sign the list and hand over the public key in advance by a different route is "who made this list" guaranteed. And a verification script must exit with a nonzero value on failure for automation to stop. This Pod can reach the internet (80/443), so it plays the download side, and the installation steps are done from the bundle alone with the outside blocked by a closed proxy.

Steps

  1. In /root/intake/request.txt, write the import request as two lines of 생태계 이름 버전 (the fields are the ecosystem, the name, and the version): pypi requests 2.32.3 and go rsc.io/quote v1.5.2.
  2. Into the bundle /root/intake/bundle/, download the Python wheels (pypi/, dependencies included, wheels only) and the Go module proxy (goproxy/, the contents of cache/download of a new module cache).
  3. Inside the bundle directory, create /root/intake/bundle/SHA256SUMS with relative paths (leave out the list and the signature file itself).
  4. Create /root/intake/keys/intake.key (an Ed25519 private key) and intake.pub, sign SHA256SUMS, and put the signature in /root/intake/bundle/SHA256SUMS.sig.
  5. Create the media /root/intake/media/bundle.tar (without the private key), unpack it on the air-gapped side at /root/intake/inside/, and place the public key separately as /root/intake/inside/trusted.pub.
  6. Write the air-gapped-side verification script /root/intake/verify.sh <번들> <공개키> (the placeholders are the bundle and the public key). It must check signature first, then hash, and also files that are not in the list, and it must exit with a nonzero value on failure.
  7. With the outside blocked, install from /root/intake/inside/bundle alone: requests into the venv /root/intake/venv, and the Go program at /root/intake/goapp (printing the quote.Hello() of rsc.io/quote), built into /root/intake/bin/hello.
  8. Leave the import record /root/intake/record.json (keys: request, files, sha256sums_sha256, signer_pub_sha256, verified, installed).

Notes

Write the import request with exact versions

In /root/intake/request.txt, write two lines: pypi requests 2.32.3 and go rsc.io/quote v1.5.2.

Each line has three fields: ecosystem, name, and version. If you use a range (>=) or latest, the result differs by the day of the download. The version of a Go module starts with v.

Download the bundle as requested

Download the requests 2.32.3 and dependency wheels into /root/intake/bundle/pypi/, and the module proxy contents of rsc.io/quote v1.5.2 into /root/intake/bundle/goproxy/.

For pip, use download together with the option that makes it fetch only wheels; for Go, download once into a new module cache (GOMODCACHE) and then move its cache/download. Go needs a small module in which the module to be downloaded is used — create now the /root/intake/goapp that you will use in step 7.

A relative-path hash list

Inside the bundle directory, create /root/intake/bundle/SHA256SUMS with relative paths (leave out the list and the signature file itself).

If you enter the bundle directory, gather files with find, and pass them to sha256sum, the paths start with ./. Exclude the list file by name so that it does not include itself. After creating it, check with -c in the same directory.

Sign the list

Create /root/intake/keys/intake.key (Ed25519) and intake.pub, and put the signature of SHA256SUMS in /root/intake/bundle/SHA256SUMS.sig.

Create an ed25519 key with OpenSSL 3's genpkey and extract the public key with pkey. Ed25519 takes the original data as it is, so pkeyutl needs -rawin. Keep the private key outside the bundle (keys/).

Media without the private key, and the public key handed over separately

Create /root/intake/media/bundle.tar and unpack it at /root/intake/inside/, and place the public key separately as /root/intake/inside/trusted.pub.

The tar holds only the bundle directory. Make inside/bundle appear when you unpack it. If the public key is inside the media, whoever does the substitution simply puts in their own key, so the key to be trusted must already be in place by another route.

Signature first, hash next — the verification script

Write /root/intake/verify.sh <번들> <공개키> (the placeholders are the bundle and the public key). It checks in the order signature → hash → files not in the list, and exits with a nonzero value on failure.

The grader runs your script on two copies of the bundle (one with a single byte of a file changed, and one with the list also fixed to match that file) and checks that it rejects both. The second cannot be stopped by a script that looks only at hashes.

Install from the bundle alone

With the outside blocked, install from /root/intake/inside/bundle alone: requests into the venv /root/intake/venv, and build /root/intake/goapp into /root/intake/bin/hello.

Verify first with the step 6 script before installing. Make pip look only at the bundle directory instead of an index, and for Go, point at the bundle's goproxy directory as a file:// proxy and build with a new module cache.

Leave the import record

In /root/intake/record.json, write request (the list of request lines), files (the number of files in the bundle list), sha256sums_sha256, signer_pub_sha256 (the sha256 of the trusted.pub file), verified (true), and installed (the list of what was installed).

Do not copy the values by hand; compute them from the files. files is the number of lines in SHA256SUMS. In installed, write what was actually used for installation and the build, such as requests and the Go module, in the form name==version or module@version.