mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 21:56:29 +00:00
Using CAS for content-trust (#3382)
* Using CAS for content-trust * v2 * Fix linting errors * Adjust field checked for status in CAS response * CI workflow needs CAS not VCN now * Use cwd in test as code won't be in /usr/src * Pre-cache CAS pub key for supervisor * Cas doesn't actually need key file executable Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
This commit is contained in:
parent
e5d64f6c75
commit
3478005e70
23
.github/workflows/builder.yml
vendored
23
.github/workflows/builder.yml
vendored
@ -33,6 +33,7 @@ on:
|
|||||||
- setup.py
|
- setup.py
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
DEFAULT_PYTHON: 3.9
|
||||||
BUILD_NAME: supervisor
|
BUILD_NAME: supervisor
|
||||||
BUILD_TYPE: supervisor
|
BUILD_TYPE: supervisor
|
||||||
WHEELS_TAG: 3.9-alpine3.14
|
WHEELS_TAG: 3.9-alpine3.14
|
||||||
@ -138,7 +139,7 @@ jobs:
|
|||||||
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
||||||
|
|
||||||
codenotary:
|
codenotary:
|
||||||
name: CodeNotary signature
|
name: CAS signature
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -148,6 +149,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: actions/setup-python@v2.3.1
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
- name: Install dirhash and calc hash
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
id: dirhash
|
||||||
|
run: |
|
||||||
|
pip3 install dirhash
|
||||||
|
dir_hash="$(dirhash "${{ github.workspace }}" -a sha256 --match "*.py")"
|
||||||
|
echo "::set-output name=dirhash::${dir_hash}"
|
||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: home-assistant/actions/helpers/version@master
|
uses: home-assistant/actions/helpers/version@master
|
||||||
@ -158,10 +173,8 @@ jobs:
|
|||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: home-assistant/actions/helpers/codenotary@master
|
uses: home-assistant/actions/helpers/codenotary@master
|
||||||
with:
|
with:
|
||||||
source: dir://${{ github.workspace }}
|
source: hash://${{ steps.dirhash.outputs.dirhash }}
|
||||||
user: ${{ secrets.VCN_USER }}
|
token: ${{ secrets.CAS_TOKEN }}
|
||||||
password: ${{ secrets.VCN_PASSWORD }}
|
|
||||||
organisation: ${{ secrets.VCN_ORG }}
|
|
||||||
|
|
||||||
version:
|
version:
|
||||||
name: Update version
|
name: Update version
|
||||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: 3.9
|
DEFAULT_PYTHON: 3.9
|
||||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||||
DEFAULT_VCN: v0.9.8
|
DEFAULT_CAS: v1.0.1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Separate job to pre-populate the base dependency cache
|
# Separate job to pre-populate the base dependency cache
|
||||||
@ -351,10 +351,10 @@ jobs:
|
|||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install VCN tools
|
- name: Install CAS tools
|
||||||
uses: home-assistant/actions/helpers/vcn@master
|
uses: home-assistant/actions/helpers/cas@master
|
||||||
with:
|
with:
|
||||||
vcn_version: ${{ env.DEFAULT_VCN }}
|
version: ${{ env.DEFAULT_CAS }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2.1.7
|
uses: actions/cache@v2.1.7
|
||||||
|
21
Dockerfile
21
Dockerfile
@ -5,10 +5,12 @@ ENV \
|
|||||||
S6_SERVICES_GRACETIME=10000 \
|
S6_SERVICES_GRACETIME=10000 \
|
||||||
SUPERVISOR_API=http://localhost
|
SUPERVISOR_API=http://localhost
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
ARG \
|
||||||
WORKDIR /usr/src
|
BUILD_ARCH \
|
||||||
|
CAS_VERSION
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
|
WORKDIR /usr/src
|
||||||
RUN \
|
RUN \
|
||||||
set -x \
|
set -x \
|
||||||
&& apk add --no-cache \
|
&& apk add --no-cache \
|
||||||
@ -18,7 +20,20 @@ RUN \
|
|||||||
libffi \
|
libffi \
|
||||||
libpulse \
|
libpulse \
|
||||||
musl \
|
musl \
|
||||||
openssl
|
openssl \
|
||||||
|
&& apk add --no-cache --virtual .build-dependencies \
|
||||||
|
build-base \
|
||||||
|
go \
|
||||||
|
\
|
||||||
|
&& git clone -b "v${CAS_VERSION}" --depth 1 \
|
||||||
|
https://github.com/codenotary/cas \
|
||||||
|
&& cd cas \
|
||||||
|
&& make cas \
|
||||||
|
&& mv cas /usr/bin/cas \
|
||||||
|
\
|
||||||
|
&& apk del .build-dependencies \
|
||||||
|
&& rm -rf /root/go /root/.cache \
|
||||||
|
&& rm -rf /usr/src/cas
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
@ -9,6 +9,8 @@ build_from:
|
|||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
args:
|
||||||
|
CAS_VERSION: 1.0.1
|
||||||
labels:
|
labels:
|
||||||
io.hass.type: supervisor
|
io.hass.type: supervisor
|
||||||
org.opencontainers.image.title: Home Assistant Supervisor
|
org.opencontainers.image.title: Home Assistant Supervisor
|
||||||
|
@ -11,6 +11,7 @@ cpe==1.2.1
|
|||||||
cryptography==36.0.1
|
cryptography==36.0.1
|
||||||
debugpy==1.5.1
|
debugpy==1.5.1
|
||||||
deepmerge==1.0.1
|
deepmerge==1.0.1
|
||||||
|
dirhash==0.2.1
|
||||||
docker==5.0.3
|
docker==5.0.3
|
||||||
gitpython==3.1.26
|
gitpython==3.1.26
|
||||||
jinja2==3.0.3
|
jinja2==3.0.3
|
||||||
|
4
rootfs/root/.cas-trusted-signing-pub-key
Normal file
4
rootfs/root/.cas-trusted-signing-pub-key
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE03LvYuz79GTJx4uKp3w6NrSe5JZI
|
||||||
|
iBtgzzYi0YQYtZO/r+xFpgDJEa0gLHkXtl94fpqrFiN89In83lzaszbZtA==
|
||||||
|
-----END PUBLIC KEY-----
|
@ -633,7 +633,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
"""Validate trust of content."""
|
"""Validate trust of content."""
|
||||||
checksum = image_id.partition(":")[2]
|
checksum = image_id.partition(":")[2]
|
||||||
job = asyncio.run_coroutine_threadsafe(
|
job = asyncio.run_coroutine_threadsafe(
|
||||||
self.sys_security.verify_own_content(checksum=checksum), self.sys_loop
|
self.sys_security.verify_own_content(checksum), self.sys_loop
|
||||||
)
|
)
|
||||||
job.result(timeout=20)
|
job.result(timeout=20)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from ...const import CoreState
|
from ...const import CoreState
|
||||||
from ...coresys import CoreSys
|
from ...coresys import CoreSys
|
||||||
from ...exceptions import CodeNotaryError, CodeNotaryUntrusted
|
from ...exceptions import CodeNotaryError, CodeNotaryUntrusted
|
||||||
|
from ...utils.codenotary import calc_checksum_path_sourcecode
|
||||||
from ..const import UnsupportedReason
|
from ..const import UnsupportedReason
|
||||||
from .base import EvaluateBase
|
from .base import EvaluateBase
|
||||||
|
|
||||||
@ -41,8 +42,13 @@ class EvaluateSourceMods(EvaluateBase):
|
|||||||
_LOGGER.warning("Disabled content-trust, skipping evaluation")
|
_LOGGER.warning("Disabled content-trust, skipping evaluation")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Calculate sume of the sourcecode
|
||||||
|
checksum = await self.sys_run_in_executor(
|
||||||
|
calc_checksum_path_sourcecode, _SUPERVISOR_SOURCE
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.sys_security.verify_own_content(path=_SUPERVISOR_SOURCE)
|
await self.sys_security.verify_own_content(checksum)
|
||||||
except CodeNotaryUntrusted:
|
except CodeNotaryUntrusted:
|
||||||
return True
|
return True
|
||||||
except CodeNotaryError:
|
except CodeNotaryError:
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Fetch last versions from webserver."""
|
"""Fetch last versions from webserver."""
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from typing import Awaitable
|
||||||
from typing import Awaitable, Optional
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONTENT_TRUST,
|
ATTR_CONTENT_TRUST,
|
||||||
@ -11,7 +10,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .coresys import CoreSys, CoreSysAttributes
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
from .exceptions import CodeNotaryError, CodeNotaryUntrusted, PwnedError
|
from .exceptions import CodeNotaryError, CodeNotaryUntrusted, PwnedError
|
||||||
from .utils.codenotary import vcn_validate
|
from .utils.codenotary import cas_validate
|
||||||
from .utils.common import FileConfiguration
|
from .utils.common import FileConfiguration
|
||||||
from .utils.pwned import check_pwned_password
|
from .utils.pwned import check_pwned_password
|
||||||
from .validate import SCHEMA_SECURITY_CONFIG
|
from .validate import SCHEMA_SECURITY_CONFIG
|
||||||
@ -57,16 +56,14 @@ class Security(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Set pwned is enabled/disabled."""
|
"""Set pwned is enabled/disabled."""
|
||||||
self._data[ATTR_PWNED] = value
|
self._data[ATTR_PWNED] = value
|
||||||
|
|
||||||
async def verify_own_content(
|
async def verify_own_content(self, checksum: str) -> Awaitable[None]:
|
||||||
self, checksum: Optional[str] = None, path: Optional[Path] = None
|
|
||||||
) -> Awaitable[None]:
|
|
||||||
"""Verify content from HA org."""
|
"""Verify content from HA org."""
|
||||||
if not self.content_trust:
|
if not self.content_trust:
|
||||||
_LOGGER.warning("Disabled content-trust, skip validation")
|
_LOGGER.warning("Disabled content-trust, skip validation")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await vcn_validate(checksum, path, org="home-assistant.io")
|
await cas_validate(checksum=checksum, signer="notary@home-assistant.io")
|
||||||
except CodeNotaryUntrusted:
|
except CodeNotaryUntrusted:
|
||||||
raise
|
raise
|
||||||
except CodeNotaryError:
|
except CodeNotaryError:
|
||||||
|
@ -127,7 +127,7 @@ class Supervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
await self.sys_security.verify_own_content(checksum=calc_checksum(data))
|
await self.sys_security.verify_own_content(calc_checksum(data))
|
||||||
except CodeNotaryUntrusted as err:
|
except CodeNotaryUntrusted as err:
|
||||||
raise SupervisorAppArmorError(
|
raise SupervisorAppArmorError(
|
||||||
"Content-Trust is broken for the AppArmor profile fetch!",
|
"Content-Trust is broken for the AppArmor profile fetch!",
|
||||||
|
@ -207,7 +207,7 @@ class Updater(FileConfiguration, CoreSysAttributes):
|
|||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
await self.sys_security.verify_own_content(checksum=calc_checksum(data))
|
await self.sys_security.verify_own_content(calc_checksum(data))
|
||||||
except CodeNotaryUntrusted as err:
|
except CodeNotaryUntrusted as err:
|
||||||
raise UpdaterError(
|
raise UpdaterError(
|
||||||
"Content-Trust is broken for the version file fetch!", _LOGGER.critical
|
"Content-Trust is broken for the version file fetch!", _LOGGER.critical
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
"""Small wrapper for CodeNotary."""
|
"""Small wrapper for CodeNotary."""
|
||||||
# pylint: disable=unreachable
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Optional, Union
|
from typing import Final, Union
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
from dirhash import dirhash
|
||||||
|
|
||||||
from . import clean_env
|
from . import clean_env
|
||||||
from ..exceptions import CodeNotaryBackendError, CodeNotaryError, CodeNotaryUntrusted
|
from ..exceptions import CodeNotaryBackendError, CodeNotaryError, CodeNotaryUntrusted
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_VCN_CMD: str = "vcn authenticate --silent --output json"
|
_CAS_CMD: str = (
|
||||||
_CACHE: set[tuple[str, Path, str, str]] = set()
|
"cas authenticate --signerID {signer} --silent --output json --hash {sum}"
|
||||||
|
)
|
||||||
|
_CACHE: set[tuple[str, str]] = set()
|
||||||
|
|
||||||
|
|
||||||
_ATTR_ERROR = "error"
|
_ATTR_ERROR: Final = "error"
|
||||||
_ATTR_VERIFICATION = "verification"
|
_ATTR_STATUS: Final = "status"
|
||||||
_ATTR_STATUS = "status"
|
|
||||||
|
|
||||||
|
|
||||||
def calc_checksum(data: Union[str, bytes]) -> str:
|
def calc_checksum(data: Union[str, bytes]) -> str:
|
||||||
@ -31,36 +32,24 @@ def calc_checksum(data: Union[str, bytes]) -> str:
|
|||||||
return hashlib.sha256(data).hexdigest()
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
async def vcn_validate(
|
def calc_checksum_path_sourcecode(folder: Path) -> str:
|
||||||
checksum: Optional[str] = None,
|
"""Calculate checksum for a path source code."""
|
||||||
path: Optional[Path] = None,
|
return dirhash(folder.as_posix(), "sha256", match=["*.py"])
|
||||||
org: Optional[str] = None,
|
|
||||||
signer: Optional[str] = None,
|
|
||||||
|
async def cas_validate(
|
||||||
|
signer: str,
|
||||||
|
checksum: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate data against CodeNotary."""
|
"""Validate data against CodeNotary."""
|
||||||
return None
|
if (checksum, signer) in _CACHE:
|
||||||
if (checksum, path, org, signer) in _CACHE:
|
|
||||||
return
|
return
|
||||||
command = shlex.split(_VCN_CMD)
|
|
||||||
|
|
||||||
# Generate command for request
|
# Generate command for request
|
||||||
if org:
|
command = shlex.split(_CAS_CMD.format(signer=signer, sum=checksum))
|
||||||
command.extend(["--org", org])
|
|
||||||
elif signer:
|
|
||||||
command.extend(["--signerID", signer])
|
|
||||||
|
|
||||||
if checksum:
|
|
||||||
command.extend(["--hash", checksum])
|
|
||||||
elif path:
|
|
||||||
if path.is_dir:
|
|
||||||
command.append(f"dir://{path.as_posix()}")
|
|
||||||
else:
|
|
||||||
command.append(path.as_posix())
|
|
||||||
else:
|
|
||||||
RuntimeError("At least path or checksum need to be set!")
|
|
||||||
|
|
||||||
# Request notary authorization
|
# Request notary authorization
|
||||||
_LOGGER.debug("Send vcn command: %s", command)
|
_LOGGER.debug("Send cas command: %s", command)
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
*command,
|
*command,
|
||||||
@ -93,7 +82,7 @@ async def vcn_validate(
|
|||||||
if _ATTR_ERROR in data_json:
|
if _ATTR_ERROR in data_json:
|
||||||
raise CodeNotaryBackendError(data_json[_ATTR_ERROR], _LOGGER.warning)
|
raise CodeNotaryBackendError(data_json[_ATTR_ERROR], _LOGGER.warning)
|
||||||
|
|
||||||
if data_json[_ATTR_VERIFICATION][_ATTR_STATUS] == 0:
|
if data_json[_ATTR_STATUS] == 0:
|
||||||
_CACHE.add((checksum, path, org, signer))
|
_CACHE.add((checksum, signer))
|
||||||
else:
|
else:
|
||||||
raise CodeNotaryUntrusted()
|
raise CodeNotaryUntrusted()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Test evaluation base."""
|
"""Test evaluation base."""
|
||||||
# pylint: disable=import-error,protected-access
|
# pylint: disable=import-error,protected-access
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from supervisor.const import CoreState
|
from supervisor.const import CoreState
|
||||||
@ -10,6 +12,10 @@ from supervisor.resolution.evaluations.source_mods import EvaluateSourceMods
|
|||||||
|
|
||||||
async def test_evaluation(coresys: CoreSys):
|
async def test_evaluation(coresys: CoreSys):
|
||||||
"""Test evaluation."""
|
"""Test evaluation."""
|
||||||
|
with patch(
|
||||||
|
"supervisor.resolution.evaluations.source_mods._SUPERVISOR_SOURCE",
|
||||||
|
Path(os.getcwd()),
|
||||||
|
):
|
||||||
sourcemods = EvaluateSourceMods(coresys)
|
sourcemods = EvaluateSourceMods(coresys)
|
||||||
coresys.core.state = CoreState.RUNNING
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user