Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Commit

Permalink
impl host key derivation from static secret (#146)
Browse files Browse the repository at this point in the history
* impl host key derivation from static secret

* update readme, note on backing up the secret

* remove libp2p, now unnecessary in main bin

* require secret be provided in config
  • Loading branch information
chunningham authored Jul 11, 2023
1 parent 20824ea commit c75fc3f
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 171 deletions.
144 changes: 76 additions & 68 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ base64 = "0.13"
futures = { default-features = false, version = "0.3", features = ["alloc", "std"] }
hyper = "0.14" # Prometheus server
lazy_static = "1.4.0"
libp2p = { default-features = false, version = "0.51.3" }
opentelemetry = { version = "0.17.0", features = ["rt-tokio"] }
opentelemetry-jaeger = { version = "0.16.0", features = ["rt-tokio", "reqwest_collector_client"] }
pin-project = "1"
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ The following common options are available:
| storage.blocks.type | KEPLER_STORAGE_BLOCKS_TYPE | Set the mode of block storage, options are "Local" and "S3" |
| storage.limit | KEPLER_STORAGE_LIMIT | Set a maximum limit on storage available to Orbits hosted on this instance. Limits are written as strings, e.g. `10 MiB`, `100 GiB` |
| storage.database | KEPLER_STORAGE_DATABASE | Set the location of the SQL database |
| storage.staging | KEPLER_STORAGE_STAGING | Set the mode of content staging, options are "Memory" and "FileSystem" |
| storage.staging | KEPLER_STORAGE_STAGING | Set the mode of content staging, options are "Memory" and "FileSystem" |
| keys.type | KEPLER_KEYS_TYPE | Set the type of host key store, options are "Static" |
| orbits.allowlist | KEPLER_ORBITS_ALLOWLIST | Set the URL of an allowlist service for gating the creation of Orbit Peers |

### Database Config
Expand Down Expand Up @@ -95,6 +96,20 @@ When `storage.blocks.type` is `S3` the instance will use the S3 AWS service for

Additionally, the following environment variables must be present: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_DEFAULT_REGION`.

### Keys Config

Kepler hosts require key pairs to provide replication. The `keys` config fields specify how a Kepler instance generates and stores these key pairs.

#### Static Secret Derivation

When `keys.type` is `Static` the instance will use an array of bytes as a static secret from which it will derive key pairs on a per-Orbit basis. The following config options will be available:

| Option | env var | description |
|:------------|:-------------------|:-----------------------------------------------------------------------------|
| keys.secret | KEPLER_KEYS_SECRET | Unpadded base64Url-encoded byte string from which key pairs will be derived. |

The secret MUST contain at least 32 bytes of entropy (either randomly generated or derived in a cryptographically secure way). It is STRONGLY RECOMMENDED that the secret be given via environment variables and NOT in the `kepler.toml` config file. Additionally it is STRONGLY RECOMMENDED that the secret be backed up in a secure place if used in production. Loss of the secret will result in total loss of function for the Kepler instance.

## Running

Kepler instances can be started via command line, e.g.:
Expand Down
2 changes: 1 addition & 1 deletion kepler-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ futures = { default-features = false, version = "0.3", features = ["alloc", "std
pin-project = "1"
time = "0.3"
kepler-lib = { version = "0.1", path = "../lib" }
libp2p = { version = "0.51.3", default-features = false }
libp2p = { version = "0.52.1", default-features = false, features = ["ed25519"] }
thiserror = "1"
ssi = "0.6"
serde = { version = "1", features = ["derive"] }
Expand Down
93 changes: 64 additions & 29 deletions kepler-core/src/db.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::events::{epoch_hash, Delegation, Event, HashError, Invocation, Operation, Revocation};
use crate::hash::Hash;
use crate::keys::Secrets;
use crate::migrations::Migrator;
use crate::models::*;
use crate::relationships::*;
Expand All @@ -13,6 +14,7 @@ use kepler_lib::{
authorization::{EncodingError, KeplerDelegation},
resource::OrbitId,
};
use libp2p::identity::PublicKey;
use sea_orm::{
entity::prelude::*,
error::{DbErr, RuntimeErr, SqlxError},
Expand All @@ -24,9 +26,10 @@ use sea_orm_migration::MigratorTrait;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct OrbitDatabase<C, B> {
pub struct OrbitDatabase<C, B, S> {
conn: C,
storage: B,
secrets: S,
}

#[derive(Debug, Clone)]
Expand All @@ -39,7 +42,7 @@ pub struct Commit {

#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum TxError<S: StorageSetup> {
pub enum TxError<S: StorageSetup, K: Secrets> {
#[error("database error: {0}")]
Db(#[from] DbErr),
#[error(transparent)]
Expand All @@ -58,20 +61,23 @@ pub enum TxError<S: StorageSetup> {
Encoding(#[from] EncodingError),
#[error(transparent)]
StoreSetup(S::Error),
#[error(transparent)]
Secrets(K::Error),
#[error("Orbit not found")]
OrbitNotFound,
}

#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum TxStoreError<B, S>
pub enum TxStoreError<B, S, K>
where
B: ImmutableReadStore + ImmutableWriteStore<S> + ImmutableDeleteStore + StorageSetup,
S: ImmutableStaging,
S::Writable: 'static + Unpin,
K: Secrets,
{
#[error(transparent)]
Tx(#[from] TxError<B>),
Tx(#[from] TxError<B, K>),
#[error(transparent)]
StoreRead(<B as ImmutableReadStore>::Error),
#[error(transparent)]
Expand All @@ -84,25 +90,51 @@ where
MissingInput,
}

impl<B, S> From<DbErr> for TxStoreError<B, S>
impl<B, S, K> From<DbErr> for TxStoreError<B, S, K>
where
B: ImmutableReadStore + ImmutableWriteStore<S> + ImmutableDeleteStore + StorageSetup,
S: ImmutableStaging,
S::Writable: 'static + Unpin,
K: Secrets,
{
fn from(e: DbErr) -> Self {
TxStoreError::Tx(e.into())
}
}

impl<B> OrbitDatabase<DatabaseConnection, B> {
pub async fn wrap(conn: DatabaseConnection, storage: B) -> Result<Self, DbErr> {
impl<B, K> OrbitDatabase<DatabaseConnection, B, K> {
pub async fn new(conn: DatabaseConnection, storage: B, secrets: K) -> Result<Self, DbErr> {
Migrator::up(&conn, None).await?;
Ok(Self { conn, storage })
Ok(Self {
conn,
storage,
secrets,
})
}
}

impl<C, B, K> OrbitDatabase<C, B, K>
where
K: Secrets,
{
pub async fn stage_key(&self, orbit: &OrbitId) -> Result<PublicKey, K::Error> {
self.secrets.stage_keypair(orbit).await
}
}

impl<C, B> OrbitDatabase<C, B>
impl<C, B, K> OrbitDatabase<C, B, K>
where
C: TransactionTrait,
{
// to allow users to make custom read queries
pub async fn readable(&self) -> Result<DatabaseTransaction, DbErr> {
self.conn
.begin_with_config(None, Some(sea_orm::AccessMode::ReadOnly))
.await
}
}

impl<C, B, K> OrbitDatabase<C, B, K>
where
B: StoreSize,
{
Expand All @@ -113,18 +145,22 @@ where

pub type InvocationInputs<W> = HashMap<(OrbitId, String), (Metadata, HashBuffer<W>)>;

impl<C, B> OrbitDatabase<C, B>
impl<C, B, K> OrbitDatabase<C, B, K>
where
C: TransactionTrait,
B: StorageSetup,
K: Secrets,
{
async fn transact(&self, events: Vec<Event>) -> Result<HashMap<OrbitId, Commit>, TxError<B>> {
async fn transact(
&self,
events: Vec<Event>,
) -> Result<HashMap<OrbitId, Commit>, TxError<B, K>> {
let tx = self
.conn
.begin_with_config(Some(sea_orm::IsolationLevel::ReadUncommitted), None)
.await?;

let commit = transact(&tx, &self.storage, events).await?;
let commit = transact(&tx, &self.storage, &self.secrets, events).await?;

tx.commit().await?;

Expand All @@ -134,26 +170,19 @@ where
pub async fn delegate(
&self,
delegation: Delegation,
) -> Result<HashMap<OrbitId, Commit>, TxError<B>> {
) -> Result<HashMap<OrbitId, Commit>, TxError<B, K>> {
self.transact(vec![Event::Delegation(Box::new(delegation))])
.await
}

pub async fn revoke(
&self,
revocation: Revocation,
) -> Result<HashMap<OrbitId, Commit>, TxError<B>> {
) -> Result<HashMap<OrbitId, Commit>, TxError<B, K>> {
self.transact(vec![Event::Revocation(Box::new(revocation))])
.await
}

// to allow users to make custom read queries
pub async fn readable(&self) -> Result<DatabaseTransaction, DbErr> {
self.conn
.begin_with_config(None, Some(sea_orm::AccessMode::ReadOnly))
.await
}

pub async fn invoke<S>(
&self,
invocation: Invocation,
Expand All @@ -163,7 +192,7 @@ where
HashMap<OrbitId, Commit>,
Vec<InvocationOutcome<B::Readable>>,
),
TxStoreError<B, S>,
TxStoreError<B, S, K>,
>
where
B: ImmutableWriteStore<S> + ImmutableDeleteStore + ImmutableReadStore,
Expand Down Expand Up @@ -219,6 +248,7 @@ where
let commit = transact(
&tx,
&self.storage,
&self.secrets,
vec![Event::Invocation(Box::new(invocation), ops)],
)
.await?;
Expand Down Expand Up @@ -288,7 +318,7 @@ pub enum InvocationOutcome<R> {
OpenSessions(HashMap<Hash, DelegationInfo>),
}

impl<S: StorageSetup> From<delegation::Error> for TxError<S> {
impl<S: StorageSetup, K: Secrets> From<delegation::Error> for TxError<S, K> {
fn from(e: delegation::Error) -> Self {
match e {
delegation::Error::InvalidDelegation(e) => Self::InvalidDelegation(e),
Expand All @@ -297,7 +327,7 @@ impl<S: StorageSetup> From<delegation::Error> for TxError<S> {
}
}

impl<S: StorageSetup> From<invocation::Error> for TxError<S> {
impl<S: StorageSetup, K: Secrets> From<invocation::Error> for TxError<S, K> {
fn from(e: invocation::Error) -> Self {
match e {
invocation::Error::InvalidInvocation(e) => Self::InvalidInvocation(e),
Expand All @@ -306,7 +336,7 @@ impl<S: StorageSetup> From<invocation::Error> for TxError<S> {
}
}

impl<S: StorageSetup> From<revocation::Error> for TxError<S> {
impl<S: StorageSetup, K: Secrets> From<revocation::Error> for TxError<S, K> {
fn from(e: revocation::Error) -> Self {
match e {
revocation::Error::InvalidRevocation(e) => Self::InvalidRevocation(e),
Expand Down Expand Up @@ -366,11 +396,12 @@ async fn event_orbits<'a, C: ConnectionTrait>(
Ok(orbits)
}

pub(crate) async fn transact<C: ConnectionTrait, S: StorageSetup>(
pub(crate) async fn transact<C: ConnectionTrait, S: StorageSetup, K: Secrets>(
db: &C,
store_setup: &S,
secrets: &K,
events: Vec<Event>,
) -> Result<HashMap<OrbitId, Commit>, TxError<S>> {
) -> Result<HashMap<OrbitId, Commit>, TxError<S, K>> {
// for each event, get the hash and the relevent orbit(s)
let event_hashes = events
.into_iter()
Expand Down Expand Up @@ -576,6 +607,10 @@ pub(crate) async fn transact<C: ConnectionTrait, S: StorageSetup>(
.create(&orbit.0)
.await
.map_err(TxError::StoreSetup)?;
secrets
.save_keypair(&orbit.0)
.await
.map_err(TxError::Secrets)?;
}

Ok(orbit_order
Expand Down Expand Up @@ -688,10 +723,10 @@ async fn get_kv_entity<C: ConnectionTrait>(
// })
}

async fn get_valid_delegations<C: ConnectionTrait, S: StorageSetup>(
async fn get_valid_delegations<C: ConnectionTrait, S: StorageSetup, K: Secrets>(
db: &C,
orbit: &OrbitId,
) -> Result<HashMap<Hash, DelegationInfo>, TxError<S>> {
) -> Result<HashMap<Hash, DelegationInfo>, TxError<S, K>> {
let (dels, abilities): (Vec<delegation::Model>, Vec<Vec<abilities::Model>>) =
delegation::Entity::find()
.left_join(revocation::Entity)
Expand Down
78 changes: 78 additions & 0 deletions kepler-core/src/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use kepler_lib::{
libipld::cid::multihash::{Blake3_256, Hasher},
resource::OrbitId,
};
use libp2p::{
identity::{
ed25519::{Keypair as EdKP, SecretKey},
DecodingError, Keypair, PublicKey,
},
PeerId,
};
use sea_orm_migration::async_trait::async_trait;
use std::error::Error as StdError;

#[async_trait]
pub trait Secrets {
type Error: StdError;
async fn get_keypair(&self, orbit: &OrbitId) -> Result<Keypair, Self::Error>;
async fn get_pubkey(&self, orbit: &OrbitId) -> Result<PublicKey, Self::Error> {
Ok(self.get_keypair(orbit).await?.public())
}
async fn stage_keypair(&self, orbit: &OrbitId) -> Result<PublicKey, Self::Error>;
async fn save_keypair(&self, orbit: &OrbitId) -> Result<(), Self::Error>;
async fn get_peer_id(&self, orbit: &OrbitId) -> Result<PeerId, Self::Error> {
Ok(self.get_pubkey(orbit).await?.to_peer_id())
}
}

#[async_trait]
pub trait SecretsSetup {
type Error: StdError;
type Input;
type Output: Secrets;
async fn setup(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
}

#[derive(Clone)]
pub struct StaticSecret {
secret: Vec<u8>,
}

impl StaticSecret {
pub fn new(secret: Vec<u8>) -> Result<Self, Vec<u8>> {
if secret.len() < 32 {
Err(secret)
} else {
Ok(Self { secret })
}
}
}

#[async_trait]
impl Secrets for StaticSecret {
type Error = DecodingError;
async fn get_keypair(&self, orbit: &OrbitId) -> Result<Keypair, Self::Error> {
let mut hasher = Blake3_256::default();
hasher.update(&self.secret);
hasher.update(orbit.to_string().as_bytes());
let derived = hasher.finalize().to_vec();
Ok(EdKP::from(SecretKey::try_from_bytes(derived)?).into())
}
async fn stage_keypair(&self, orbit: &OrbitId) -> Result<PublicKey, Self::Error> {
self.get_pubkey(orbit).await
}
async fn save_keypair(&self, _orbit: &OrbitId) -> Result<(), Self::Error> {
Ok(())
}
}

#[async_trait]
impl SecretsSetup for StaticSecret {
type Error = std::convert::Infallible;
type Input = ();
type Output = Self;
async fn setup(&self, _input: Self::Input) -> Result<Self::Output, Self::Error> {
Ok(self.clone())
}
}
2 changes: 2 additions & 0 deletions kepler-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod db;
pub mod events;
pub mod hash;
pub mod keys;
pub mod manifest;
pub mod migrations;
pub mod models;
Expand All @@ -10,5 +11,6 @@ pub mod types;
pub mod util;

pub use db::{Commit, InvocationOutcome, OrbitDatabase, TxError, TxStoreError};
pub use libp2p;
pub use sea_orm;
pub use sea_orm_migration;
Loading

0 comments on commit c75fc3f

Please sign in to comment.