Skip to content

Commit 25f6624

Browse files
committed
Update client
1 parent b103650 commit 25f6624

File tree

14 files changed

+233
-221
lines changed

14 files changed

+233
-221
lines changed

irrd/conf/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,7 @@ def _check_staging_config(self) -> List[str]:
244244
Validate the current staging configuration.
245245
Returns a list of any errors, or an empty list for a valid config.
246246
"""
247-
from irrd.utils.crypto import (
248-
ed25519_private_key_from_str,
249-
ed25519_public_key_from_str,
250-
)
247+
from irrd.utils.crypto import eckey_from_str
251248

252249
errors = []
253250
config = self.user_config_staging
@@ -433,7 +430,7 @@ def _validate_subconfig(key, value):
433430

434431
if details.get("nrtm4_client_initial_public_key"):
435432
try:
436-
ed25519_public_key_from_str(details["nrtm4_client_initial_public_key"])
433+
eckey_from_str(details["nrtm4_client_initial_public_key"])
437434
except ValueError as ve:
438435
errors.append(
439436
f"Invalid value for setting nrtm4_client_initial_public_key for source {name}: {ve}"
@@ -469,15 +466,15 @@ def _validate_subconfig(key, value):
469466

470467
if details.get("nrtm4_server_private_key"):
471468
try:
472-
ed25519_private_key_from_str(details["nrtm4_server_private_key"])
469+
eckey_from_str(details["nrtm4_server_private_key"], require_private=True)
473470
except ValueError as ve:
474471
errors.append(
475472
f"Invalid value for setting nrtm4_server_private_key for source {name}: {ve}"
476473
)
477474

478475
if details.get("nrtm4_server_private_key_next"):
479476
try:
480-
ed25519_private_key_from_str(details["nrtm4_server_private_key_next"])
477+
eckey_from_str(details["nrtm4_server_private_key_next"], require_private=True)
481478
except ValueError as ve:
482479
errors.append(
483480
f"Invalid value for setting nrtm4_server_private_key_next for source {name}: {ve}"

irrd/conf/test_conf.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
import pytest
77
import yaml
88

9+
from ..mirroring.nrtm4.tests import (
10+
MOCK_UNF_PRIVATE_KEY,
11+
MOCK_UNF_PRIVATE_KEY_OTHER_STR,
12+
MOCK_UNF_PRIVATE_KEY_STR,
13+
)
14+
from ..utils.crypto import eckey_public_key_as_str
915
from . import (
1016
ConfigurationError,
1117
config_init,
@@ -119,10 +125,10 @@ def test_load_valid_reload_valid_config(self, monkeypatch, save_yaml_config, tmp
119125
"TESTDB-NRTM4": {
120126
"keep_journal": True,
121127
"nrtm4_client_notification_file_url": "https://testhost/",
122-
"nrtm4_client_initial_public_key": "kL7kSk56ASeaHl6Nj0eXC3XCHkCzktoPA3ceKz/cjOo=",
128+
"nrtm4_client_initial_public_key": eckey_public_key_as_str(MOCK_UNF_PRIVATE_KEY),
123129
"nrtm4_server_base_url": "https://example.com",
124-
"nrtm4_server_private_key": "FalXchs8HIU22Efc3ipNcxVwYwB+Mp0x9TCM9BFtig0=",
125-
"nrtm4_server_private_key_next": "4YDgaXpRDIU8vJbFYeYgPQqEa4YAdHeRF1s6SLdXCsE=",
130+
"nrtm4_server_private_key": MOCK_UNF_PRIVATE_KEY_STR,
131+
"nrtm4_server_private_key_next": MOCK_UNF_PRIVATE_KEY_OTHER_STR,
126132
"nrtm4_server_local_path": str(tmpdir),
127133
"nrtm4_server_snapshot_frequency": 3600 * 2,
128134
},
@@ -457,14 +463,8 @@ def test_load_invalid_config(self, save_yaml_config, tmpdir):
457463
assert "Unknown setting key: log.unknown" in str(ce.value)
458464
assert "Unknown key(s) under source TESTDB: unknown" in str(ce.value)
459465

460-
assert (
461-
"Invalid value for setting nrtm4_server_private_key for source TESTDB: Incorrect padding"
462-
in str(ce.value)
463-
)
464-
assert (
465-
"Invalid value for setting nrtm4_server_private_key_next for source TESTDB: Incorrect padding"
466-
in str(ce.value)
467-
)
466+
assert "Invalid value for setting nrtm4_server_private_key for source TESTDB:" in str(ce.value)
467+
assert "Invalid value for setting nrtm4_server_private_key_next for source TESTDB:" in str(ce.value)
468468
assert "Setting nrtm4_server_base_url for source TESTDB is not a valid https or file URL." in str(
469469
ce.value
470470
)

irrd/integration_tests/run.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import ujson
1616
import yaml
1717
from alembic import command, config
18-
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
18+
from joserfc.rfc7518.ec_key import ECKey
1919
from python_graphql_client import GraphqlClient
2020

2121
from irrd.conf import PASSWORD_HASH_DUMMY_VALUE, config_init
@@ -41,7 +41,7 @@
4141
)
4242
from irrd.utils.whois_client import whois_query, whois_query_irrd
4343

44-
from ..utils.crypto import ed25519_private_key_as_str, ed25519_public_key_as_str
44+
from ..utils.crypto import eckey_private_key_as_str, eckey_public_key_as_str
4545
from .constants import (
4646
EMAIL_DISCARD_MSGS_COMMAND,
4747
EMAIL_END,
@@ -924,7 +924,7 @@ def _start_irrds(self):
924924
with open(self.config_path1, "w") as yaml_file:
925925
yaml.safe_dump(config1, yaml_file)
926926

927-
self.nrtm4_private_key = Ed25519PrivateKey.generate()
927+
self.nrtm4_private_key = ECKey.generate_key()
928928
config2 = base_config.copy()
929929
config2["irrd"]["piddir"] = self.piddir2
930930
config2["irrd"]["database_url"] = self.database_url2
@@ -944,7 +944,7 @@ def _start_irrds(self):
944944
"nrtm_host": "127.0.0.1",
945945
"nrtm_port": str(self.port_whois1),
946946
"nrtm_access_list": "localhost",
947-
"nrtm4_server_private_key": ed25519_private_key_as_str(self.nrtm4_private_key),
947+
"nrtm4_server_private_key": eckey_private_key_as_str(self.nrtm4_private_key),
948948
"nrtm4_server_local_path": self.nrtm4_dir2,
949949
"nrtm4_server_base_url": f"file://{self.nrtm4_dir2}",
950950
"nrtm4_server_snapshot_frequency": 3600,
@@ -964,7 +964,7 @@ def _start_irrds(self):
964964
config3["irrd"]["sources"]["TEST"] = {
965965
"keep_journal": True,
966966
"nrtm4_client_notification_file_url": f"file://{self.nrtm4_dir2}update-notification-file.json",
967-
"nrtm4_client_initial_public_key": ed25519_public_key_as_str(self.nrtm4_private_key.public_key()),
967+
"nrtm4_client_initial_public_key": eckey_public_key_as_str(self.nrtm4_private_key),
968968
}
969969
with open(self.config_path3, "w") as yaml_file:
970970
yaml.safe_dump(config3, yaml_file)

irrd/mirroring/nrtm4/nrtm4_client.py

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import hashlib
21
import logging
32
import os
4-
from base64 import b64decode
53
from typing import Any, Dict, List, Optional, Tuple
64
from urllib.parse import urlparse
75

86
import pydantic
9-
from cryptography.exceptions import InvalidSignature
7+
from joserfc.rfc7515.model import CompactSignature
108

119
from irrd.conf import get_setting
1210
from irrd.mirroring.nrtm4.jsonseq import jsonseq_decode
@@ -29,7 +27,7 @@
2927
NRTM4ClientDatabaseStatus,
3028
)
3129
from irrd.storage.queries import DatabaseStatusQuery
32-
from irrd.utils.crypto import ed25519_public_key_from_str
30+
from irrd.utils.crypto import eckey_from_config, eckey_from_str, jws_deserialize
3331
from irrd.utils.misc import format_pydantic_errors
3432

3533
logger = logging.getLogger(__name__)
@@ -107,7 +105,7 @@ def _run_client(self) -> bool:
107105
)
108106
return has_loaded_snapshot
109107

110-
def _retrieve_unf(self) -> Tuple[NRTM4UpdateNotificationFile, str]:
108+
def _retrieve_unf(self) -> Tuple[NRTM4UpdateNotificationFile, Optional[str]]:
111109
"""
112110
Retrieve, verify and parse the Update Notification File.
113111
Returns the UNF object and the used key in base64 string.
@@ -116,24 +114,17 @@ def _retrieve_unf(self) -> Tuple[NRTM4UpdateNotificationFile, str]:
116114
if not notification_file_url: # pragma: no cover
117115
raise RuntimeError("NRTM4 client called for a source without a Update Notification File URL")
118116

