Skip to content

Support cosign container signing

The official Home Assistant builder supports both singing the images it generates, as well as checking the signature of base images.

This is configured in the build configuration file using:

Key Description
codenotary Enable container signature with codenotary CAS.
codenotary.signer Owner signer E-Mail address for this image.
codenotary.base_image Verify the base container image. If you use our official images, use notary@home-assistant.io

From: https://developers.home-assistant.io/docs/add-ons/configuration/#add-on-extended-build

It seems the official documentation is outdated or wrong, as the build.sh script looks for information under the cosign key, not the codenotary key:

    # Read build.json / cosign
    if bashio::fs.file_exists "/tmp/build_config/build.json"; then
        cosign_base_identity="$(jq --raw-output '.cosign.base_identity // empty' "/tmp/build_config/build.json")"
        cosign_base_issuer="$(jq --raw-output '.cosign.base_issuer // "https://token.actions.githubusercontent.com"' "/tmp/build_config/build.json")"
        cosign_identity="$(jq --raw-output '.cosign.identity // empty' "/tmp/build_config/build.json")"
        cosign_issuer="$(jq --raw-output '.cosign.issuer // "https://token.actions.githubusercontent.com"' "/tmp/build_config/build.json")"
    fi
...
cosign_verify "${cosign_issuer}" "${cosign_identity}" "${repository}/${image}:${cache_tag}" "${docker_platform}" "false"
...
cosign_verify "${cosign_base_issuer}" "${cosign_base_identity}" "${build_from}" "${docker_platform}" "true"
...
            image_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "${repository}/${image}:${version}")
            cosign_sign "${image_digest}"
...

function cosign_sign() {
    local image=$1

    local success=false

    for j in {1..6}; do
        if cosign sign --yes "${image}"; then
            success=true
            break
        fi
        sleep $((5 * j))
    done

    if bashio::var.false "${success}"; then
        bashio::exit.nok "Failed to sign the image with cosign"
    fi
    bashio::log.info "Signed ${image} with cosign"
}

function cosign_verify() {
    local issuer=$1
    local identity=$2
    local image=$3
    local platform=$4
    local pull=$5

    local success=false

    if bashio::var.false "${COSIGN_VERIFY}"; then
        bashio::log.warning "Validation of ${image} signature is disabled"
        return 0
    fi

    # Support scratch image
    if [ "$image" == "scratch" ]; then
        bashio::log.info "Scratch image, skiping validation with cosign"
        return 0
    fi

    # Nothing to validate against
    if ! bashio::var.has_value "${issuer}" || ! bashio::var.has_value "${identity}" ; then
        return 0
    fi

    # Pull image if needed
    if bashio::var.true "${pull}"; then
        bashio::log.info "Download image ${image} for cosign validation"
        docker pull "${image}" --platform "${platform}" > /dev/null 2>&1 || bashio::exit.nok "Can't pull image ${image}"
    fi

    # validate image
    for j in {1..6}; do
        if cosign verify --certificate-oidc-issuer-regexp "${issuer}" --certificate-identity-regexp "${identity}" "${image}"; then
            success=true
            break
        fi
        sleep $((5 * j))
    done

    if bashio::var.false "${success}"; then
        bashio::log.warning "Validation of ${image} fails with cosign!"
        if bashio::var.true "${pull}"; then
            docker rmi "${image}" > /dev/null 2>&1 || true
        fi
        return 1
    fi
    bashio::log.info "Image ${image} is trusted by cosign"
}

The singing and validation is done using cosign. This is done using "keyless" singing somehow. Documentation about that can be found here.

Edited by Kevin Joris Holm