233 lines
8.1 KiB
YAML
233 lines
8.1 KiB
YAML
|
|
---
|
||
|
|
# Install monerod from the published release, and prove the bytes are the ones
|
||
|
|
# binaryFate signed before they are allowed anywhere near /usr/local/bin.
|
||
|
|
#
|
||
|
|
# These tasks are the security control of this role. Do not "simplify" them.
|
||
|
|
#
|
||
|
|
# Why the published binary and not a build from source: the reproducible-build
|
||
|
|
# system that yields a byte-identical tarball (contrib/guix) exists only on
|
||
|
|
# monero master. The v0.18.x release branch still uses Gitian, which wants an
|
||
|
|
# Ubuntu 18.04 host and Python 2. A plain `make release-static` is not
|
||
|
|
# reproducible, so it would give us a self-built binary whose hash matches
|
||
|
|
# nothing - neither upstream's nor anyone else's. The signature chain below is
|
||
|
|
# the strongest claim actually available for this release. See README.md.
|
||
|
|
|
||
|
|
- name: Install the tools the verification chain needs
|
||
|
|
apt:
|
||
|
|
name:
|
||
|
|
- gnupg
|
||
|
|
- curl
|
||
|
|
- bzip2
|
||
|
|
state: present
|
||
|
|
update_cache: yes
|
||
|
|
|
||
|
|
- name: Check whether monerod is already installed
|
||
|
|
ansible.builtin.stat:
|
||
|
|
path: "{{ monero_bin_dir }}/monerod"
|
||
|
|
register: monero_binary
|
||
|
|
|
||
|
|
- name: Read the installed monerod version
|
||
|
|
ansible.builtin.command: "{{ monero_bin_dir }}/monerod --version"
|
||
|
|
register: monero_installed_version
|
||
|
|
changed_when: false
|
||
|
|
failed_when: false
|
||
|
|
check_mode: false
|
||
|
|
when: monero_binary.stat.exists
|
||
|
|
|
||
|
|
- name: Decide whether monerod needs installing
|
||
|
|
ansible.builtin.set_fact:
|
||
|
|
monero_needs_install: >-
|
||
|
|
{{ not monero_binary.stat.exists
|
||
|
|
or monero_version not in (monero_installed_version.stdout | default('')) }}
|
||
|
|
|
||
|
|
# Everything below runs only for a fresh install or a version bump.
|
||
|
|
|
||
|
|
# A throwaway keyring. The verification must not depend on, or add to, whatever
|
||
|
|
# happens to be in root's keyring already.
|
||
|
|
- name: Create a temporary GnuPG home for the verification
|
||
|
|
ansible.builtin.tempfile:
|
||
|
|
state: directory
|
||
|
|
suffix: monero-gpg
|
||
|
|
register: monero_gnupghome
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Copy binaryFate's signing key to the host
|
||
|
|
ansible.builtin.copy:
|
||
|
|
src: binaryfate.asc
|
||
|
|
dest: "{{ monero_gnupghome.path }}/binaryfate.asc"
|
||
|
|
mode: '0644'
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Import binaryFate's signing key
|
||
|
|
ansible.builtin.command: "gpg --batch --import {{ monero_gnupghome.path }}/binaryfate.asc"
|
||
|
|
environment:
|
||
|
|
GNUPGHOME: "{{ monero_gnupghome.path }}"
|
||
|
|
register: monero_key_import
|
||
|
|
changed_when: "'imported' in monero_key_import.stderr"
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Read the fingerprints of the imported key
|
||
|
|
ansible.builtin.shell: >-
|
||
|
|
set -o pipefail &&
|
||
|
|
gpg --batch --with-colons --fingerprint | awk -F: '/^fpr:/{print $10}'
|
||
|
|
args:
|
||
|
|
executable: /bin/bash
|
||
|
|
environment:
|
||
|
|
GNUPGHOME: "{{ monero_gnupghome.path }}"
|
||
|
|
register: monero_key_fingerprints
|
||
|
|
changed_when: false
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# The vendored file could be swapped in a bad pull request. The fingerprint is
|
||
|
|
# the thing being trusted, so it is checked, not assumed.
|
||
|
|
- name: Assert the imported key is binaryFate's
|
||
|
|
ansible.builtin.assert:
|
||
|
|
that:
|
||
|
|
- monero_signing_fingerprint in monero_key_fingerprints.stdout_lines
|
||
|
|
quiet: true
|
||
|
|
fail_msg: >-
|
||
|
|
files/binaryfate.asc does not contain the expected key
|
||
|
|
{{ monero_signing_fingerprint }}. Refusing to go on.
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Download the signed hashes.txt
|
||
|
|
ansible.builtin.get_url:
|
||
|
|
url: "{{ monero_hashes_url }}"
|
||
|
|
dest: "{{ monero_gnupghome.path }}/hashes.txt"
|
||
|
|
mode: '0644'
|
||
|
|
force: yes
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# One command that both verifies the signature and writes out ONLY the verified
|
||
|
|
# plaintext. Grepping the clearsigned file directly would search text that has
|
||
|
|
# not been through the signature check.
|
||
|
|
- name: Verify hashes.txt and extract the verified plaintext
|
||
|
|
ansible.builtin.command: >-
|
||
|
|
gpg --batch --status-fd 2
|
||
|
|
--output {{ monero_gnupghome.path }}/hashes-verified.txt
|
||
|
|
--decrypt {{ monero_gnupghome.path }}/hashes.txt
|
||
|
|
environment:
|
||
|
|
GNUPGHOME: "{{ monero_gnupghome.path }}"
|
||
|
|
register: monero_hashes_verify
|
||
|
|
changed_when: false
|
||
|
|
failed_when: false
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# Greping for "Good signature" - which is what the bitcoin_knots role does -
|
||
|
|
# passes for ANY key in the keyring. VALIDSIG names the key, and the line
|
||
|
|
# carries both the signing subkey and the primary key fingerprint, so checking
|
||
|
|
# that our fingerprint appears on it survives a subkey rotation.
|
||
|
|
- name: Assert hashes.txt carries a valid signature from binaryFate
|
||
|
|
ansible.builtin.assert:
|
||
|
|
that:
|
||
|
|
- monero_hashes_verify.rc == 0
|
||
|
|
- monero_hashes_verify.stderr is search('VALIDSIG[^\n]*' ~ monero_signing_fingerprint)
|
||
|
|
quiet: true
|
||
|
|
fail_msg: >-
|
||
|
|
hashes.txt did not verify against {{ monero_signing_fingerprint }}.
|
||
|
|
This is the whole security control of this role - aborting.
|
||
|
|
gpg said: {{ monero_hashes_verify.stderr | default('') }}
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Download the monerod release tarball
|
||
|
|
ansible.builtin.get_url:
|
||
|
|
url: "{{ monero_url }}"
|
||
|
|
dest: "/tmp/{{ monero_tarball }}"
|
||
|
|
mode: '0644'
|
||
|
|
checksum: "sha256:{{ monero_sha256 }}"
|
||
|
|
force: yes
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Compute the SHA256 of the downloaded tarball
|
||
|
|
ansible.builtin.stat:
|
||
|
|
path: "/tmp/{{ monero_tarball }}"
|
||
|
|
checksum_algorithm: sha256
|
||
|
|
get_checksum: yes
|
||
|
|
register: monero_tarball_stat
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# The check that actually ties these bytes to binaryFate's signature. The
|
||
|
|
# get_url checksum above only ties them to a string in our own repo; this ties
|
||
|
|
# them to the signed list. Both the hash AND the filename must be on one line,
|
||
|
|
# so a hash belonging to a different artefact cannot satisfy it.
|
||
|
|
- name: Look up the downloaded tarball's checksum in the signed hashes
|
||
|
|
ansible.builtin.command:
|
||
|
|
argv:
|
||
|
|
- grep
|
||
|
|
- -F
|
||
|
|
- -x
|
||
|
|
- "{{ monero_tarball_stat.stat.checksum }} {{ monero_tarball }}"
|
||
|
|
- "{{ monero_gnupghome.path }}/hashes-verified.txt"
|
||
|
|
register: monero_hash_match
|
||
|
|
changed_when: false
|
||
|
|
failed_when: false
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Assert the tarball is the one binaryFate signed
|
||
|
|
ansible.builtin.assert:
|
||
|
|
that:
|
||
|
|
- monero_hash_match.rc == 0
|
||
|
|
quiet: true
|
||
|
|
fail_msg: >-
|
||
|
|
{{ monero_tarball }} hashes to {{ monero_tarball_stat.stat.checksum }},
|
||
|
|
which is not the value signed for that filename in hashes.txt.
|
||
|
|
Do not install this file.
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Create a temporary extraction directory
|
||
|
|
ansible.builtin.tempfile:
|
||
|
|
state: directory
|
||
|
|
suffix: monero-extract
|
||
|
|
register: monero_extract_dir
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Extract the monerod tarball
|
||
|
|
ansible.builtin.unarchive:
|
||
|
|
src: "/tmp/{{ monero_tarball }}"
|
||
|
|
dest: "{{ monero_extract_dir.path }}"
|
||
|
|
remote_src: yes
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# By find rather than by a hardcoded path: the tarball unpacks to
|
||
|
|
# monero-x86_64-linux-gnu-v<version>/, which is not the name of the tarball.
|
||
|
|
- name: Locate the extracted monerod binary
|
||
|
|
ansible.builtin.find:
|
||
|
|
paths: "{{ monero_extract_dir.path }}"
|
||
|
|
patterns: monerod
|
||
|
|
recurse: yes
|
||
|
|
file_type: file
|
||
|
|
register: monero_extracted
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
- name: Assert the archive contained monerod
|
||
|
|
ansible.builtin.assert:
|
||
|
|
that:
|
||
|
|
- monero_extracted.files | length > 0
|
||
|
|
quiet: true
|
||
|
|
fail_msg: "No monerod binary in {{ monero_tarball }}"
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
|
||
|
|
# Only monerod. The wallet and blockchain utilities are in the same tarball if
|
||
|
|
# they are ever wanted; a node does not need them and every binary installed is
|
||
|
|
# surface that has to be kept current.
|
||
|
|
- name: Install monerod
|
||
|
|
ansible.builtin.copy:
|
||
|
|
src: "{{ monero_extracted.files[0].path }}"
|
||
|
|
dest: "{{ monero_bin_dir }}/monerod"
|
||
|
|
remote_src: yes
|
||
|
|
owner: root
|
||
|
|
group: root
|
||
|
|
mode: '0755'
|
||
|
|
when: monero_needs_install | bool
|
||
|
|
notify: Restart monerod
|
||
|
|
|
||
|
|
- name: Clean up the download and verification artefacts
|
||
|
|
ansible.builtin.file:
|
||
|
|
path: "{{ item }}"
|
||
|
|
state: absent
|
||
|
|
loop:
|
||
|
|
- "/tmp/{{ monero_tarball }}"
|
||
|
|
- "{{ monero_extract_dir.path | default('/nonexistent') }}"
|
||
|
|
- "{{ monero_gnupghome.path | default('/nonexistent') }}"
|
||
|
|
when: monero_needs_install | bool
|