Skip to content

Commit ad91d41

Browse files
feat(pki): Make server verify accept payload
Closes #11940
1 parent 1f7f082 commit ad91d41

File tree

8 files changed

+242
-166
lines changed

8 files changed

+242
-166
lines changed

server/parsec/components/memory/pki.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
DeviceID,
1010
HumanHandle,
1111
OrganizationID,
12-
PkiEnrollmentAnswerPayload,
1312
PKIEnrollmentID,
1413
PkiInvalidCertificateDER,
1514
PkiInvalidSignature,
@@ -19,6 +18,7 @@
1918
UserCertificate,
2019
UserProfile,
2120
VerifyKey,
21+
load_accept_payload,
2222
load_submit_payload,
2323
)
2424
from parsec.ballpark import RequireGreaterTimestamp, TimestampOutOfBallpark
@@ -36,7 +36,7 @@
3636
from parsec.components.pki import (
3737
BasePkiEnrollmentComponent,
3838
PkiCertificate,
39-
PkiEnrollmentAcceptStoreBadOutcome,
39+
PkiEnrollmentAcceptBadOutcome,
4040
PkiEnrollmentAcceptValidateBadOutcome,
4141
PkiEnrollmentInfo,
4242
PkiEnrollmentInfoAccepted,
@@ -390,38 +390,50 @@ async def accept(
390390
) -> (
391391
tuple[UserCertificate, DeviceCertificate]
392392
| PkiEnrollmentAcceptValidateBadOutcome
393-
| PkiEnrollmentAcceptStoreBadOutcome
393+
| PkiEnrollmentAcceptBadOutcome
394394
| TimestampOutOfBallpark
395395
| RequireGreaterTimestamp
396396
):
397397
try:
398398
org = self._data.organizations[organization_id]
399399
except KeyError:
400-
return PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_NOT_FOUND
400+
return PkiEnrollmentAcceptBadOutcome.ORGANIZATION_NOT_FOUND
401401
if org.is_expired:
402-
return PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_EXPIRED
402+
return PkiEnrollmentAcceptBadOutcome.ORGANIZATION_EXPIRED
403403

404404
# 1) Write lock common topic
405405

406406
async with org.topics_lock(write=["common"]) as (common_topic_last_timestamp,):
407407
try:
408408
author_device = org.devices[author]
409409
except KeyError:
410-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_FOUND
410+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_FOUND
411411
author_user_id = author_device.cooked.user_id
412412

413413
author_user = org.users[author_user_id]
414414
if author_user.is_revoked:
415-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_REVOKED
415+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_REVOKED
416416

417417
if author_user.current_profile != UserProfile.ADMIN:
418-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_ALLOWED
418+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_ALLOWED
419419

420420
# 2) Validate certificates
421421
try:
422-
PkiEnrollmentAnswerPayload.load(payload)
422+
load_accept_payload(
423+
SignedMessage(payload_signature_algorithm, payload_signature, payload),
424+
accepter_trustchain[0].content,
425+
list(map(lambda v: v.content, accepter_trustchain[1:])),
426+
self._config.x509_trust_anchor,
427+
now,
428+
)
429+
except PkiUntrusted:
430+
return PkiEnrollmentAcceptBadOutcome.INVALID_X509_TRUSTCHAIN
431+
except PkiInvalidCertificateDER:
432+
return PkiEnrollmentAcceptBadOutcome.INVALID_DER_X509_CERTIFICATE
433+
except PkiInvalidSignature:
434+
return PkiEnrollmentAcceptBadOutcome.INVALID_PAYLOAD_SIGNATURE
423435
except ValueError:
424-
return PkiEnrollmentAcceptStoreBadOutcome.INVALID_ACCEPT_PAYLOAD
436+
return PkiEnrollmentAcceptBadOutcome.INVALID_ACCEPT_PAYLOAD
425437

426438
match pki_enrollment_accept_validate(
427439
now=now,
@@ -452,25 +464,25 @@ async def accept(
452464
try:
453465
enrollment = org.pki_enrollments[enrollment_id]
454466
except KeyError:
455-
return PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NOT_FOUND
467+
return PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NOT_FOUND
456468

457469
if enrollment.enrollment_state != MemoryPkiEnrollmentState.SUBMITTED:
458-
return PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE
470+
return PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE
459471

460472
# 6) Check the user_id/device_id don't already exists and human_handle
461473
# is not already taken
462474

463475
if org.active_user_limit_reached():
464-
return PkiEnrollmentAcceptStoreBadOutcome.ACTIVE_USERS_LIMIT_REACHED
476+
return PkiEnrollmentAcceptBadOutcome.ACTIVE_USERS_LIMIT_REACHED
465477

466478
if u_certif.user_id in org.users:
467-
return PkiEnrollmentAcceptStoreBadOutcome.USER_ALREADY_EXISTS
479+
return PkiEnrollmentAcceptBadOutcome.USER_ALREADY_EXISTS
468480
assert d_certif.device_id not in org.devices
469481

470482
if any(
471483
True for u in org.active_users() if u.cooked.human_handle == u_certif.human_handle
472484
):
473-
return PkiEnrollmentAcceptStoreBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN
485+
return PkiEnrollmentAcceptBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN
474486

475487
# 7) All checks are good, now we do the actual insertion
476488

server/parsec/components/pki.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ class PkiEnrollmentRejectBadOutcome(BadOutcomeEnum):
246246
ENROLLMENT_NO_LONGER_AVAILABLE = auto()
247247

248248

249-
class PkiEnrollmentAcceptStoreBadOutcome(BadOutcomeEnum):
249+
class PkiEnrollmentAcceptBadOutcome(BadOutcomeEnum):
250250
ORGANIZATION_NOT_FOUND = auto()
251251
ORGANIZATION_EXPIRED = auto()
252252
AUTHOR_NOT_FOUND = auto()
@@ -258,6 +258,7 @@ class PkiEnrollmentAcceptStoreBadOutcome(BadOutcomeEnum):
258258
HUMAN_HANDLE_ALREADY_TAKEN = auto()
259259
ACTIVE_USERS_LIMIT_REACHED = auto()
260260
INVALID_ACCEPT_PAYLOAD = auto()
261+
INVALID_PAYLOAD_SIGNATURE = auto()
261262
INVALID_X509_TRUSTCHAIN = auto()
262263
INVALID_DER_X509_CERTIFICATE = auto()
263264

@@ -321,7 +322,7 @@ async def accept(
321322
) -> (
322323
tuple[UserCertificate, DeviceCertificate]
323324
| PkiEnrollmentAcceptValidateBadOutcome
324-
| PkiEnrollmentAcceptStoreBadOutcome
325+
| PkiEnrollmentAcceptBadOutcome
325326
| TimestampOutOfBallpark
326327
| RequireGreaterTimestamp
327328
):
@@ -593,28 +594,31 @@ async def api_pki_enrollment_accept(
593594
match outcome:
594595
case (_, _):
595596
return authenticated_cmds.latest.pki_enrollment_accept.RepOk()
596-
case PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE:
597+
case PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE:
597598
return (
598599
authenticated_cmds.latest.pki_enrollment_accept.RepEnrollmentNoLongerAvailable()
599600
)
600-
case PkiEnrollmentAcceptStoreBadOutcome.USER_ALREADY_EXISTS:
601+
case PkiEnrollmentAcceptBadOutcome.USER_ALREADY_EXISTS:
601602
return authenticated_cmds.latest.pki_enrollment_accept.RepUserAlreadyExists()
602-
case PkiEnrollmentAcceptStoreBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN:
603+
case PkiEnrollmentAcceptBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN:
603604
return authenticated_cmds.latest.pki_enrollment_accept.RepHumanHandleAlreadyTaken()
604-
case PkiEnrollmentAcceptStoreBadOutcome.ACTIVE_USERS_LIMIT_REACHED:
605+
case PkiEnrollmentAcceptBadOutcome.ACTIVE_USERS_LIMIT_REACHED:
605606
return authenticated_cmds.latest.pki_enrollment_accept.RepActiveUsersLimitReached()
606-
case PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NOT_FOUND:
607+
case PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NOT_FOUND:
607608
return authenticated_cmds.latest.pki_enrollment_accept.RepEnrollmentNotFound()
608-
case PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_ALLOWED:
609+
case PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_ALLOWED:
609610
return authenticated_cmds.latest.pki_enrollment_accept.RepAuthorNotAllowed()
610-
case PkiEnrollmentAcceptStoreBadOutcome.INVALID_ACCEPT_PAYLOAD:
611+
case PkiEnrollmentAcceptBadOutcome.INVALID_ACCEPT_PAYLOAD:
611612
return authenticated_cmds.latest.pki_enrollment_accept.RepInvalidPayload()
612-
case PkiEnrollmentAcceptStoreBadOutcome.INVALID_X509_TRUSTCHAIN:
613+
case PkiEnrollmentAcceptBadOutcome.INVALID_X509_TRUSTCHAIN:
613614
return authenticated_cmds.latest.pki_enrollment_accept.RepInvalidX509Trustchain()
614-
case PkiEnrollmentAcceptStoreBadOutcome.INVALID_DER_X509_CERTIFICATE:
615+
case PkiEnrollmentAcceptBadOutcome.INVALID_DER_X509_CERTIFICATE:
615616
return (
616617
authenticated_cmds.latest.pki_enrollment_accept.RepInvalidDerX509Certificate()
617618
)
619+
case PkiEnrollmentAcceptBadOutcome.INVALID_PAYLOAD_SIGNATURE:
620+
return authenticated_cmds.latest.pki_enrollment_accept.RepInvalidPayloadSignature()
621+
618622
# TODO: https://github.com/Scille/parsec-cloud/issues/11648
619623
case PkiEnrollmentAcceptValidateBadOutcome():
620624
raise NotImplementedError()
@@ -629,11 +633,11 @@ async def api_pki_enrollment_accept(
629633
return authenticated_cmds.latest.pki_enrollment_accept.RepRequireGreaterTimestamp(
630634
strictly_greater_than=error.strictly_greater_than
631635
)
632-
case PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_NOT_FOUND:
636+
case PkiEnrollmentAcceptBadOutcome.ORGANIZATION_NOT_FOUND:
633637
client_ctx.organization_not_found_abort()
634-
case PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_EXPIRED:
638+
case PkiEnrollmentAcceptBadOutcome.ORGANIZATION_EXPIRED:
635639
client_ctx.organization_expired_abort()
636-
case PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_FOUND:
640+
case PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_FOUND:
637641
client_ctx.author_not_found_abort()
638-
case PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_REVOKED:
642+
case PkiEnrollmentAcceptBadOutcome.AUTHOR_REVOKED:
639643
client_ctx.author_revoked_abort()

server/parsec/components/postgresql/pki.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from parsec.components.pki import (
1919
BasePkiEnrollmentComponent,
2020
PkiCertificate,
21-
PkiEnrollmentAcceptStoreBadOutcome,
21+
PkiEnrollmentAcceptBadOutcome,
2222
PkiEnrollmentAcceptValidateBadOutcome,
2323
PkiEnrollmentInfo,
2424
PkiEnrollmentInfoBadOutcome,
@@ -139,7 +139,7 @@ async def accept(
139139
) -> (
140140
tuple[UserCertificate, DeviceCertificate]
141141
| PkiEnrollmentAcceptValidateBadOutcome
142-
| PkiEnrollmentAcceptStoreBadOutcome
142+
| PkiEnrollmentAcceptBadOutcome
143143
| TimestampOutOfBallpark
144144
| RequireGreaterTimestamp
145145
):
@@ -158,4 +158,5 @@ async def accept(
158158
submitter_redacted_user_certificate,
159159
submitter_device_certificate,
160160
submitter_redacted_device_certificate,
161+
self._config,
161162
)

server/parsec/components/postgresql/pki_accept.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
DeviceCertificate,
77
DeviceID,
88
OrganizationID,
9-
PkiEnrollmentAnswerPayload,
109
PKIEnrollmentID,
10+
PkiInvalidCertificateDER,
11+
PkiInvalidSignature,
1112
PkiSignatureAlgorithm,
13+
PkiUntrusted,
14+
SignedMessage,
1215
UserCertificate,
1316
UserProfile,
1417
VerifyKey,
18+
load_accept_payload,
1519
)
1620
from parsec.ballpark import RequireGreaterTimestamp, TimestampOutOfBallpark
1721
from parsec.components.pki import (
1822
PkiCertificate,
19-
PkiEnrollmentAcceptStoreBadOutcome,
23+
PkiEnrollmentAcceptBadOutcome,
2024
PkiEnrollmentAcceptValidateBadOutcome,
2125
pki_enrollment_accept_validate,
2226
)
@@ -30,6 +34,7 @@
3034
)
3135
from parsec.components.postgresql.user_create_user import _q_insert_user_and_device
3236
from parsec.components.postgresql.utils import Q
37+
from parsec.config import BackendConfig
3338
from parsec.events import EventCommonCertificate, EventPkiEnrollment
3439

3540
_q_get_enrollment = Q("""
@@ -80,10 +85,11 @@ async def pki_accept(
8085
submitter_redacted_user_certificate: bytes,
8186
submitter_device_certificate: bytes,
8287
submitter_redacted_device_certificate: bytes,
88+
config: BackendConfig,
8389
) -> (
8490
tuple[UserCertificate, DeviceCertificate]
8591
| PkiEnrollmentAcceptValidateBadOutcome
86-
| PkiEnrollmentAcceptStoreBadOutcome
92+
| PkiEnrollmentAcceptBadOutcome
8793
| TimestampOutOfBallpark
8894
| RequireGreaterTimestamp
8995
):
@@ -96,23 +102,35 @@ async def pki_accept(
96102
case AuthAndLockCommonOnlyData() as db_common:
97103
pass
98104
case AuthAndLockCommonOnlyBadOutcome.ORGANIZATION_NOT_FOUND:
99-
return PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_NOT_FOUND
105+
return PkiEnrollmentAcceptBadOutcome.ORGANIZATION_NOT_FOUND
100106
case AuthAndLockCommonOnlyBadOutcome.ORGANIZATION_EXPIRED:
101-
return PkiEnrollmentAcceptStoreBadOutcome.ORGANIZATION_EXPIRED
107+
return PkiEnrollmentAcceptBadOutcome.ORGANIZATION_EXPIRED
102108
case AuthAndLockCommonOnlyBadOutcome.AUTHOR_NOT_FOUND:
103-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_FOUND
109+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_FOUND
104110
case AuthAndLockCommonOnlyBadOutcome.AUTHOR_REVOKED:
105-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_REVOKED
111+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_REVOKED
106112

107113
if db_common.user_current_profile != UserProfile.ADMIN:
108-
return PkiEnrollmentAcceptStoreBadOutcome.AUTHOR_NOT_ALLOWED
114+
return PkiEnrollmentAcceptBadOutcome.AUTHOR_NOT_ALLOWED
109115

110116
# 2) Validate certificates
111117

112118
try:
113-
PkiEnrollmentAnswerPayload.load(payload)
119+
load_accept_payload(
120+
SignedMessage(payload_signature_algorithm, payload_signature, payload),
121+
accepter_trustchain[0].content,
122+
list(map(lambda v: v.content, accepter_trustchain[1:])),
123+
config.x509_trust_anchor,
124+
now,
125+
)
126+
except PkiUntrusted:
127+
return PkiEnrollmentAcceptBadOutcome.INVALID_X509_TRUSTCHAIN
128+
except PkiInvalidCertificateDER:
129+
return PkiEnrollmentAcceptBadOutcome.INVALID_DER_X509_CERTIFICATE
130+
except PkiInvalidSignature:
131+
return PkiEnrollmentAcceptBadOutcome.INVALID_PAYLOAD_SIGNATURE
114132
except ValueError:
115-
return PkiEnrollmentAcceptStoreBadOutcome.INVALID_ACCEPT_PAYLOAD
133+
return PkiEnrollmentAcceptBadOutcome.INVALID_ACCEPT_PAYLOAD
116134

117135
match pki_enrollment_accept_validate(
118136
now=now,
@@ -146,7 +164,7 @@ async def pki_accept(
146164
)
147165
)
148166
if enrollment_row is None:
149-
return PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NOT_FOUND
167+
return PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NOT_FOUND
150168

151169
match enrollment_row["enrollment_internal_id"]:
152170
case int() as enrollment_internal_id:
@@ -158,7 +176,7 @@ async def pki_accept(
158176
case True:
159177
pass
160178
case False:
161-
return PkiEnrollmentAcceptStoreBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE
179+
return PkiEnrollmentAcceptBadOutcome.ENROLLMENT_NO_LONGER_AVAILABLE
162180
case _:
163181
assert False, enrollment_row
164182

@@ -190,31 +208,31 @@ async def pki_accept(
190208
case False:
191209
pass
192210
case True:
193-
return PkiEnrollmentAcceptStoreBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN
211+
return PkiEnrollmentAcceptBadOutcome.HUMAN_HANDLE_ALREADY_TAKEN
194212
case _:
195213
assert False, row
196214

197215
match row["active_users_limit_reached"]:
198216
case False:
199217
pass
200218
case True:
201-
return PkiEnrollmentAcceptStoreBadOutcome.ACTIVE_USERS_LIMIT_REACHED
219+
return PkiEnrollmentAcceptBadOutcome.ACTIVE_USERS_LIMIT_REACHED
202220
case _:
203221
assert False, row
204222

205223
match row["new_user_internal_id"]:
206224
case int():
207225
pass
208226
case None:
209-
return PkiEnrollmentAcceptStoreBadOutcome.USER_ALREADY_EXISTS
227+
return PkiEnrollmentAcceptBadOutcome.USER_ALREADY_EXISTS
210228
case _:
211229
assert False, row
212230

213231
match row["new_device_internal_id"]:
214232
case int() as new_device_internal_id:
215233
pass
216234
case None:
217-
return PkiEnrollmentAcceptStoreBadOutcome.USER_ALREADY_EXISTS
235+
return PkiEnrollmentAcceptBadOutcome.USER_ALREADY_EXISTS
218236
case _:
219237
assert False, row
220238

0 commit comments

Comments
 (0)