diff --git a/.github/workflows/actions/deploy-terraform-infrastructure/action.yml b/.github/workflows/actions/deploy-terraform-infrastructure/action.yml index fe3153daab5..25609913ca0 100644 --- a/.github/workflows/actions/deploy-terraform-infrastructure/action.yml +++ b/.github/workflows/actions/deploy-terraform-infrastructure/action.yml @@ -102,10 +102,14 @@ inputs: description: Maximum disk space used by the IPFS node datastore (experimental). required: false default: "50GB" + mithril_ipfs_ipns_signing_key: + description: IPNS signing key in PEM PKCS#8 format used by the IPFS node to publish the immutable archives directory (optional, the aggregator does not publish to IPNS if empty, experimental). + required: false + default: "" ipfs_node_version: description: IPFS node version (experimental). required: false - default: "v0.38.1" + default: "v0.43.1" ipfs_node_docker_registry: description: IPFS node Docker image registry (experimental). required: false @@ -282,6 +286,9 @@ runs: MITHRIL_AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS_BASE64=$(echo '${{ inputs.mithril_aggregator_ancillary_signer_gcp_kms_credentials }}' | base64 -w 0) echo "::add-mask::$MITHRIL_AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS_BASE64" echo "mithril_aggregator_ancillary_signer_gcp_kms_credentials_base64=$MITHRIL_AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS_BASE64" >> $GITHUB_OUTPUT + MITHRIL_IPFS_IPNS_SIGNING_KEY_BASE64=$(printf '%s' '${{ inputs.mithril_ipfs_ipns_signing_key }}' | base64 -w 0) + echo "::add-mask::$MITHRIL_IPFS_IPNS_SIGNING_KEY_BASE64" + echo "mithril_ipfs_ipns_signing_key_base64=$MITHRIL_IPFS_IPNS_SIGNING_KEY_BASE64" >> $GITHUB_OUTPUT - name: Prepare terraform variables shell: bash @@ -316,6 +323,7 @@ runs: mithril_p2p_signer_relay_signature_registration_mode = "${{ inputs.mithril_p2p_signer_relay_signature_registration_mode }}" mithril_ipfs_enabled = "${{ inputs.mithril_ipfs_enabled }}" mithril_ipfs_storage_max = "${{ inputs.mithril_ipfs_storage_max }}" + mithril_ipfs_ipns_signing_key = "${{ steps.prepare.outputs.mithril_ipfs_ipns_signing_key_base64 }}" mithril_api_domain = "${{ inputs.mithril_api_domain }}" mithril_image_id = "${{ inputs.mithril_image_id }}" mithril_genesis_verification_key_url = "${{ inputs.mithril_genesis_verification_key_url }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f46cf2393f..96b764b51f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -849,6 +849,7 @@ jobs: mithril_aggregator_ancillary_signer_secret_key: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_SECRET_KEY }} mithril_aggregator_ancillary_signer_gcp_kms_resource_name: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_RESOURCE_NAME }} mithril_aggregator_ancillary_signer_gcp_kms_credentials: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS }} + mithril_ipfs_ipns_signing_key: ${{ secrets.IPFS_IPNS_SIGNING_KEY }} mithril_aggregator_custom_origin_tag_white_list: ${{ vars.AGGREGATOR_CUSTOM_ORIGIN_TAG_WHITE_LIST }} prometheus_auth_username: ${{ secrets.PROMETHEUS_AUTH_USERNAME }} prometheus_auth_password: ${{ secrets.PROMETHEUS_AUTH_PASSWORD }} diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 226b607eb3d..9c99e1ad166 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -305,6 +305,7 @@ jobs: mithril_aggregator_ancillary_signer_secret_key: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_SECRET_KEY }} mithril_aggregator_ancillary_signer_gcp_kms_resource_name: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_RESOURCE_NAME }} mithril_aggregator_ancillary_signer_gcp_kms_credentials: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS }} + mithril_ipfs_ipns_signing_key: ${{ secrets.IPFS_IPNS_SIGNING_KEY }} mithril_aggregator_custom_origin_tag_white_list: ${{ vars.AGGREGATOR_CUSTOM_ORIGIN_TAG_WHITE_LIST }} prometheus_auth_username: ${{ secrets.PROMETHEUS_AUTH_USERNAME }} prometheus_auth_password: ${{ secrets.PROMETHEUS_AUTH_PASSWORD }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76e8c124d9a..5e763f3d5b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,6 +221,7 @@ jobs: mithril_aggregator_ancillary_signer_secret_key: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_SECRET_KEY }} mithril_aggregator_ancillary_signer_gcp_kms_resource_name: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_RESOURCE_NAME }} mithril_aggregator_ancillary_signer_gcp_kms_credentials: ${{ secrets.AGGREGATOR_ANCILLARY_SIGNER_GCP_KMS_CREDENTIALS }} + mithril_ipfs_ipns_signing_key: ${{ secrets.IPFS_IPNS_SIGNING_KEY }} mithril_aggregator_custom_origin_tag_white_list: ${{ vars.AGGREGATOR_CUSTOM_ORIGIN_TAG_WHITE_LIST }} prometheus_auth_username: ${{ secrets.PROMETHEUS_AUTH_USERNAME }} prometheus_auth_password: ${{ secrets.PROMETHEUS_AUTH_PASSWORD }} diff --git a/.github/workflows/test-deploy-network.yml b/.github/workflows/test-deploy-network.yml index 502a225fa0f..42347b8a737 100644 --- a/.github/workflows/test-deploy-network.yml +++ b/.github/workflows/test-deploy-network.yml @@ -150,6 +150,7 @@ jobs: mithril_p2p_network_bootstrap_peer: ${{ matrix.mithril_p2p_network_bootstrap_peer }} mithril_ipfs_enabled: ${{ matrix.mithril_ipfs_enabled }} mithril_ipfs_storage_max: ${{ vars.IPFS_STORAGE_MAX }} + mithril_ipfs_ipns_signing_key: ${{ secrets.IPFS_IPNS_SIGNING_KEY }} ipfs_node_version: ${{ vars.IPFS_NODE_VERSION }} ipfs_node_docker_registry: ${{ vars.IPFS_NODE_DOCKER_REGISTRY }} mithril_api_domain: ${{ matrix.mithril_api_domain }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c131591c1..b55dcbaa6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ As a minor extension, we have adopted a slightly different versioning convention - Reworked the Mithril aggregator's file archiver to produce byte-stable archives across systems. - Existing archives must be regenerated by the Mithril aggregator to ensure byte stability. - Preliminary support for uploading immutable files to IPFS with the Mithril aggregator and downloading them from IPFS with the Mithril client library and CLI. + - Support for the IPNS signing key of the IPFS node of the Mithril aggregator in the infrastructure, and upgrade of the default Kubo version to `v0.43.1`. - Support for the circuit verification key registry, a genesis-signed whitelist (with revocations) of the circuit verification keys trusted for SNARK certificates. - The registry lives in the new `mithril-circuit-key-registry` crate, holding one entry per circuit verification key, either allowed over an epoch range or revoked for every epoch. - The registry is retrieved over HTTPS and the last verified registry is kept when a refresh fails. diff --git a/docs/runbook/README.md b/docs/runbook/README.md index b27bad2cb82..d1b00d4f01f 100644 --- a/docs/runbook/README.md +++ b/docs/runbook/README.md @@ -31,3 +31,4 @@ This page gathers the available guides to operate a Mithril network. | **Update circuits verification keys** | [update-circuit-keys](./update-circuit-keys/README.md) | Update the circuits verification keys after an intentional circuit modification. | | **GitHub self-hosted runner** | [self-hosted-runner](./self-hosted-runner/README.md) | Set up a GCP virtual machine as a GitHub self-hosted runner for heavy workloads. | | **Circuit verification key registry** | [circuit-key-registry](./circuit-key-registry/README.md) | Publish, rotate and revoke circuit verification keys in the genesis-signed registry. | +| **IPNS signing key** | [ipns-signing-key](./ipns-signing-key/README.md) | Create, register and rotate the IPNS signing key of the aggregator IPFS node. | diff --git a/docs/runbook/ipns-signing-key/README.md b/docs/runbook/ipns-signing-key/README.md new file mode 100644 index 00000000000..9f612751ac3 --- /dev/null +++ b/docs/runbook/ipns-signing-key/README.md @@ -0,0 +1,76 @@ +# IPNS signing key + +## Introduction + +When IPFS is enabled on a Mithril network, the aggregator publishes the IPFS directory of the immutable archives under an IPNS name. This IPNS name is derived from an Ed25519 signing key that the IPFS node of the aggregator imports at startup. + +The signing key is provided to the deployment by the `IPFS_IPNS_SIGNING_KEY` secret of the GitHub environment of the network, in PEM PKCS#8 format. This secret is optional (e.g. for a follower aggregator): the aggregator does not publish to IPNS when it is empty. + +> [!IMPORTANT] +> :fire: The IPNS signing key is a private key: anyone who holds it can publish content under the IPNS name of the network. It must not be stored anywhere other than the GitHub environment secret and the IPFS node of the aggregator. + +All the commands below are run locally and require [Docker](https://docs.docker.com/get-docker/). + +## Pre-requisites + +Export the environment variables: + +```bash +export IPFS_IMAGE=ipfs/kubo:v0.43.1 +export KEY_DIRECTORY=$(mktemp -d) +``` + +## Create a signing key + +Generate a new Ed25519 signing key and export it in PEM PKCS#8 format: + +```bash +docker run --rm -u $(id -u):$(id -g) -e IPFS_PATH=/tmp/ipfs -v $KEY_DIRECTORY:/keys --entrypoint sh $IPFS_IMAGE -c ' +ipfs init > /dev/null +ipfs key gen --type=ed25519 ipns-signing-key > /dev/null +ipfs key export ipns-signing-key --format=pem-pkcs8-cleartext -o /keys/ipns-signing-key.pem +' +``` + +Display the signing key: + +```bash +cat $KEY_DIRECTORY/ipns-signing-key.pem +``` + +## Compute the IPNS name + +Compute the IPNS name associated to the signing key: + +```bash +docker run --rm -u $(id -u):$(id -g) -e IPFS_PATH=/tmp/ipfs -v $KEY_DIRECTORY:/keys:ro --entrypoint sh $IPFS_IMAGE -c ' +ipfs init > /dev/null +ipfs key import --format=pem-pkcs8-cleartext ipns-signing-key /keys/ipns-signing-key.pem +' +``` + +The command outputs the IPNS name, and the immutable archives directory is then available at `/ipns/`. + +> [!TIP] +> The same command computes the IPNS name of an existing signing key, provided it is first stored in `$KEY_DIRECTORY/ipns-signing-key.pem`. + +## Register the signing key + +Copy the full content of `$KEY_DIRECTORY/ipns-signing-key.pem` (including the `BEGIN` and `END` lines) into the `IPFS_IPNS_SIGNING_KEY` secret of the GitHub environment of the network. + +The key is imported by the IPFS node at the next deployment of the network. + +Then delete the local copy of the signing key: + +```bash +rm -rf $KEY_DIRECTORY +``` + +## Rotate the signing key + +The IPFS node replaces its signing key with the one of the deployment at each start. + +> [!WARNING] +> Rotating the signing key changes the IPNS name of the network. The third-party nodes that follow the previous IPNS name must be informed of the new one before the rotation is deployed. This coordination is planned to be handled with DNSLink, which is not implemented yet. + +To rotate the signing key, create a new signing key, compute its IPNS name and register it as described above. diff --git a/mithril-infra/assets/docker/docker-compose-aggregator-ipfs-override.yaml b/mithril-infra/assets/docker/docker-compose-aggregator-ipfs-override.yaml index 9fb852b46ef..f09bb5deaf6 100644 --- a/mithril-infra/assets/docker/docker-compose-aggregator-ipfs-override.yaml +++ b/mithril-infra/assets/docker/docker-compose-aggregator-ipfs-override.yaml @@ -16,6 +16,8 @@ services: - IPFS_SWARM_PORT=${AGGREGATOR_IPFS_SWARM_PORT} - IPFS_PUBLIC_ADDRESS=${AGGREGATOR_IPFS_PUBLIC_ADDRESS} - IPFS_STORAGE_MAX=${AGGREGATOR_IPFS_STORAGE_MAX} + - IPFS_IPNS_SIGNING_KEY_NAME=${AGGREGATOR_IPFS_IPNS_SIGNING_KEY_NAME} + - IPFS_IPNS_SIGNING_KEY=${AGGREGATOR_IPFS_IPNS_SIGNING_KEY} volumes: - ../data/${NETWORK}/mithril-aggregator/ipfs:/data/ipfs - ./ipfs/container-init.d:/container-init.d:ro diff --git a/mithril-infra/assets/docker/ipfs/container-init.d/configure-node.sh b/mithril-infra/assets/docker/ipfs/container-init.d/configure-node.sh index ade0d402b67..dc05a2bed46 100755 --- a/mithril-infra/assets/docker/ipfs/container-init.d/configure-node.sh +++ b/mithril-infra/assets/docker/ipfs/container-init.d/configure-node.sh @@ -34,3 +34,18 @@ ipfs config --json Swarm.ResourceMgr.MaxFileDescriptors 1024 ipfs config Datastore.StorageMax "${IPFS_STORAGE_MAX}" ipfs config --json Experimental.FilestoreEnabled true ipfs config Plugins.Plugins.telemetry.Config.Mode off + +# The IPNS signing key is replaced at each start so that a rotated key from the deployment is applied +if [ -n "${IPFS_IPNS_SIGNING_KEY:-}" ]; then + if [ -z "${IPFS_IPNS_SIGNING_KEY_NAME:-}" ]; then + echo "configure-node: IPFS_IPNS_SIGNING_KEY_NAME is not set" >&2 + exit 1 + fi + if ipfs key list | grep -qx "${IPFS_IPNS_SIGNING_KEY_NAME}"; then + ipfs key rm "${IPFS_IPNS_SIGNING_KEY_NAME}" > /dev/null + fi + ipns_key_file=$(mktemp) + trap 'rm -f "$ipns_key_file"' EXIT + printf '%s\n' "${IPFS_IPNS_SIGNING_KEY}" > "$ipns_key_file" + ipfs key import --format=pem-pkcs8-cleartext "${IPFS_IPNS_SIGNING_KEY_NAME}" "$ipns_key_file" > /dev/null +fi diff --git a/mithril-infra/assets/infra.version b/mithril-infra/assets/infra.version index 10c87c552e6..dc7c331d3fa 100644 --- a/mithril-infra/assets/infra.version +++ b/mithril-infra/assets/infra.version @@ -1,2 +1,2 @@ -0.5.28 +0.5.29 diff --git a/mithril-infra/mithril.aggregator.tf b/mithril-infra/mithril.aggregator.tf index 922ccb47f78..7c763d7b0e0 100644 --- a/mithril-infra/mithril.aggregator.tf +++ b/mithril-infra/mithril.aggregator.tf @@ -3,6 +3,7 @@ locals { mithril_aggregator_dmq_port = 6161 mithril_aggregator_ipfs_swarm_port = 4001 mithril_aggregator_ipfs_rpc_api_url = "http://ipfs-node-aggregator:5001/" + mithril_aggregator_ipfs_ipns_signing_key = base64decode(var.mithril_ipfs_ipns_signing_key) mithril_aggregator_ancillary_signer_gcp_kms_credentials = base64decode(var.mithril_aggregator_ancillary_signer_gcp_kms_credentials) } @@ -34,6 +35,8 @@ resource "null_resource" "mithril_aggregator" { mithril_p2p_dmq_dense_topology = var.mithril_p2p_dmq_dense_topology, mithril_ipfs_enabled = var.mithril_ipfs_enabled, mithril_ipfs_storage_max = var.mithril_ipfs_storage_max, + mithril_ipfs_ipns_signing_key = sha256(var.mithril_ipfs_ipns_signing_key), + mithril_ipfs_ipns_signing_key_name = var.mithril_ipfs_ipns_signing_key_name, ipfs_image_id = var.ipfs_image_id, ipfs_image_registry = var.ipfs_image_registry, } @@ -191,7 +194,7 @@ EOT , <<-EOT if [ "${var.mithril_ipfs_enabled}" = "true" ]; then - export IPFS_RPC_SERVER_CONFIG=$(jq -nc --arg url '${local.mithril_aggregator_ipfs_rpc_api_url}' '{"url": $url}') + export IPFS_RPC_SERVER_CONFIG=$(jq -nc --arg url '${local.mithril_aggregator_ipfs_rpc_api_url}' --arg ipns_key_name '${var.mithril_ipfs_ipns_signing_key_name}' --arg ipns_enabled '${var.mithril_ipfs_ipns_signing_key != ""}' '{"url": $url} + if $ipns_enabled == "true" then {"ipns_key_name": $ipns_key_name} else {} end') fi EOT , @@ -220,6 +223,8 @@ EOT "export AGGREGATOR_IPFS_SWARM_PORT='${local.mithril_aggregator_ipfs_swarm_port}'", "export AGGREGATOR_IPFS_PUBLIC_ADDRESS='${google_compute_address.mithril-external-address.address}'", "export AGGREGATOR_IPFS_STORAGE_MAX='${var.mithril_ipfs_storage_max}'", + "export AGGREGATOR_IPFS_IPNS_SIGNING_KEY_NAME='${var.mithril_ipfs_ipns_signing_key_name}'", + "export AGGREGATOR_IPFS_IPNS_SIGNING_KEY='${local.mithril_aggregator_ipfs_ipns_signing_key}'", "export P2P_BOOTSTRAP_PEER='${var.mithril_p2p_network_bootstrap_peer}'", "export ENABLE_METRICS_SERVER=true", "export METRICS_SERVER_IP=0.0.0.0", diff --git a/mithril-infra/variables.tf b/mithril-infra/variables.tf index b1ae67daf82..bd22a84e439 100644 --- a/mithril-infra/variables.tf +++ b/mithril-infra/variables.tf @@ -434,10 +434,23 @@ variable "mithril_ipfs_storage_max" { default = "50GB" } +variable "mithril_ipfs_ipns_signing_key" { + type = string + description = "The IPNS signing key in PEM PKCS#8 format base64 encoded, imported by the IPFS node to publish the immutable archives directory (optional, the aggregator does not publish to IPNS if empty)" + sensitive = true + default = "" +} + +variable "mithril_ipfs_ipns_signing_key_name" { + type = string + description = "The name under which the IPNS signing key is imported by the IPFS node and used by the aggregator" + default = "mithril-snapshot" +} + variable "ipfs_image_id" { type = string description = "The IPFS image tag of service to deploy" - default = "v0.38.1" + default = "v0.43.1" } variable "ipfs_image_registry" {