6.2 KiB
Project Signing and Verification
Project signing and verification allows project maintainers to sign their project directory files with GPG and verify them at project-update time in AWX/Controller.
Signing
Signing is provided by a CLI tool and library called
ansible-sign which makes use of
python-gnupg to ultimately shell out to GPG to do signing. Currently the only
supported end-user use of this tool is as a CLI utility, but it does provide a
somewhat clean API as well for internal use, and we use this in our verification
process in AWX. More on that below.
ansible-sign expects a MANIFEST.in file (written in valid distlib.manifest
format familiar to most Python project maintainers) which lists the files that
should be included and excluded from the signing process.
Internally, there is a concept of "differs", and a differ is what allows us to
know if files have been added or removed along with which files we should care
about signing and verifying. Currently only one is shipped and supported and
that is the DistlibManifestChecksumFileExistenceDiffer which uses
distlib.manifest to allow our MANIFEST.in machinery to work.
At a broad implementation level, ansible-sign works like this when it is asked
to sign a project:
- First, it will ask
distlib.manifestto read in theMANIFEST.infile and resolve the entries in it to actual file paths. It processes the directives one line at a time. We skip lines starting with#along with blank lines. We also always implicitly includeMANIFEST.initself. - Once all of the
(recursive-)include-ed files are resolved, it will iterate through all of them and calculate sha256sums for all of them. (It does this by reading chunks of the file at a time, to avoid reading entire potentially large files into memory). - Now we have a dictionary of 'file path -> checksum', and we can write it out
to a file. We store the file in
.ansible-sign/sha256sum.txt. This is called the "checksum manifest file" and it has one line per file. It is in standard GNU Coreutilssha256sumformat. - Once the checksum manifest is written, we sign it. Signing is modular-ish
(like the "differ" concept) though only GPG is currently supported and
implemented. GPG signing uses the
GPGSignerclass which internally uses thepython-gnupglibrary, which itself shells out togpgto sign the file.GPGSignertakes parameters such as the passphrase, GnuPG home directory, private key to use, and so on. By defaultgpgwill use the first available private signing key found in the user's default keyring. It will write out the detached signature to.ansible-sign/sha256sum.txt.sig. - We do some sanity checking such as ensuring that we get a
0return code (indicating success) back fromgpg.
Verifying
On the AWX side, we have a GPG Public Key credential type that ships with
AWX. This credential type allows the user to paste in a public GPG key, which
should correspond to the private key used to sign the content. The validity and
"realness" of this key is not currently checked.
Once a GPG Public Key credential has been created, it can be attached to the
project (this is just a normal FK relationship). If the project has such a
credential associated with it, content verification will be enabled. Otherwise,
it will be skipped.
Project verification happens only during project update, not during Job
launch. There is an action plugin in
awx/playbooks/action_plugins/verify_project.py which uses ansible-sign as a
library for doing verification. The implementation is similar to the
ansible-sign project gpg-verify subcommand; they both use the same library
calls internally. If the API changes, both places will need to be updated.
Verifying reverts the general signing process described above:
- First we ensure a few files exist (the signature file, the manifest file, and
MANIFEST.in). - We once again use
python-gnupg(via ourGPGVerifierclass this time) and ask it to validate the detached signature. It will check it against keys in our public keyring unless we give it another keyring to use instead. (On the CLI we can do this with--keyring; on the AWX/Controller side, we get a fresh keyring every time the EE spawns, so we import the public key from the credential and just let it check against the default keyring). - Once the key is imported, we can use it to verify if the signature corresponds
to the checksum manifest.
gpgdoes this for us (we usepython-gnupg'sverify_filemethod), but effectively it is checking for:- Does the key match up with something known/trusted in our keyring?
- Does the signature correspond to the checksum manifest? (In other words, has the checksum manifest been modified?)
- After
gpgtells us everything is okay, the checksum manifest can then be used as a "source of truth" for everything else. Our next step is to parse checksum manifest file (this isChecksumFile#parse). We'll ultimately have a dictionary offile path -> checksumafter this. - We then call
ChecksumFile#verifywhich internally does a few things:- It will call the differ to parse
MANIFEST.inagain, viaChecksumFile#diff. We inject an implicitglobal-include *at the top so that we catch any files that have been added to the project as well. UltimatelyChecksumFile#diffwill call the differ'scompare_filelistmethod which takes a list of files (those listed in the checksum manifest parsed byChecksumFile#parsea few steps up) and compares them against all the files in the project (captured byglobal-include *). It returns a dict and groups the results intoaddedandremovedkeys. - Check the result from above. If there are any files listed in
addedorremoved, we throwChecksumMismatchand bail out early. - Otherwise, no files have been added or removed from the project. In this case, we can iterate all the files in the project and take a new checksum hash of all of them.
- Once we have those, compare those against the parsed manifest file's
checksums. If there are checksum mismatches, accumulate a list of them and
raise
ChecksumMismatch.
- It will call the differ to parse