-
Notifications
You must be signed in to change notification settings - Fork 204
Feat/persist peer identity 312 #1185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
4ecf47e
e371c42
c23cf1e
da05feb
702a8c8
209969f
fa44f67
a5fad51
f52517c
a649460
29b72e7
9adca1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| """ | ||
| Identity persistence utilities for py-libp2p. | ||
|
|
||
| This module provides helper functions for saving, loading, and creating | ||
| peer identities. These utilities enable opt-in identity persistence without | ||
| changing the default behavior of generating random identities. | ||
|
|
||
| Example usage: | ||
| >>> from libp2p.identity_utils import save_identity, load_identity | ||
| >>> from libp2p.crypto.ed25519 import create_new_key_pair | ||
| >>> | ||
| >>> # Create and save an identity | ||
| >>> key_pair = create_new_key_pair() | ||
| >>> save_identity(key_pair, "my_peer.key") | ||
| >>> | ||
| >>> # Load it later | ||
| >>> loaded_key_pair = load_identity("my_peer.key") | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from libp2p.crypto.ed25519 import ( | ||
| Ed25519PrivateKey, | ||
| create_new_key_pair as create_new_ed25519_key_pair, | ||
| ) | ||
| from libp2p.crypto.keys import KeyPair | ||
|
|
||
|
|
||
| def save_identity(key_pair: KeyPair, filepath: str | Path) -> None: | ||
| """ | ||
| Save a keypair to disk for later reuse. | ||
|
|
||
| The private key is serialized and saved to the specified file. | ||
| The file should be kept secure as it contains the peer's private key. | ||
|
|
||
| Args: | ||
| key_pair: The KeyPair to save | ||
| filepath: Path where the private key will be saved | ||
|
|
||
| Raises: | ||
| OSError: If the file cannot be written | ||
|
|
||
| Example: | ||
| >>> from libp2p.crypto.ed25519 import create_new_key_pair | ||
| >>> key_pair = create_new_key_pair() | ||
| >>> save_identity(key_pair, "my_peer_identity.key") | ||
|
|
||
| """ | ||
| filepath = Path(filepath) | ||
|
|
||
| # Serialize the private key to bytes | ||
| private_key_bytes = key_pair.private_key.to_bytes() | ||
|
|
||
| # Write to file with restrictive permissions (owner read/write only) | ||
| filepath.write_bytes(private_key_bytes) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issue: If the parent directory doesn't exist, Recommendation: Create parent directories: filepath = Path(filepath)
filepath.parent.mkdir(parents=True, exist_ok=True)
private_key_bytes = key_pair.private_key.to_bytes()
filepath.write_bytes(private_key_bytes) |
||
| # Set file permissions to 0600 (owner read/write only) for security | ||
| try: | ||
| filepath.chmod(0o600) | ||
| except (OSError, NotImplementedError): | ||
| # Some filesystems don't support chmod, ignore the error | ||
| pass | ||
|
|
||
|
|
||
| def load_identity(filepath: str | Path) -> KeyPair: | ||
| """ | ||
| Load a keypair from disk. | ||
|
|
||
| Reads a previously saved private key and reconstructs the full keypair. | ||
| Currently only supports Ed25519 keys. | ||
|
|
||
| Args: | ||
| filepath: Path to the saved private key file | ||
|
|
||
| Returns: | ||
| KeyPair loaded from the file | ||
|
|
||
| Raises: | ||
| FileNotFoundError: If the file doesn't exist | ||
| ValueError: If the file contains invalid key data | ||
|
|
||
| Example: | ||
| >>> key_pair = load_identity("my_peer_identity.key") | ||
| >>> from libp2p import new_host | ||
| >>> host = new_host(key_pair=key_pair) | ||
|
|
||
| """ | ||
| filepath = Path(filepath) | ||
|
|
||
| # Read the private key bytes | ||
| private_key_bytes = filepath.read_bytes() | ||
|
|
||
| # Reconstruct the Ed25519 private key | ||
| private_key = Ed25519PrivateKey.from_bytes(private_key_bytes) | ||
|
|
||
| # Derive the public key | ||
| public_key = private_key.get_public_key() | ||
|
|
||
| return KeyPair(private_key, public_key) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No Private Key Validation on LoadIssue:
Impact: Corrupted identity files could:
Recommendation: Add validation: def load_identity(filepath: str | Path) -> KeyPair:
filepath = Path(filepath)
private_key_bytes = filepath.read_bytes()
# Ed25519 private keys are exactly 32 bytes
if len(private_key_bytes) != 32:
raise ValueError(f"Invalid private key file: expected 32 bytes, got {len(private_key_bytes)}")
# Reconstruct and validate
private_key = Ed25519PrivateKey.from_bytes(private_key_bytes)
# Verify key is valid by attempting to get public key
try:
public_key = private_key.get_public_key()
# Additional validation: verify we can serialize/deserialize roundtrip
_ = public_key.serialize()
except Exception as e:
raise ValueError(f"Invalid private key in file: {e}")
return KeyPair(private_key, public_key) |
||
|
|
||
| def create_identity_from_seed(seed: bytes) -> KeyPair: | ||
| """ | ||
| Create a deterministic identity from a seed. | ||
|
|
||
| The same seed will always produce the same keypair and peer ID. | ||
| This is useful for testing or when you want a deterministic identity | ||
| without saving keys to disk. | ||
|
|
||
| Args: | ||
| seed: A 32-byte seed for key generation. Must be exactly 32 bytes. | ||
|
|
||
| Returns: | ||
| KeyPair generated deterministically from the seed | ||
|
|
||
| Raises: | ||
| ValueError: If the seed is not 32 bytes or produces an invalid key | ||
|
|
||
| Example: | ||
| >>> seed = b"my_secret_seed_32_bytes_long!!!!" | ||
| >>> key_pair = create_identity_from_seed(seed) | ||
| >>> from libp2p import new_host | ||
| >>> host = new_host(key_pair=key_pair) | ||
| >>> # Same seed always produces same peer ID | ||
| >>> print(host.get_id()) | ||
|
|
||
| """ | ||
| if len(seed) != 32: | ||
| raise ValueError( | ||
| f"Seed must be exactly 32 bytes, got {len(seed)} bytes. " | ||
| "Consider using hashlib.sha256(your_seed).digest() " | ||
| "to derive a 32-byte seed." | ||
| ) | ||
|
|
||
| return create_new_ed25519_key_pair(seed=seed) | ||
|
|
||
|
|
||
| def identity_exists(filepath: str | Path) -> bool: | ||
| """ | ||
| Check if an identity file exists at the given path. | ||
|
|
||
| Args: | ||
| filepath: Path to check for an existing identity file | ||
|
|
||
| Returns: | ||
| True if the file exists, False otherwise | ||
|
|
||
| Example: | ||
| >>> if identity_exists("my_peer.key"): | ||
| ... key_pair = load_identity("my_peer.key") | ||
| ... else: | ||
| ... key_pair = create_new_key_pair() | ||
| ... save_identity(key_pair, "my_peer.key") | ||
|
|
||
| """ | ||
| return Path(filepath).exists() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue: Between
write_bytes()(line 55) andchmod()(line 59), the file exists with default permissions (often 0644). In a multi-user system, another process could read the private key during this window.Recommendation: Create file with restrictive permissions atomically: