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 8cd5970 commit e5ed694
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@
}
]
}
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

use libparsec_tests_lite::prelude::*;

use super::authenticated_cmds;

// TODO
pub fn rep_already_set() {}
pub fn rep_invalid_certification() {}
pub fn rep_invalid_data() {}
pub fn rep_ok() {}
pub fn req() {}
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
3 changes: 3 additions & 0 deletions server/parsec/components/postgresql/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from parsec.components.postgresql.organization import PGOrganizationComponent
from parsec.components.postgresql.ping import PGPingComponent
from parsec.components.postgresql.realm import PGRealmComponent
from parsec.components.postgresql.shamir import PGShamirComponent
from parsec.components.postgresql.user import PGUserComponent
from parsec.components.postgresql.vlob import PGVlobComponent
from parsec.config import BackendConfig
Expand Down Expand Up @@ -53,6 +54,7 @@ async def components_factory(
config=config.blockstore_config, postgresql_pool=pool
)
block = PGBlockComponent(pool=pool)
shamir = PGShamirComponent()

pki = None
sequester = None
Expand All @@ -72,6 +74,7 @@ async def components_factory(
"blockstore": blockstore,
"pki": pki,
"sequester": sequester,
"shamir": shamir,
}
for component in components.values():
method = getattr(component, "register_components", None)
Expand Down
8 changes: 8 additions & 0 deletions server/parsec/components/postgresql/shamir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

from parsec.components.shamir import BaseShamirComponent


class PGShamirComponent(BaseShamirComponent):
# TODO
pass
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 e5ed694

Please sign in to comment.