/
/
/
1"""Tests for the WebRTC DTLS certificate persistence."""
2
3from __future__ import annotations
4
5import stat
6from typing import TYPE_CHECKING
7
8from cryptography.hazmat.primitives import serialization
9from cryptography.hazmat.primitives.asymmetric import ec
10
11from music_assistant.helpers.webrtc_certificate import (
12 CERT_FILENAME,
13 KEY_FILENAME,
14 _get_or_create_certificate,
15 get_or_create_remote_id,
16 get_or_create_webrtc_certificate_pems,
17)
18
19if TYPE_CHECKING:
20 from pathlib import Path
21
22# Frozen fixtures derived from _generate_certificate()/_remote_id_from_certificate(),
23# pinned as literals so these tests keep guarding the derivation even if the
24# implementation changes later.
25FIXTURE_CERT_PEM = (
26 "-----BEGIN CERTIFICATE-----\n"
27 "MIIBQTCB6KADAgECAhRuYKuRIszhzAOCq29MyuvEOzJ9EDAKBggqhkjOPQQDAjAh\n"
28 "MR8wHQYDVQQDDBZNdXNpYyBBc3Npc3RhbnQgV2ViUlRDMB4XDTI2MDcxOTEwMjIw\n"
29 "OFoXDTM2MDcxNzEwMjIwOFowITEfMB0GA1UEAwwWTXVzaWMgQXNzaXN0YW50IFdl\n"
30 "YlJUQzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAU/5JpGlyTQ2pexaev5083F\n"
31 "1/dkvNKxCCRWQbfZOvk1vRhJP4jsT710Un7jzfMx25rtMVsv1lzvCmerRFCsacsw\n"
32 "CgYIKoZIzj0EAwIDSAAwRQIgOTVzTaqOXVyaD2NDNIVjXlX3hDOxEgbacy7Q7Lfj\n"
33 "61QCIQCq3RN0iyR9qfuo28VjkWceEcHkDwynS1vFKr2oqffO6g==\n"
34 "-----END CERTIFICATE-----\n"
35)
36FIXTURE_KEY_PEM = (
37 "-----BEGIN PRIVATE KEY-----\n"
38 "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrrFr6dcpp+Sr1j+e\n"
39 "dY0oUp/WYDjh/xpceu3sIxmldUWhRANCAAQFP+SaRpck0NqXsWnr+dPNxdf3ZLzS\n"
40 "sQgkVkG32Tr5Nb0YST+I7E+9dFJ+483zMdua7TFbL9Zc7wpnq0RQrGnL\n"
41 "-----END PRIVATE KEY-----\n"
42)
43FIXTURE_REMOTE_ID = "QCPJJTJMI3IDBNURU6XJJENRYU"
44
45
46def test_certificate_is_persistent(tmp_path: Path) -> None:
47 """Repeated calls return the same keypair."""
48 key, cert = _get_or_create_certificate(str(tmp_path))
49 key2, cert2 = _get_or_create_certificate(str(tmp_path))
50 assert cert2 == cert
51 assert key2.public_key() == key.public_key()
52
53
54def test_mismatched_key_regenerates_pair(tmp_path: Path) -> None:
55 """A key file not matching the certificate yields a fresh consistent pair."""
56 _, cert = _get_or_create_certificate(str(tmp_path))
57 stray_key = ec.generate_private_key(ec.SECP256R1())
58 (tmp_path / KEY_FILENAME).write_bytes(
59 stray_key.private_bytes(
60 encoding=serialization.Encoding.PEM,
61 format=serialization.PrivateFormat.PKCS8,
62 encryption_algorithm=serialization.NoEncryption(),
63 )
64 )
65 key2, cert2 = _get_or_create_certificate(str(tmp_path))
66 assert key2.public_key() == cert2.public_key()
67 assert cert2 != cert
68
69
70def test_private_key_created_with_restrictive_permissions(tmp_path: Path) -> None:
71 """A fresh private key file is owner read/write only."""
72 _get_or_create_certificate(str(tmp_path))
73 assert stat.S_IMODE((tmp_path / KEY_FILENAME).stat().st_mode) == 0o600
74
75
76def test_private_key_permissions_tightened_on_regeneration(tmp_path: Path) -> None:
77 """Regenerating over a loose-permissions key file restores owner-only access."""
78 _get_or_create_certificate(str(tmp_path))
79 key_path = tmp_path / KEY_FILENAME
80 key_path.chmod(0o644)
81 (tmp_path / CERT_FILENAME).unlink()
82 _get_or_create_certificate(str(tmp_path))
83 assert stat.S_IMODE(key_path.stat().st_mode) == 0o600
84
85
86def test_private_key_permissions_tightened_on_load(tmp_path: Path) -> None:
87 """Loading a valid pair tightens a loose key file to owner-only access."""
88 _, cert = _get_or_create_certificate(str(tmp_path))
89 key_path = tmp_path / KEY_FILENAME
90 key_path.chmod(0o644)
91 _, cert2 = _get_or_create_certificate(str(tmp_path))
92 assert cert2 == cert
93 assert stat.S_IMODE(key_path.stat().st_mode) == 0o600
94
95
96def test_remote_id_and_pems_stable_across_frozen_fixture(tmp_path: Path) -> None:
97 """A pre-existing certificate pair yields the frozen Remote ID and is returned as-is."""
98 (tmp_path / CERT_FILENAME).write_text(FIXTURE_CERT_PEM)
99 (tmp_path / KEY_FILENAME).write_text(FIXTURE_KEY_PEM)
100
101 assert get_or_create_remote_id(str(tmp_path)) == FIXTURE_REMOTE_ID
102 assert get_or_create_webrtc_certificate_pems(str(tmp_path)) == (
103 FIXTURE_CERT_PEM,
104 FIXTURE_KEY_PEM,
105 )
106
107
108def test_get_or_create_webrtc_certificate_pems_persists(tmp_path: Path) -> None:
109 """A fresh call creates the certificate files and a second call returns the same PEMs."""
110 cert_pem, key_pem = get_or_create_webrtc_certificate_pems(str(tmp_path))
111 cert_pem2, key_pem2 = get_or_create_webrtc_certificate_pems(str(tmp_path))
112 assert cert_pem2 == cert_pem
113 assert key_pem2 == key_pem
114