Skip to content

Commit

Permalink
[Shamir] Add create setup test.
Browse files Browse the repository at this point in the history
  • Loading branch information
AureliaDolo committed May 16, 2024
1 parent 3272683 commit bee2c50
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 10 deletions.
8 changes: 4 additions & 4 deletions server/parsec/_parsec_pyi/certif.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -393,17 +393,17 @@ class ShamirRecoveryShareCertificate:
self,
author: DeviceID | None,
timestamp: DateTime,
realm_id: VlobID,
configuration: RealmArchivingConfiguration,
recipient: UserID,
ciphered_share: bytes,
) -> None: ...
@property
def author(self) -> DeviceID | None: ...
@property
def timestamp(self) -> DateTime: ...
@property
def realm_id(self) -> VlobID: ...
def recipient(self) -> UserID: ...
@property
def configuration(self) -> RealmArchivingConfiguration: ...
def ciphered_share(self) -> bytes: ...
@classmethod
def verify_and_load(
cls,
Expand Down
4 changes: 4 additions & 0 deletions server/parsec/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from parsec.components.postgresql import components_factory as postgresql_components_factory
from parsec.components.realm import BaseRealmComponent
from parsec.components.sequester import BaseSequesterComponent
from parsec.components.shamir import BaseShamirComponent
from parsec.components.user import BaseUserComponent
from parsec.components.vlob import BaseVlobComponent
from parsec.config import BackendConfig
Expand Down Expand Up @@ -68,6 +69,7 @@ async def backend_factory(config: BackendConfig) -> AsyncGenerator[Backend, None
pki=components["pki"],
sequester=components["sequester"],
events=components["events"],
shamir=components["shamir"],
)


Expand All @@ -88,6 +90,7 @@ class Backend:
pki: BasePkiEnrollmentComponent
sequester: BaseSequesterComponent
events: BaseEventsComponent
shamir: BaseShamirComponent

# Only available if `config.db_url == "MOCKED"`
mocked_data: MemoryDatamodel | None = None
Expand All @@ -106,6 +109,7 @@ def __post_init__(self) -> None:
self.block,
self.pki,
self.events,
self.shamir,
# Ping command is only used in tests
include_ping=self.config.debug,
)
Expand Down
24 changes: 24 additions & 0 deletions server/parsec/components/memory/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SequesterRevokedServiceCertificate,
SequesterServiceCertificate,
SequesterServiceID,
ShamirRecoveryBriefCertificate,
UserCertificate,
UserID,
UserProfile,
Expand Down Expand Up @@ -68,6 +69,8 @@ class MemoryOrganization:
vlobs: dict[VlobID, list[MemoryVlobAtom]] = field(default_factory=dict)
blocks: dict[BlockID, MemoryBlock] = field(default_factory=dict)
block_store: dict[BlockID, bytes] = field(default_factory=dict, repr=False)
# The user id is the author of the shamir recovery process
shamir_setup: dict[UserID, MemoryShamirSetup] = field(default_factory=dict)

@property
def last_sequester_certificate_timestamp(self) -> DateTime:
Expand Down Expand Up @@ -395,3 +398,24 @@ class MemoryBlock:
created_on: DateTime
# None if not deleted
deleted_on: DateTime | None = None


