Skip to content
Draft
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
3 changes: 3 additions & 0 deletions libp2p/crypto/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ def _serialize_to_protobuf(self) -> crypto_pb2.PublicKey:
data = self.to_bytes()
protobuf_key = crypto_pb2.PublicKey(key_type=key_type, data=data)
return protobuf_key

def serialize_to_protobuf(self) -> crypto_pb2.PublicKey:
return self._serialize_to_protobuf()

def serialize(self) -> bytes:
"""Return the canonical serialization of this ``Key``."""
Expand Down
Empty file added libp2p/record/__init__.py
Empty file.
137 changes: 137 additions & 0 deletions libp2p/record/envelope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from libp2p.crypto.keys import PrivateKey, PublicKey
from libp2p.crypto.serialization import deserialize_public_key
from typing import Optional, Tuple

from libp2p.record.record import Record, unmarshal_record_payload
import threading
import libp2p.record.pb.envelope_pb2 as pb
from libp2p.record.exceptions import (
ErrEmptyDomain,
ErrEmptyPayloadType,
ErrInvalidSignature
)

class Envelope:
public_key: PublicKey
payload_type: bytes
raw_payload: bytes
signature: bytes

def __init__(self, public_key: PublicKey, payload_type: bytes, raw_payload: bytes, signature: bytes):
self.public_key = public_key
self.payload_type = payload_type
self.raw_payload = raw_payload
self.signature = signature

self._cached_record: Optional[Record] = None
self._unmarshal_error: Optional[Exception] = None
self._unmarshal_lock = threading.Lock()
self._record_initialized: bool = False

def marshal(self) -> bytes:
key = self.public_key.serialize_to_protobuf()
msg = pb.Envelope(
public_key=key,
payload_type=self.payload_type,
payload=self.raw_payload,
signature=self.signature
)
return msg.SerializeToString()

@classmethod
def unmarshal_envelope(cls, data: bytes) -> "Envelope":
msg = pb.Envelope()
msg.ParseFromString(data)

key = deserialize_public_key(msg.public_key.SerializePartialToString())
return cls(
public_key=key,
payload_type=msg.payload_type,
raw_payload=msg.payload,
signature=msg.signature
)

def equal(self, other: "Envelope") -> bool:
if other is None:
return False
return (
self.public_key.__eq__(other.public_key) and
self.payload_type == other.payload_type and
self.raw_payload == other.raw_payload and
self.signature == other.signature
)

def record(self) -> Record:
"""Return the unmarshalled Record, caching on first access."""
with self._unmarshal_lock:
if self._cached_record is not None:
return self._cached_record
try:
self._cached_record = unmarshal_record_payload(
payload_type=self.payload_type,
payload_bytes=self.raw_payload
)
except Exception as e:
self._unmarshal_error = e
raise
return self._cached_record

def validate(self, domain: str) -> bool:
if not domain:
raise ErrEmptyDomain("domain cannot be empty")
if not self.payload_type:
raise ErrEmptyPayloadType("payload_type cannot be empty")

try:
unsigned = make_unsigned(domain=domain, payload_type=self.payload_type, payload=self.raw_payload)
valid = self.public_key.verify(unsigned, self.signature)
if not valid:
raise ErrInvalidSignature("invalid signature or domain")
except Exception as e:
raise e
return True


def seal(rec: Record, private_key: PrivateKey) -> Envelope:
payload = rec.marshal_record()
payload_type = rec.codec()
domain = rec.domain()

if not domain:
raise ErrEmptyDomain()
if not payload_type:
raise ErrEmptyPayloadType()

unsigned = make_unsigned(domain=domain, payload_type=payload_type, payload=payload)
sig = private_key.sign(unsigned)
return Envelope(
public_key=private_key.get_public_key(),
payload_type=payload_type,
raw_payload=payload,
signature=sig
)


def consume_envelope(data: bytes, domain: str) -> Tuple[Envelope, Record]:
msg = Envelope.unmarshal_envelope(data)
msg.validate(domain)
rec = msg.record()
return msg, rec

def encode_uvarint(value: int) -> bytes:
"""Encode an int as protobuf-style unsigned varint."""
buf = bytearray()
while value > 0x7F:
buf.append((value & 0x7F) | 0x80)
value >>= 7
buf.append(value)
return bytes(buf)


def make_unsigned(domain: str, payload_type: bytes, payload: bytes) -> bytes:
fields = [domain.encode("utf-8"), payload_type, payload]
b = bytearray()
for f in fields:
b.extend(encode_uvarint(len(f)))
b.extend(f)
return bytes(b)
10 changes: 10 additions & 0 deletions libp2p/record/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

