Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions .github/workflows/builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,6 @@ jobs:
--target /data \
--cosign \
--generic ${{ needs.init.outputs.version }}
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
version:
name: Update version
Expand Down Expand Up @@ -293,33 +291,6 @@ jobs:
exit 1
fi
- name: Check the Supervisor code sign
if: needs.init.outputs.publish == 'true'
run: |
echo "Enable Content-Trust"
test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then
exit 1
fi
echo "Run supervisor health check"
test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then
exit 1
fi
echo "Check supervisor unhealthy"
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]')
if [ "$test" != "" ]; then
exit 1
fi
echo "Check supervisor supported"
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unsupported[]')
if [[ "$test" =~ source_mods ]]; then
exit 1
fi
- name: Create full backup
id: backup
run: |
Expand Down
7 changes: 0 additions & 7 deletions supervisor/addons/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1513,13 +1513,6 @@ def _restore_data():
_LOGGER.info("Finished restore for add-on %s", self.slug)
return wait_for_start

def check_trust(self) -> Awaitable[None]:
"""Calculate Addon docker content trust.

Return Coroutine.
"""
return self.instance.check_trust()

@Job(
name="addon_restart_after_problem",
throttle_period=WATCHDOG_THROTTLE_PERIOD,
Expand Down
10 changes: 2 additions & 8 deletions supervisor/addons/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
from .const import (
ATTR_BACKUP,
ATTR_BREAKING_VERSIONS,
ATTR_CODENOTARY,
ATTR_PATH,
ATTR_READ_ONLY,
AddonBackupMode,
Expand Down Expand Up @@ -632,13 +631,8 @@ def with_journald(self) -> bool:

@property
def signed(self) -> bool:
"""Return True if the image is signed."""
return ATTR_CODENOTARY in self.data

@property
def codenotary(self) -> str | None:
"""Return Signer email address for CAS."""
return self.data.get(ATTR_CODENOTARY)
"""Currently no signing support."""
return False

@property
def breaking_versions(self) -> list[AwesomeVersion]:
Expand Down
7 changes: 6 additions & 1 deletion supervisor/addons/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ def _warn_addon_config(config: dict[str, Any]):
name,
)

if ATTR_CODENOTARY in config:
_LOGGER.warning(
"Add-on '%s' uses deprecated 'codenotary' field in config. This field is no longer used and will be ignored. Please report this to the maintainer.",
name,
)

return config


Expand Down Expand Up @@ -417,7 +423,6 @@ def _migrate(config: dict[str, Any]):
vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce(
AddonBackupMode
),
vol.Optional(ATTR_CODENOTARY): vol.Email(),
vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
vol.Schema({str: SCHEMA_ELEMENT}),
Expand Down
22 changes: 9 additions & 13 deletions supervisor/api/security.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
"""Init file for Supervisor Security RESTful API."""

import asyncio
import logging
from typing import Any

from aiohttp import web
import attr
import voluptuous as vol

from ..const import ATTR_CONTENT_TRUST, ATTR_FORCE_SECURITY, ATTR_PWNED
from supervisor.exceptions import APIGone

from ..const import ATTR_FORCE_SECURITY, ATTR_PWNED
from ..coresys import CoreSysAttributes
from .utils import api_process, api_validate

_LOGGER: logging.Logger = logging.getLogger(__name__)

# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema(
Copy link
Contributor

@mdegat01 mdegat01 Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema for security/options includes content_trust and force_security:

SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_PWNED): vol.Boolean(),
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
}
)