@dataclass(slots=True)
class MemoryShamirSetup:
# The actual data we want to recover.
# It is encrypted with `data_key` that is itself split into shares.
# This should contains a serialized `LocalDevice`
ciphered_data: bytes
# The token the claimer should provide to get access to `ciphered_data`.
# This token is split into shares, hence it acts as a proof the claimer
# asking for the `ciphered_data` had it identity confirmed by the recipients.
reveal_token: bytes
# The Shamir recovery setup provided as a `ShamirRecoveryBriefCertificate`.
# It contains the threshold for the quorum and the shares recipients.
# This field has a certain level of duplication with the "shares" below,
# but they are used for different things (we provide the encrypted share
# data only when needed)
brief: ShamirRecoveryBriefCertificate
# The shares provided as a `ShamirRecoveryShareCertificate` since
# each share is aimed at a specific recipient.
shares: dict[UserID, bytes]
3 changes: 2 additions & 1 deletion server/parsec/components/memory/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def components_factory(config: BackendConfig) -> AsyncGenerator[dict[str,
ping = MemoryPingComponent(event_bus)
pki = MemoryPkiEnrollmentComponent(data, event_bus)
sequester = MemorySequesterComponent(data, event_bus)
shamir = MemoryShamirComponent()
shamir = MemoryShamirComponent(data, event_bus)
blockstore = blockstore_factory(config.blockstore_config, mocked_data=data)
block = MemoryBlockComponent(data, blockstore)
events = MemoryEventsComponent(data, config, event_bus)
Expand All @@ -66,6 +66,7 @@ async def components_factory(config: BackendConfig) -> AsyncGenerator[dict[str,
"sequester": sequester,
"block": block,
"blockstore": blockstore,
"shamir": shamir,
}

yield components
40 changes: 38 additions & 2 deletions server/parsec/components/memory/shamir.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
from parsec.components.shamir import BaseShamirComponent
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

from parsec._parsec import DeviceID, OrganizationID, UserID, VerifyKey, authenticated_cmds
from parsec.components.events import EventBus
from parsec.components.memory.datamodel import (
MemoryDatamodel,
MemoryShamirSetup,
)
from parsec.components.shamir import BaseShamirComponent, verify_certificates


class MemoryShamirComponent(BaseShamirComponent):
pass
def __init__(self, data: MemoryDatamodel, event_bus: EventBus) -> None:
super().__init__()
self._data = data
self._event_bus = event_bus

async def remove_recovery_setup(
self,
organization_id: OrganizationID,
author: UserID,
) -> None:
self._data.organizations[organization_id].shamir_setup.pop(author)

async def add_recovery_setup(
self,
organization_id: OrganizationID,
author: UserID,
device: DeviceID,
author_verify_key: VerifyKey,
setup: authenticated_cmds.latest.shamir_recovery_setup.ShamirRecoverySetup,
) -> None | authenticated_cmds.latest.shamir_recovery_setup.Rep:
match verify_certificates(setup, device, author_verify_key):
case (brief, shares):
self._data.organizations[organization_id].shamir_setup[author] = MemoryShamirSetup(
setup.ciphered_data, setup.reveal_token, brief, shares
)

case authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData() as error:
return error
100 changes: 97 additions & 3 deletions server/parsec/components/shamir.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,102 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

from __future__ import annotations

from parsec._parsec import (
DeviceID,
OrganizationID,
ShamirRecoveryBriefCertificate,
ShamirRecoveryShareCertificate,
UserID,
VerifyKey,
authenticated_cmds,
)
from parsec.api import api
from parsec._parsec import authenticated_cmds
from parsec.client_context import AuthenticatedClientContext


class BaseShamirComponent:
@api
async def create_shared_recovery_device(self, client_ctx: AuthenticatedClientContext, req: authenticated_cmds.latest.shamir_recovery_setup.Req) -> authenticated_cmds.latest.shamir_recovery_setup.Rep:
pass
async def create_shared_recovery_device(
self,
client_ctx: AuthenticatedClientContext,
req: authenticated_cmds.latest.shamir_recovery_setup.Req,
) -> authenticated_cmds.latest.shamir_recovery_setup.Rep:
if req.setup is None:
await self.remove_recovery_setup(client_ctx.organization_id, client_ctx.user_id)
return authenticated_cmds.latest.shamir_recovery_setup.RepOk()
else:
match await self.add_recovery_setup(
client_ctx.organization_id,
client_ctx.user_id,
client_ctx.device_id,
client_ctx.device_verify_key,
req.setup,
):
case None:
return authenticated_cmds.latest.shamir_recovery_setup.RepOk()
case authenticated_cmds.latest.shamir_recovery_setup.Rep() as error:
return error

# async def test_dump_current_shamir(
# self, organization_id: OrganizationID
# ) -> dict[UserID, ShamirDump]:
# raise NotImplementedError

async def remove_recovery_setup(
self,
organization_id: OrganizationID,
author: UserID,
) -> None:
raise NotImplementedError

async def add_recovery_setup(
self,
organization_id: OrganizationID,
author: UserID,
device: DeviceID,
author_verify_key: VerifyKey,
setup: authenticated_cmds.latest.shamir_recovery_setup.ShamirRecoverySetup,
) -> None | authenticated_cmds.latest.shamir_recovery_setup.Rep:
raise NotImplementedError


def verify_certificates(
setup: authenticated_cmds.latest.shamir_recovery_setup.ShamirRecoverySetup,
author: DeviceID,
author_verify_key: VerifyKey,
) -> (
tuple[ShamirRecoveryBriefCertificate, dict[UserID, bytes]]
| authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData
):
share_certificates: dict[UserID, bytes] = {}
try:
brief_certificate = ShamirRecoveryBriefCertificate.verify_and_load(
setup.brief, author_verify_key, expected_author=author
)
except ValueError:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()

for raw_share in setup.shares:
try:
share_certificate = ShamirRecoveryShareCertificate.verify_and_load(
raw_share, author_verify_key, expected_author=author, expected_recipient=None
)
except ValueError:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()

# share recipient not in brief
if share_certificate.recipient not in brief_certificate.per_recipient_shares:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()
# this recipient already has a share
if share_certificate.recipient in share_certificates:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()
# user included themselves as a share recipient
if share_certificate.recipient == author.user_id:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()
share_certificates[share_certificate.recipient] = raw_share
delta = set(brief_certificate.per_recipient_shares) - set(share_certificates)
# some recipient specified in brief has no share
if delta:
return authenticated_cmds.latest.shamir_recovery_setup.RepInvalidData()
return brief_certificate, share_certificates
1 change: 1 addition & 0 deletions server/tests/api_v4/authenticated/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
from .test_vlob_read_batch import * # noqa
from .test_vlob_read_versions import * # noqa
from .test_vlob_update import * # noqa
from .test_shamir_recovery_setup import * # noqa
64 changes: 64 additions & 0 deletions server/tests/api_v4/authenticated/test_shamir_recovery_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

from parsec._parsec import (
DateTime,
ShamirRecoveryBriefCertificate,
ShamirRecoveryShareCertificate,
authenticated_cmds,
)
from tests.common import Backend, CoolorgRpcClients, TestbedBackend


async def test_authenticated_shamir_recovery_setup_ok(
coolorg: CoolorgRpcClients,
backend: Backend,
testbed: TestbedBackend,
) -> None:
share = ShamirRecoveryShareCertificate(
coolorg.alice.device_id, DateTime.now(), coolorg.mallory.user_id, b"abc"
)
brief = ShamirRecoveryBriefCertificate(
author=coolorg.alice.device_id,
timestamp=DateTime.now(),
threshold=1,
per_recipient_shares={coolorg.mallory.user_id: 2},
)

setup = authenticated_cmds.v4.shamir_recovery_setup.ShamirRecoverySetup(
b"abc",
b"def",
brief.dump_and_sign(coolorg.alice.signing_key),
[share.dump_and_sign(coolorg.alice.signing_key)],
)
rep = await coolorg.alice.shamir_recovery_setup(setup)
assert rep == authenticated_cmds.v4.shamir_recovery_setup.RepOk()

# TODO dump


async def test_authenticated_shamir_recovery_setup_already_set(
coolorg: CoolorgRpcClients,
backend: Backend,
testbed: TestbedBackend,
) -> None:
pass


async def test_authenticated_shamir_recovery_setup_invalid_certification(
coolorg: CoolorgRpcClients,
backend: Backend,
testbed: TestbedBackend,
) -> None:
pass


async def test_authenticated_shamir_recovery_setup_invalid_data(
coolorg: CoolorgRpcClients,
backend: Backend,
testbed: TestbedBackend,
) -> None:
setup = authenticated_cmds.v4.shamir_recovery_setup.ShamirRecoverySetup(
bytes("abc", "utf-8"), bytes("def", "utf-8"), bytes("ijk", "utf-8"), [bytes("lmn", "utf-8")]
)
rep = await coolorg.alice.shamir_recovery_setup(setup)
assert rep == authenticated_cmds.v4.shamir_recovery_setup.RepInvalidData()

0 comments on commit bee2c50

Please sign in to comment.