--- # 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/, 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