Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions syft/sync/peers/key_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Validation and fingerprints for peer public key bundles.

A peer's public encryption bundle is a DID document that arrives over Google
Drive, and the threat model treats that transport as adversarial. Before a
bundle is pinned it must:

- parse as a DID document whose inner signatures verify: the identity key
signed the key-agreement keys it is bundled with, and
- assert the identity it is filed under: ``id`` is ``did:syft:<email>`` and the
``identity`` field, when present, is that email.

Neither check proves the identity key belongs to the person. Only comparing
fingerprints out of band does, which is what :func:`format_fingerprint` is for.
"""

from typing import Optional

import syft_crypto_python as syc

DID_PREFIX = "did:syft:"


class InvalidPeerBundleError(ValueError):
"""The bundle does not parse, its signatures fail, or it names another identity."""


class PeerKeyChangedError(ValueError):
"""A bundle for an already-pinned peer carries a different identity key."""

def __init__(
self, peer_email: str, old_fingerprint: str, new_fingerprint: str
) -> None:
self.peer_email = peer_email
self.old_fingerprint = old_fingerprint
self.new_fingerprint = new_fingerprint
super().__init__(
f"The encryption key of {peer_email} changed.\n"
f" pinned: {format_fingerprint(old_fingerprint)}\n"
f" new: {format_fingerprint(new_fingerprint)}\n"
"A key changes when the peer reinstalls or regenerates their keys, "
"and also when someone tampers with the key exchange. Confirm the "
"new fingerprint with the peer out of band before trusting it."
)


def did_for_email(email: str) -> str:
return f"{DID_PREFIX}{email}"


def _same_email(a: str, b: str) -> bool:
return a.strip().casefold() == b.strip().casefold()


def format_fingerprint(fingerprint: Optional[str]) -> str:
"""Group a hex fingerprint into blocks of four for reading aloud."""
if not fingerprint:
return ""
return " ".join(fingerprint[i : i + 4] for i in range(0, len(fingerprint), 4))


def bundle_fingerprint(bundle: dict) -> str:
"""Fingerprint of the identity key in ``bundle`` (no identity check)."""
return _parse(bundle).identity_fingerprint()


def _parse(bundle: dict) -> syc.SyftPublicKeyBundle:
if not isinstance(bundle, dict):
raise InvalidPeerBundleError(
f"Expected a DID document (dict), got {type(bundle).__name__}"
)
try:
parsed = syc.SyftPublicKeyBundle.from_did_document(bundle)
except Exception as e:
raise InvalidPeerBundleError(f"Could not parse key bundle: {e}") from e
if not parsed.verify_signatures():
raise InvalidPeerBundleError("Key bundle signatures do not verify")
return parsed


def parse_and_validate_bundle(peer_email: str, bundle: dict) -> syc.SyftPublicKeyBundle:
"""Parse ``bundle``, verify its signatures, and check it belongs to ``peer_email``.

Raises:
InvalidPeerBundleError: on any failure.
"""
parsed = _parse(bundle)

did = bundle.get("id")
if not isinstance(did, str) or not did.startswith(DID_PREFIX):
raise InvalidPeerBundleError(
f"Key bundle for {peer_email} has no {DID_PREFIX!r} id (got {did!r})"
)
did_email = did[len(DID_PREFIX) :]
if not _same_email(did_email, peer_email):
raise InvalidPeerBundleError(
f"Key bundle filed under {peer_email} asserts the identity {did_email!r}"
)

identity = bundle.get("identity")
if identity is not None and (
not isinstance(identity, str) or not _same_email(identity, peer_email)
):
raise InvalidPeerBundleError(
f"Key bundle filed under {peer_email} carries identity {identity!r}"
)

return parsed
15 changes: 15 additions & 0 deletions syft/sync/peers/peer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from typing import Any, List, Optional
from pydantic import BaseModel, PrivateAttr
from syft.sync.peers.key_bundle import bundle_fingerprint
from syft.sync.platforms.base_platform import BasePlatform
from syft.sync.version.version_info import VersionInfo

Expand All @@ -23,6 +24,20 @@ class Peer(BaseModel):
# Set by SyftboxManager.peers when this Peer is handed out.
_manager: Any = PrivateAttr(default=None)

@property
def fingerprint(self) -> Optional[str]:
"""Fingerprint of the peer's pinned identity key, or None without a bundle.

Compare it with the peer out of band: the bundle arrives over Drive, and
only that comparison shows the key is theirs.
"""
if self.public_encryption_bundle is None:
return None
try:
return bundle_fingerprint(self.public_encryption_bundle)
except ValueError:
return None

@property
def is_approved(self) -> bool:
"""Returns True if peer is accepted"""
Expand Down
Loading
Loading