@api_process
async def options(self, request: web.Request) -> None:
"""Set options for Security."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_PWNED in body:
self.sys_security.pwned = body[ATTR_PWNED]
if ATTR_CONTENT_TRUST in body:
self.sys_security.content_trust = body[ATTR_CONTENT_TRUST]
if ATTR_FORCE_SECURITY in body:
self.sys_security.force = body[ATTR_FORCE_SECURITY]
await self.sys_security.save_data()
await self.sys_resolution.evaluate.evaluate_system()

Neither of these options make sense anymore. They should be removed from here, the python properties removed from security module and removed from the schema for the persistent config file for the security module here:

SCHEMA_SECURITY_CONFIG = vol.Schema(
{
vol.Optional(ATTR_CONTENT_TRUST, default=True): vol.Boolean(),
vol.Optional(ATTR_PWNED, default=True): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY, default=False): vol.Boolean(),
},
extra=vol.REMOVE_EXTRA,
)

That will also require a change in the client library, cli and documentation to remove those options.

The security/info API should not return their values either:

async def info(self, request: web.Request) -> dict[str, Any]:
"""Return Security information."""
return {
ATTR_CONTENT_TRUST: self.sys_security.content_trust,
ATTR_PWNED: self.sys_security.pwned,
ATTR_FORCE_SECURITY: self.sys_security.force,
}

That will require a change in the client library and documentation.

Should be able to toss those constants after as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, my intention was to leave the outside API intact, so we can reuse it for the cosign implementation if it feels right. But after a bit more consideration, I think it is better to remove the API as well. With cosign I'd like to use cosign specific wording, e.g. image signature verification for the verification process etc. Content verification will work quite a bit different with EROFS/fsverity, so the API does not really match.

The force_security flag is also used by HIBP, so I did not remove that part.

{
vol.Optional(ATTR_PWNED): vol.Boolean(),
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
}
)
Expand All @@ -31,7 +27,6 @@ class APISecurity(CoreSysAttributes):
async def info(self, request: web.Request) -> dict[str, Any]:
"""Return Security information."""
return {
ATTR_CONTENT_TRUST: self.sys_security.content_trust,
ATTR_PWNED: self.sys_security.pwned,
ATTR_FORCE_SECURITY: self.sys_security.force,
}
Expand All @@ -43,8 +38,6 @@ async def options(self, request: web.Request) -> None:

if ATTR_PWNED in body:
self.sys_security.pwned = body[ATTR_PWNED]
if ATTR_CONTENT_TRUST in body:
self.sys_security.content_trust = body[ATTR_CONTENT_TRUST]
if ATTR_FORCE_SECURITY in body:
self.sys_security.force = body[ATTR_FORCE_SECURITY]

Expand All @@ -54,6 +47,9 @@ async def options(self, request: web.Request) -> None:

@api_process
async def integrity_check(self, request: web.Request) -> dict[str, Any]:
"""Run backend integrity check."""
result = await asyncio.shield(self.sys_security.integrity_check())
return attr.asdict(result)
"""Run backend integrity check.
CodeNotary integrity checking has been removed. This endpoint now returns
an error indicating the feature is gone.
"""
raise APIGone("Integrity check feature has been removed.")
4 changes: 0 additions & 4 deletions supervisor/api/supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@
ATTR_BLK_READ,
ATTR_BLK_WRITE,
ATTR_CHANNEL,
ATTR_CONTENT_TRUST,
ATTR_COUNTRY,
ATTR_CPU_PERCENT,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_FORCE_SECURITY,
ATTR_HEALTHY,
ATTR_ICON,
ATTR_IP_ADDRESS,
Expand Down Expand Up @@ -69,8 +67,6 @@
vol.Optional(ATTR_DEBUG): vol.Boolean(),
vol.Optional(ATTR_DEBUG_BLOCK): vol.Boolean(),
vol.Optional(ATTR_DIAGNOSTICS): vol.Boolean(),
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
vol.Optional(ATTR_COUNTRY): str,
Expand Down
1 change: 0 additions & 1 deletion supervisor/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ async def initialize_coresys() -> CoreSys:

if coresys.dev:
coresys.updater.channel = UpdateChannel.DEV
coresys.security.content_trust = False

# Convert datetime
logging.Formatter.converter = lambda *args: coresys.now().timetuple()
Expand Down
10 changes: 0 additions & 10 deletions supervisor/docker/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,16 +846,6 @@ async def stop(self, remove_container: bool = True) -> None:
):
self.sys_resolution.dismiss_issue(self.addon.device_access_missing_issue)

async def _validate_trust(self, image_id: str) -> None:
"""Validate trust of content."""
if not self.addon.signed:
return

checksum = image_id.partition(":")[2]
return await self.sys_security.verify_content(
cast(str, self.addon.codenotary), checksum
)

@Job(
name="docker_addon_hardware_events",
conditions=[JobCondition.OS_AGENT],
Expand Down
12 changes: 1 addition & 11 deletions supervisor/docker/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import re

from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
from awesomeversion import AwesomeVersion
from docker.types import Mount

from ..const import LABEL_MACHINE
Expand Down Expand Up @@ -244,13 +244,3 @@ def is_initialize(self) -> Awaitable[bool]:
self.image,
self.sys_homeassistant.version,
)

async def _validate_trust(self, image_id: str) -> None:
"""Validate trust of content."""
try:
if self.version in {None, LANDINGPAGE} or self.version < _VERIFY_TRUST:
return
except AwesomeVersionCompareException:
return

await super()._validate_trust(image_id)
46 changes: 0 additions & 46 deletions supervisor/docker/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,12 @@
)
from ..coresys import CoreSys
from ..exceptions import (
CodeNotaryError,
CodeNotaryUntrusted,
DockerAPIError,
DockerError,
DockerJobError,
DockerLogOutOfOrder,
DockerNotFound,
DockerRequestError,
DockerTrustError,
)
from ..jobs import SupervisorJob
from ..jobs.const import JOB_GROUP_DOCKER_INTERFACE, JobConcurrency
Expand Down Expand Up @@ -425,18 +422,6 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
platform=MAP_ARCH[image_arch],
)

# Validate content
try:
await self._validate_trust(cast(str, docker_image.id))
except CodeNotaryError:
with suppress(docker.errors.DockerException):
await self.sys_run_in_executor(
self.sys_docker.images.remove,
image=f"{image}:{version!s}",
force=True,
)
raise

# Tag latest
if latest:
_LOGGER.info(
Expand All @@ -462,16 +447,6 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
raise DockerError(
f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error
) from err
except CodeNotaryUntrusted as err:
raise DockerTrustError(
f"Pulled image {image}:{version!s} failed on content-trust verification!",
_LOGGER.critical,
) from err
except CodeNotaryError as err:
raise DockerTrustError(
f"Error happened on Content-Trust check for {image}:{version!s}: {err!s}",
_LOGGER.error,
) from err
finally:
if listener:
self.sys_bus.remove_listener(listener)
Expand Down Expand Up @@ -809,24 +784,3 @@ def run_inside(self, command: str) -> Awaitable[CommandReturn]:
return self.sys_run_in_executor(
self.sys_docker.container_run_inside, self.name, command
)

async def _validate_trust(self, image_id: str) -> None:
"""Validate trust of content."""
checksum = image_id.partition(":")[2]
return await self.sys_security.verify_own_content(checksum)

@Job(
name="docker_interface_check_trust",
on_condition=DockerJobError,
concurrency=JobConcurrency.GROUP_REJECT,
)
async def check_trust(self) -> None:
"""Check trust of exists Docker image."""
try:
image = await self.sys_run_in_executor(
self.sys_docker.images.get, f"{self.image}:{self.version!s}"
)
except (docker.errors.DockerException, requests.RequestException):
return

await self._validate_trust(cast(str, image.id))
21 changes: 6 additions & 15 deletions supervisor/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ class APINotFound(APIError):
status = 404


class APIGone(APIError):
"""API is no longer available."""

status = 410


class APIAddonNotInstalled(APIError):
"""Not installed addon requested at addons API."""

Expand Down Expand Up @@ -577,21 +583,6 @@ class PwnedConnectivityError(PwnedError):
"""Connectivity errors while checking pwned passwords."""


# util/codenotary


class CodeNotaryError(HassioError):
"""Error general with CodeNotary."""


class CodeNotaryUntrusted(CodeNotaryError):
"""Error on untrusted content."""


class CodeNotaryBackendError(CodeNotaryError):
"""CodeNotary backend error happening."""


# util/whoami


Expand Down
7 changes: 0 additions & 7 deletions supervisor/homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,6 @@ def logs(self) -> Awaitable[bytes]:
"""
return self.instance.logs()

def check_trust(self) -> Awaitable[None]:
"""Calculate HomeAssistant docker content trust.

Return Coroutine.
"""
return self.instance.check_trust()

async def stats(self) -> DockerStats:
"""Return stats of Home Assistant."""
try:
Expand Down
7 changes: 0 additions & 7 deletions supervisor/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,6 @@ def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress

def check_trust(self) -> Awaitable[None]:
"""Calculate plugin docker content trust.

Return Coroutine.
"""
return self.instance.check_trust()

def logs(self) -> Awaitable[bytes]:
"""Get docker plugin logs.

Expand Down
Loading
Loading