mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[wireguard] Mark private keys sensitive, stop redacting public keys (#17736)
This commit is contained in:
+10
-2
@@ -1510,10 +1510,18 @@ def _redact_with_legacy_fallback(output: str) -> str:
|
|||||||
m = _LEGACY_REDACTION_RE.search(line)
|
m = _LEGACY_REDACTION_RE.search(line)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
|
key = m.group("key")
|
||||||
if not in_substitutions:
|
if not in_substitutions:
|
||||||
unmarked.add(m.group("key"))
|
# Public keys (e.g. wireguard's peer_public_key) are not secret;
|
||||||
|
# redacting them and telling maintainers to mark them cv.sensitive
|
||||||
|
# would be wrong on both counts. Substitution keys are user-named
|
||||||
|
# with no schema behind them, so anything secret-shaped there
|
||||||
|
# (public or not) stays conservatively redacted.
|
||||||
|
if "public" in key.split("_"):
|
||||||
|
continue
|
||||||
|
unmarked.add(key)
|
||||||
lines[i] = (
|
lines[i] = (
|
||||||
f"{line[: m.start()]}{m.group('key')}: "
|
f"{line[: m.start()]}{key}: "
|
||||||
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
|
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
|
||||||
)
|
)
|
||||||
output = "\n".join(lines)
|
output = "\n".join(lines)
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||||
cv.Required(CONF_ADDRESS): cv.ipv4address,
|
cv.Required(CONF_ADDRESS): cv.ipv4address,
|
||||||
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address,
|
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address,
|
||||||
cv.Required(CONF_PRIVATE_KEY): _wireguard_key,
|
cv.Required(CONF_PRIVATE_KEY): cv.sensitive(_wireguard_key),
|
||||||
cv.Required(CONF_PEER_ENDPOINT): cv.string,
|
cv.Required(CONF_PEER_ENDPOINT): cv.string,
|
||||||
cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key,
|
cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key,
|
||||||
cv.Optional(CONF_PEER_PORT, default=51820): cv.port,
|
cv.Optional(CONF_PEER_PORT, default=51820): cv.port,
|
||||||
cv.Optional(CONF_PEER_PRESHARED_KEY): _wireguard_key,
|
cv.Optional(CONF_PEER_PRESHARED_KEY): cv.sensitive(_wireguard_key),
|
||||||
cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list(
|
cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list(
|
||||||
_cidr_network
|
_cidr_network
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the wireguard component."""
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Tests for the wireguard component schema."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome.components.wireguard import CONFIG_SCHEMA
|
||||||
|
from esphome.const import PlatformFramework
|
||||||
|
from esphome.yaml_util import SensitiveStr
|
||||||
|
from tests.component_tests.types import SetCoreConfigCallable
|
||||||
|
|
||||||
|
# Any 42 base64 chars plus a valid terminator satisfies _WG_KEY_REGEX.
|
||||||
|
PRIVATE_KEY = "a" * 42 + "A="
|
||||||
|
PEER_PUBLIC_KEY = "b" * 42 + "A="
|
||||||
|
PEER_PRESHARED_KEY = "c" * 42 + "A="
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field", "value", "sensitive"),
|
||||||
|
[
|
||||||
|
("private_key", PRIVATE_KEY, True),
|
||||||
|
("peer_preshared_key", PEER_PRESHARED_KEY, True),
|
||||||
|
("peer_public_key", PEER_PUBLIC_KEY, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_key_sensitivity(
|
||||||
|
field: str,
|
||||||
|
value: str,
|
||||||
|
sensitive: bool,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""The private and preshared keys are secrets and must be tagged so dump
|
||||||
|
tooling redacts them deterministically; the peer's public key is not a
|
||||||
|
secret and must stay readable in redacted dumps (see issue #17718)."""
|
||||||
|
set_core_config(PlatformFramework.ESP32_IDF)
|
||||||
|
config = CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
"address": "10.0.0.2",
|
||||||
|
"private_key": PRIVATE_KEY,
|
||||||
|
"peer_endpoint": "wg.example.com",
|
||||||
|
"peer_public_key": PEER_PUBLIC_KEY,
|
||||||
|
"peer_preshared_key": PEER_PRESHARED_KEY,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert isinstance(config[field], SensitiveStr) == sensitive
|
||||||
|
assert config[field] == value
|
||||||
@@ -442,6 +442,46 @@ def test_redact_with_legacy_fallback__does_not_match_fragment_as_suffix(
|
|||||||
assert not any("legacy substring" in rec.message for rec in caplog.records)
|
assert not any("legacy substring" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("field", ["public_key", "peer_public_key"])
|
||||||
|
def test_redact_with_legacy_fallback__skips_public_key_fields(
|
||||||
|
field: str,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Public keys are not secret; fields with a ``public`` name segment
|
||||||
|
must pass through unredacted and without the migration warning
|
||||||
|
(see issue #17718)."""
|
||||||
|
text = f"{field}: c29tZXB1YmxpY2tleQ==\n"
|
||||||
|
with caplog.at_level(logging.WARNING, logger="esphome.__main__"):
|
||||||
|
out = _redact_with_legacy_fallback(text)
|
||||||
|
assert out == text
|
||||||
|
assert not any("legacy substring" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redact_with_legacy_fallback__public_substitution_still_redacted(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Substitution keys are user-named with no schema behind them, so the
|
||||||
|
public-key exemption does not apply there; a ``public``-named substitution
|
||||||
|
keeps the conservative silent redaction."""
|
||||||
|
text = "substitutions:\n public_key: something\nesphome:\n name: x\n"
|
||||||
|
with caplog.at_level(logging.WARNING, logger="esphome.__main__"):
|
||||||
|
out = _redact_with_legacy_fallback(text)
|
||||||
|
assert "public_key: \\033[8msomething\\033[28m" in out
|
||||||
|
assert not any("legacy substring" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redact_with_legacy_fallback__public_must_be_a_whole_segment(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""The exemption matches ``public`` as an underscore-separated segment,
|
||||||
|
not a substring; an unrelated name like ``republic_key`` keeps the
|
||||||
|
conservative redaction."""
|
||||||
|
with caplog.at_level(logging.WARNING, logger="esphome.__main__"):
|
||||||
|
out = _redact_with_legacy_fallback("republic_key: abc\n")
|
||||||
|
assert "republic_key: \\033[8mabc\\033[28m" in out
|
||||||
|
assert any("'republic_key'" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
||||||
def test_redact_with_legacy_fallback__substitutions_redacted_without_warning(
|
def test_redact_with_legacy_fallback__substitutions_redacted_without_warning(
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user