class ErrEmptyDomain(Exception):
pass

class ErrEmptyPayloadType(Exception):
pass

class ErrInvalidSignature(Exception):
pass

12 changes: 12 additions & 0 deletions libp2p/record/libp2p_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datetime import date


class Libp2pRecord:
def __init__(self, key: bytes, value: bytes, time_received: date) -> None:
self.key = key
self.value = value
self.time_received = date

def serialize(self) -> bytes:
return self.key

Empty file added libp2p/record/pb/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions libp2p/record/pb/envelope.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
syntax = "proto3";

package record.pb;

import "libp2p/crypto/pb/crypto.proto";

message Envelope {
// public_key is the public key of the keypair the enclosed payload was
// signed with.
crypto.pb.PublicKey public_key = 1;

// payload_type encodes the type of payload, so that it can be deserialized
// deterministically.
bytes payload_type = 2;

// payload is the actual payload carried inside this envelope.
bytes payload = 3;

// signature is the signature produced by the private key corresponding to
// the enclosed public key, over the payload, prefixing a domain string for
// additional security.
bytes signature = 4;
}
26 changes: 26 additions & 0 deletions libp2p/record/pb/envelope_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions libp2p/record/pb/envelope_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""

import builtins
import google.protobuf.descriptor
import google.protobuf.message
import libp2p.crypto.pb.crypto_pb2
import typing

DESCRIPTOR: google.protobuf.descriptor.FileDescriptor

@typing.final
class Envelope(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PUBLIC_KEY_FIELD_NUMBER: builtins.int
PAYLOAD_TYPE_FIELD_NUMBER: builtins.int
PAYLOAD_FIELD_NUMBER: builtins.int
SIGNATURE_FIELD_NUMBER: builtins.int
payload_type: builtins.bytes
"""payload_type encodes the type of payload, so that it can be deserialized
deterministically.
"""
payload: builtins.bytes
"""payload is the actual payload carried inside this envelope."""
signature: builtins.bytes
"""signature is the signature produced by the private key corresponding to
the enclosed public key, over the payload, prefixing a domain string for
additional security.
"""
@property
def public_key(self) -> libp2p.crypto.pb.crypto_pb2.PublicKey:
"""public_key is the public key of the keypair the enclosed payload was
signed with.
"""

def __init__(
self,
*,
public_key: libp2p.crypto.pb.crypto_pb2.PublicKey | None = ...,
payload_type: builtins.bytes = ...,
payload: builtins.bytes = ...,
signature: builtins.bytes = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["public_key", b"public_key"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["payload", b"payload", "payload_type", b"payload_type", "public_key", b"public_key", "signature", b"signature"]) -> None: ...

global___Envelope = Envelope
62 changes: 62 additions & 0 deletions libp2p/record/record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from abc import ABC, abstractmethod
from typing import Dict, Type, Optional

# Registry of payload_type (bytes) -> Record class
_payload_type_registry: Dict[bytes, Type["Record"]] = {}

class Record(ABC):
"""
Record represents a data type that can be used as the payload of an Envelope.
"""

@abstractmethod
def domain(self) -> str:
"""The signature domain (unique string for this record type)."""
pass

@abstractmethod
def codec(self) -> bytes:
"""Binary identifier (payload type)."""
pass

@abstractmethod
def marshal_record(self) -> bytes:
"""Serialize the record into bytes."""
pass

@abstractmethod
def unmarshal_record(self, data: bytes) -> None:
"""Deserialize bytes into this record instance."""
pass


def register_type(prototype: Record) -> None:
"""
Register a record type by its codec.
Should be called in module init where the Record is defined just like the Go version.
"""
codec = prototype.codec()
if not isinstance(codec, (bytes, bytearray)):
raise TypeError("codec() must return bytes")
_payload_type_registry[bytes(codec)] = type(prototype)


def blank_record_for_payload_type(payload_type: bytes) -> Record:
"""
Create a blank record for the given payload type.
"""
cls = _payload_type_registry.get(payload_type)
if cls is None:
raise ValueError("payload type is not registered")
return cls()


def unmarshal_record_payload(payload_type: bytes, payload_bytes: bytes) -> Record:
"""
Given a payload type and bytes, return a fully unmarshalled Record.
"""
rec = blank_record_for_payload_type(payload_type)
rec.unmarshal_record(payload_bytes)
return rec


Loading
Loading