119-
unf_content, _ = retrieve_file(notification_file_url, return_contents=True)
120-
unf_hash = hashlib.sha256(unf_content.encode("ascii")).hexdigest()
121-
sig_url = notification_file_url.replace(
122-
"update-notification-file.json", f"update-notification-file-signature-{unf_hash}.sig"
123-
)
124-
legacy_sig_url = notification_file_url + ".sig"
117+
unf_signed, _ = retrieve_file(notification_file_url, return_contents=True)
125118
if "nrtm.db.ripe.net" in notification_file_url: # pragma: no cover
126-
logger.warning(
127-
f"Downloading signature from legacy url {legacy_sig_url} instead of expected {sig_url}"
128-
)
129-
signature, _ = retrieve_file(legacy_sig_url, return_contents=True)
119+
# When removing this, also remove Optional[] from return type
120+
logger.warning("Expecting raw UNF as source is RIPE*, signature not checked")
121+
unf_payload = unf_signed.encode("ascii")
122+
used_key = None
130123
else:
131-
signature, _ = retrieve_file(sig_url, return_contents=True)
132-
133-
used_key = self._validate_unf_signature(unf_content, signature)
124+
unf_payload, used_key = self._deserialize_unf(unf_signed)
134125

135126
unf = NRTM4UpdateNotificationFile.model_validate_json(
136-
unf_content,
127+
unf_payload,
137128
context={
138129
"update_notification_file_scheme": urlparse(notification_file_url).scheme,
139130
"expected_values": {
@@ -143,61 +134,64 @@ def _retrieve_unf(self) -> Tuple[NRTM4UpdateNotificationFile, str]:
143134
)
144135
return unf, used_key
145136

146-
def _validate_unf_signature(self, unf_content: str, signature_b64: str) -> str:
137+
def _deserialize_unf(self, unf_content: str) -> Tuple[bytes, str]:
147138
"""
148139
Verify the Update Notification File signature,
149-
given the content (before JSON parsing) and a base64 signature.
150-
Returns the used key in base64 string.
140+
given the content (before JWS deserialize).
141+
Returns the deserialized payload and used key in PEM string.
151142
"""
143+
compact_signature: Optional[CompactSignature]
152144
unf_content_bytes = unf_content.encode("utf-8")
153-
signature = b64decode(signature_b64, validate=True)
154145
config_key = get_setting(f"sources.{self.source}.nrtm4_client_initial_public_key")
155146

156147
if self.last_status.current_key:
157-
keys = [
148+
keys_pem = [
158149
self.last_status.current_key,
159150
self.last_status.next_key,
160151
]
161152
else:
162-
keys = [get_setting(f"sources.{self.source}.nrtm4_client_initial_public_key")]
163-
164-
for key in keys:
165-
if key and self._validate_ed25519_signature(key, unf_content_bytes, signature):
166-
return key
167-
168-
if self.last_status.current_key and self._validate_ed25519_signature(
169-
config_key, unf_content_bytes, signature
170-
):
171-
# While technically just a "signature not valid case", it is a rather
172-
# confusing situation for the user, so gets a special message.
173-
msg = (
174-
f"{self.source}: No valid signature found for the Update Notification File for signature"
175-
f" {signature_b64}. The signature is valid for public key {config_key} set in the"
176-
" nrtm4_client_initial_public_key setting, but that is only used for initial validation."
177-
f" IRRD is currently expecting the public key {self.last_status.current_key}. If you want to"
178-
" clear IRRDs key information and revert to nrtm4_client_initial_public_key, use the"
179-
" 'irrdctl nrtmv4 client-clear-known-keys' command."
180-
)
181-
if self.last_status.next_key:
182-
msg += f" or {self.last_status.next_key}"
183-
raise NRTM4ClientError(msg)
153+
keys_pem = [get_setting(f"sources.{self.source}.nrtm4_client_initial_public_key")]
154+
155+
for key_pem in keys_pem:
156+
if not key_pem: # pragma: no cover
157+
continue
158+
pubkey = eckey_from_str(key_pem)
159+
try:
160+
compact_signature = jws_deserialize(unf_content_bytes, pubkey)
161+
return compact_signature.payload, key_pem
162+
except ValueError:
163+
continue
164+
165+
if self.last_status.current_key:
166+
compact_signature = None
167+
168+
try:
169+
ec_key = eckey_from_config(f"sources.{self.source}.nrtm4_client_initial_public_key")
170+
if ec_key:
171+
compact_signature = jws_deserialize(
172+
unf_content_bytes,
173+
ec_key,
174+
)
175+
except ValueError: # pragma: no cover
176+
pass
177+
if compact_signature:
178+
# While technically just a "signature not valid case", it is a rather
179+
# confusing situation for the user, so gets a special message.
180+
msg = (
181+
f"{self.source}: No valid signature found for the Update Notification File. The signature"
182+
f" is valid for public key {config_key} set in the nrtm4_client_initial_public_key"
183+
" setting, but that is only used for initial validation. IRRD is currently expecting the"
184+
f" public key {self.last_status.current_key}. If you want to clear IRRDs key information"
185+
" and revert to nrtm4_client_initial_public_key, use the 'irrdctl nrtmv4"
186+
" client-clear-known-keys' command."
187+
)
188+
if self.last_status.next_key:
189+
msg += f" or {self.last_status.next_key}"
190+
raise NRTM4ClientError(msg)
184191
raise NRTM4ClientError(
185-
f"{self.source}: No valid signature found for any known keys, signature {signature_b64},"
186-
f" considered public keys: {keys}"
192+
f"{self.source}: No valid signature found for any known keys, considered public keys: {keys_pem}"
187193
)
188194

189-
def _validate_ed25519_signature(self, key_b64: str, content: bytes, signature: bytes) -> bool:
190-
"""
191-
Verify an Ed25519 signature, given the key in base64, and the content
192-
and signature in bytes. Returns True or False for validity, raises other
193-
exceptions for things like an invalid key format.
194-
"""
195-
try:
196-
ed25519_public_key_from_str(key_b64).verify(signature, content)
197-
return True
198-
except InvalidSignature:
199-
return False
200-
201195
def _current_db_status(self) -> Tuple[bool, NRTM4ClientDatabaseStatus]:
202196
"""Look up the current status of self.source in the database."""
203197
query = DatabaseStatusQuery().source(self.source)

irrd/mirroring/nrtm4/nrtm4_server.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import base64
21
import datetime
32
import gzip
4-
import hashlib
53
import logging
64
import os
75
import secrets
@@ -24,7 +22,7 @@
2422
RPSLDatabaseJournalStatisticsQuery,
2523
RPSLDatabaseQuery,
2624
)
27-
from irrd.utils.crypto import ed25519_private_key_from_config, ed25519_public_key_as_str
25+
from irrd.utils.crypto import eckey_from_config, eckey_public_key_as_str, jws_serialize
2826
from irrd.utils.text import remove_auth_hashes
2927

3028
from ...utils.process_support import get_lockfile
@@ -222,13 +220,11 @@ def _write_unf(self) -> None:
222220
This is based on settings and self.status.
223221
"""
224222
assert self.status
225-
next_signing_private_key = ed25519_private_key_from_config(
223+
next_signing_private_key = eckey_from_config(
226224
f"sources.{self.source}.nrtm4_server_private_key_next", permit_empty=True
227225
)
228226
next_signing_public_key = (
229-
ed25519_public_key_as_str(next_signing_private_key.public_key())
230-
if next_signing_private_key
231-
else None
227+
eckey_public_key_as_str(next_signing_private_key) if next_signing_private_key else None
232228
)
233229
unf = NRTM4UpdateNotificationFile(
234230
nrtm_version=4,
@@ -251,14 +247,11 @@ def _write_unf(self) -> None:
251247
],
252248
)
253249
unf_content = unf.model_dump_json(exclude_none=True, include=unf.model_fields_set).encode("ascii")
254-
private_key = ed25519_private_key_from_config(f"sources.{self.source}.nrtm4_server_private_key")
250+
private_key = eckey_from_config(f"sources.{self.source}.nrtm4_server_private_key")
255251
assert private_key
256-
signature = private_key.sign(unf_content)
257-
unf_hash = hashlib.sha256(unf_content).hexdigest()
258-
with open(self.path / f"update-notification-file-signature-{unf_hash}.sig", "wb") as sig_file:
259-
sig_file.write(base64.b64encode(signature))
260-
with open(self.path / "update-notification-file.json", "wb") as unf_file:
261-
unf_file.write(unf_content)
252+
unf_serialized = jws_serialize(unf_content, private_key)
253+
with open(self.path / "update-notification-file.json", "w") as unf_file:
254+
unf_file.write(unf_serialized)
262255
self.status.last_update_notification_file_update = unf.timestamp
263256

264257
def _expire_deltas(self) -> None:

irrd/mirroring/nrtm4/nrtm4_types.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
from uuid import UUID
55

66
import pydantic
7+
from joserfc.rfc7518.ec_key import ECKey
78
from pytz import UTC
89
from typing_extensions import Self
910

10-
from irrd.utils.crypto import ed25519_public_key_from_str
11-
1211

1312
def get_from_pydantic_context(info: pydantic.ValidationInfo, key: str) -> Optional[Any]:
1413
"""
@@ -144,7 +143,7 @@ def validate_timestamp(cls, timestamp: datetime.datetime):
144143
def validate_next_signing_key(cls, next_signing_key: Optional[str]):
145144
if next_signing_key:
146145
try:
147-
ed25519_public_key_from_str(next_signing_key)
146+
ECKey.import_key(next_signing_key)
148147
except ValueError as ve:
149148
raise ValueError(
150149
f"Update Notification File has invalid next_signing_key {next_signing_key}: {ve}"

0 commit comments

Comments
 (0)