Documentation cleanup #5
16 changed files with 314 additions and 133 deletions
21
.drone.yml
21
.drone.yml
|
@ -24,15 +24,26 @@ steps:
|
||||||
image: clux/muslrust
|
image: clux/muslrust
|
||||||
commands:
|
commands:
|
||||||
- cargo build --release
|
- cargo build --release
|
||||||
- strip target/x86_64-unknown-linux-musl/release/coffer-server
|
- mkdir coffer-${DRONE_TAG}-x86_64-musl
|
||||||
- strip target/x86_64-unknown-linux-musl/release/coffer-client
|
- mv target/x86_64-unknown-linux-musl/release/coffer-server \
|
||||||
- strip target/x86_64-unknown-linux-musl/release/coffer-companion
|
target/x86_64-unknown-linux-musl/release/coffer-client \
|
||||||
|
target/x86_64-unknown-linux-musl/release/coffer-companion \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl
|
||||||
|
- strip coffer-${DRONE_TAG}-x86_64-musl/coffer-server
|
||||||
|
- strip coffer-${DRONE_TAG}-x86_64-musl/coffer-client
|
||||||
|
- strip coffer-${DRONE_TAG}-x86_64-musl/coffer-companion
|
||||||
|
|
||||||
- name: package
|
- name: package
|
||||||
image: alpine
|
image: alpine
|
||||||
commands:
|
commands:
|
||||||
- tar cjf coffer-${DRONE_TAG}-x86_64-musl.tar.bz2 target/x86_64-unknown-linux-musl/release/coffer-server target/x86_64-unknown-linux-musl/release/coffer-client target/x86_64-unknown-linux-musl/release/coffer-companion
|
- tar cjf coffer-${DRONE_TAG}-x86_64-musl.tar.bz2 \
|
||||||
- tar czf coffer-${DRONE_TAG}-x86_64-musl.tar.gz target/x86_64-unknown-linux-musl/release/coffer-server target/x86_64-unknown-linux-musl/release/coffer-client target/x86_64-unknown-linux-musl/release/coffer-companion
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-server \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-client \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-companion
|
||||||
|
- tar czf coffer-${DRONE_TAG}-x86_64-musl.tar.gz \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-server \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-client \
|
||||||
|
coffer-${DRONE_TAG}-x86_64-musl/coffer-companion
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
image: plugins/gitea-release
|
image: plugins/gitea-release
|
||||||
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +1,4 @@
|
||||||
*.enc filter=lfs diff=lfs merge=lfs -text
|
*.enc filter=lfs diff=lfs merge=lfs -text
|
||||||
*.cert filter=lfs diff=lfs merge=lfs -text
|
*.cert filter=lfs diff=lfs merge=lfs -text
|
||||||
|
overview.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
overview.svg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -88,9 +88,7 @@ dependencies = [
|
||||||
"exec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"exec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"serde_cbor 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_cbor 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"structopt 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"structopt 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
76
README.md
76
README.md
|
@ -1 +1,77 @@
|
||||||
# Coffer
|
# Coffer
|
||||||
|
[![Build Status](https://drone.friedl.net/api/badges/incubator/coffer/status.svg)](https://drone.friedl.net/incubator/coffer)
|
||||||
|
|
||||||
|
Coffer is a collection of tools for the simple but secure management of
|
||||||
|
application configuration.
|
||||||
|
|
||||||
|
It is meant to be flexible and simple, hence does not assume much about your
|
||||||
|
environment. Especially, you don't need a kubernetes cluster for running coffer.
|
||||||
|
Coffer runs directly on your server, just as well as in a containerized setup or
|
||||||
|
a full kubernetes cluster. The only thing Coffer needs is a TCP connection
|
||||||
|
between the `coffer-server` (securely holding your configuration) and the
|
||||||
|
`coffer-client` (retrieving configuration and setting up your application).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
![](overview.png)
|
||||||
|
|
||||||
|
## The Parts of Coffer
|
||||||
|
Coffer is split into 3 binaries and a supporting library:
|
||||||
|
|
||||||
|
* `coffer-server`: The `coffer-server` securely stores configuration data and
|
||||||
|
hands them out to `coffer-clients` upon request
|
||||||
|
* `coffer-client`: A `coffer-client` requests configuration data from a
|
||||||
|
`coffer-server`, configures the application and may also start it up
|
||||||
|
* `coffer-companion`: A helper for generating certificates and encrypting
|
||||||
|
configuration data
|
||||||
|
* `coffer-common`: A common library for all binaries containing common
|
||||||
|
cryptographic operations, protocol code and interface definitions
|
||||||
|
|
||||||
|
## Security
|
||||||
|
Coffer does not rely on a secure connection or any specifics of the environment.
|
||||||
|
Instead security is provided by a basic public key cryptography scheme. This
|
||||||
|
gives you, the user, the flexibility to set up your ecosystem according to your
|
||||||
|
own security needs.
|
||||||
|
|
||||||
|
### Certificates
|
||||||
|
Certificates in coffer are the just a keypair consisting of a public and private
|
||||||
|
key as used by public key cryptography. So, basically, a certificate is nothing
|
||||||
|
more than a tuple of two primes.
|
||||||
|
|
||||||
|
Certificates in coffer can be generated by the `coffer-companion`. Every
|
||||||
|
`coffer-server` and `coffer-client` need their own certificate. Security
|
||||||
|
in coffer squarely depends on these certificates being kept secret.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Configuration can be written in [toml](https://github.com/toml-lang/toml)
|
||||||
|
format. It is secured by encrypting it with the public key of the
|
||||||
|
`coffer-server`. This can be done by invoking the `coffer-companion`.
|
||||||
|
|
||||||
|
Encrypted configuration can be conveniently stored in VCS (e.g. via git-lfs)
|
||||||
|
with your application. As long as the server certificate stays private.
|
||||||
|
|
||||||
|
### Communication
|
||||||
|
The communication between a `coffer-server` and a `coffer-client` is not and
|
||||||
|
does not need to be secured. A `coffer-server` associates configuration data
|
||||||
|
with the public keys of `coffer-clients`. Upon request of configuration data the
|
||||||
|
server sends back the configuration encrypted with the clients public key. Only
|
||||||
|
a client in posession of the corresponding private key can read the response.
|
||||||
|
|
||||||
|
### Trust Anchors
|
||||||
|
It is worth mentioning some things about trust anchors. Every cryptography
|
||||||
|
scheme, no matter how sophisticated, needs, at some point, something that can be
|
||||||
|
trusted. In case of HTTPS this is provided by central certificate authorities.
|
||||||
|
In case of the encrypted letters to your best friend this might be a pre-shared
|
||||||
|
password transmitted over phone in 1972.
|
||||||
|
|
||||||
|
From a coffer perspective keys can be trusted. That is, coffer assumes that
|
||||||
|
certificates are distributed and kept secret according to your threat model. An
|
||||||
|
attacker in control of a certificate can steal secret configuration!
|
||||||
|
|
||||||
|
Coffer does not assume a trust anchor for you. Instead, you are free to choose
|
||||||
|
your own trust anchor according. In a simple personal server setup this might
|
||||||
|
mean just distributing certificates by hand. In a more complex, corporate
|
||||||
|
environment you may want to set up a secure, central authority.
|
||||||
|
|
||||||
|
Trust anchors are a trade-off between convenience, complexity and security.
|
||||||
|
Coffer lets _you_ choose where along these axis you put your trust anchor.
|
||||||
|
|
||||||
|
|
27
TODO.org
27
TODO.org
|
@ -1,27 +0,0 @@
|
||||||
#+TODO: TODO NEXT DONE
|
|
||||||
|
|
||||||
* General
|
|
||||||
** TODO Add a license
|
|
||||||
** TODO Better communication protocol
|
|
||||||
** TODO Add tests
|
|
||||||
** TODO Readme
|
|
||||||
* Coffer Server
|
|
||||||
** TODO Double delete if files not kept
|
|
||||||
** TODO Add secrets on-the-fly
|
|
||||||
** TODO Store secrets in secure memory
|
|
||||||
- Not persisted
|
|
||||||
- Nulled out
|
|
||||||
- Optional encrypted
|
|
||||||
* Coffer Client
|
|
||||||
** DONE Set environment variables
|
|
||||||
CLOSED: [2019-11-27 Wed 22:51]
|
|
||||||
** TODO Send key requests encrypted/signed
|
|
||||||
** TODO Secure Communication
|
|
||||||
* Coffer Companion
|
|
||||||
** TODO Add Subcommands
|
|
||||||
- [X] Generate certificate
|
|
||||||
- [ ] Seal secrets with certificate
|
|
||||||
- [ ] Open secrets with certificate
|
|
||||||
- [ ] Generate trampolin sh from dockerfile
|
|
||||||
* Docker
|
|
||||||
** TODO Create Dockerfile for server
|
|
|
@ -12,8 +12,6 @@ log = "0.4"
|
||||||
env_logger="0.7"
|
env_logger="0.7"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
# Communication
|
# Communication
|
||||||
serde = { version = "1.0", features = ["derive"]}
|
|
||||||
serde_yaml = "0.8"
|
|
||||||
serde_cbor = "0.10.2"
|
serde_cbor = "0.10.2"
|
||||||
# Executing subcommand
|
# Executing subcommand
|
||||||
exec = "0.3.1"
|
exec = "0.3.1"
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
|
//! # Coffer client
|
||||||
|
//!
|
||||||
|
//! Retrieve a secret shard from a `coffer-server`. Secrets in the shard are set
|
||||||
|
//! as environment variables for the spawned subcommand `cmd`.
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use env_logger;
|
use env_logger;
|
||||||
|
|
||||||
use structopt::StructOpt;
|
|
||||||
|
|
||||||
use std:: {
|
use std:: {
|
||||||
net::{SocketAddr, TcpStream},
|
net::TcpStream,
|
||||||
error::Error,
|
error::Error,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
io::{Write, Read},
|
io::{Write, Read},
|
||||||
convert::{TryInto, TryFrom}
|
convert::{TryInto, TryFrom}
|
||||||
};
|
};
|
||||||
|
|
||||||
use coffer_common::certificate::Certificate;
|
use coffer_common::{
|
||||||
use coffer_common::coffer::{CofferShard, CofferValue};
|
coffer::{CofferShard, CofferValue},
|
||||||
|
certificate::Certificate
|
||||||
|
};
|
||||||
|
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
/// Client for setting up the environment from coffer server secrets
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Address of the coffer server
|
/// Address of the coffer server
|
||||||
|
@ -75,6 +83,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
Err("Could not spawn sub-command".into())
|
Err("Could not spawn sub-command".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replaces the `coffer-client` process image with
|
||||||
|
/// the subcommand `cmd` with `args`
|
||||||
fn reap_coffer(cmd: &str, args: &[String]) {
|
fn reap_coffer(cmd: &str, args: &[String]) {
|
||||||
let mut cmd = exec::Command::new(cmd);
|
let mut cmd = exec::Command::new(cmd);
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
//! Common certificate handling and encryption
|
//! A keypair container providing functionality for signing, encryption and
|
||||||
|
//! decryption
|
||||||
|
//!
|
||||||
|
//! # Base libraries
|
||||||
|
//! The cryptographic operations exposed by this module are based on the
|
||||||
|
//! [NaCl](http://nacl.cr.yp.to/) fork [libsodium](https://libsodium.org) as
|
||||||
|
//! exposed by the rust bindings [sodiumoxide](https://crates.io/crates/sodiumoxide).
|
||||||
|
//!
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use std::path::Path;
|
use std::{
|
||||||
use std::io::BufReader;
|
path::Path,
|
||||||
use std::fs::File;
|
io::BufReader,
|
||||||
use std::fmt::{Debug, Formatter};
|
fs::File,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::ops::Deref; // we use this but rustc doesn't know
|
||||||
|
|
||||||
use quick_error::quick_error;
|
use quick_error::quick_error;
|
||||||
|
|
||||||
use seckey::SecKey;
|
use seckey::SecKey;
|
||||||
use sodiumoxide::crypto::box_;
|
use sodiumoxide::crypto::{box_, sealedbox};
|
||||||
use sodiumoxide::crypto::sealedbox;
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use serde_cbor;
|
use serde_cbor;
|
||||||
|
|
||||||
|
@ -25,22 +34,51 @@ quick_error! {
|
||||||
Io(err: std::io::Error) {
|
Io(err: std::io::Error) {
|
||||||
from()
|
from()
|
||||||
}
|
}
|
||||||
SecKey
|
SecKey {
|
||||||
|
from(CertificateInner)
|
||||||
|
}
|
||||||
Crypto
|
Crypto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A secure container for certificates
|
/// A secure container for a keypair
|
||||||
///
|
///
|
||||||
/// # Certificate
|
/// Secure means a best effort approach to:
|
||||||
|
/// - Prevent swapping memory to disk
|
||||||
|
/// - Zeroing out memory upon dropping
|
||||||
|
/// - Prevent other processes and buffer overflows to access the secure memory
|
||||||
|
/// area
|
||||||
///
|
///
|
||||||
/// A certificate consists of a public and a private key in a secure memory
|
/// These guarantees are currently *not* reliable. If you threat model contains
|
||||||
/// area. With a certificate data sealed and opened.
|
/// targeted attacks against coffer memory, additional precautions have to be
|
||||||
|
/// taken.
|
||||||
pub struct Certificate {
|
pub struct Certificate {
|
||||||
inner: SecKey<CertificateInner>
|
inner: SecKey<CertificateInner>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The SecKeyReadGuard prevents convenience methods for handing out references
|
||||||
|
// to private/public keys (reference outlives SecKeyReadGuard). Hence below
|
||||||
|
// macros are shortcut projections that can be used after a read guard is
|
||||||
|
// created
|
||||||
|
|
||||||
|
// Get the public key
|
||||||
|
macro_rules! pk {
|
||||||
|
($cert:ident) => {
|
||||||
|
&$cert.inner.read().public_key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the private key
|
||||||
|
macro_rules! sk {
|
||||||
|
($cert:ident) => {
|
||||||
|
&$cert.inner.read().private_key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate and its inner SecKey own their
|
||||||
|
// raw pointer without any thread local behaviour
|
||||||
unsafe impl Send for Certificate {}
|
unsafe impl Send for Certificate {}
|
||||||
|
// After initialization, certificate is read-only
|
||||||
unsafe impl Sync for Certificate {}
|
unsafe impl Sync for Certificate {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -49,13 +87,8 @@ struct CertificateInner {
|
||||||
private_key: box_::SecretKey
|
private_key: box_::SecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for CertificateInner {
|
|
||||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(fmt, "<Certificate Hidden>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Certificate {
|
impl Certificate {
|
||||||
|
/// Initialize with a generated keypair
|
||||||
pub fn new() -> Result<Certificate, CertificateError> {
|
pub fn new() -> Result<Certificate, CertificateError> {
|
||||||
debug!{"Generating new certificate"}
|
debug!{"Generating new certificate"}
|
||||||
let (public_key, private_key) = box_::gen_keypair();
|
let (public_key, private_key) = box_::gen_keypair();
|
||||||
|
@ -66,6 +99,7 @@ impl Certificate {
|
||||||
Ok(Certificate{inner})
|
Ok(Certificate{inner})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize from a serialized certificate in [cbor](https://cbor.io/) format
|
||||||
pub fn new_from_cbor<T: AsRef<Path>>(path: T) -> Result<Certificate, CertificateError> {
|
pub fn new_from_cbor<T: AsRef<Path>>(path: T) -> Result<Certificate, CertificateError> {
|
||||||
debug!{"Reading certificate from {}", path.as_ref().display()}
|
debug!{"Reading certificate from {}", path.as_ref().display()}
|
||||||
let f = File::open(path)?;
|
let f = File::open(path)?;
|
||||||
|
@ -76,40 +110,35 @@ impl Certificate {
|
||||||
Ok(Certificate{inner})
|
Ok(Certificate{inner})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize a certificate to a file in [cbor](https://cbor.io/) format
|
||||||
#[cfg(feature = "export")]
|
#[cfg(feature = "export")]
|
||||||
pub fn to_cbor(&self) -> Result<Vec<u8>, CertificateError> {
|
pub fn to_cbor(&self) -> Result<Vec<u8>, CertificateError> {
|
||||||
let inner_cert = &*self.inner.read();
|
let read_guard = self.inner.read();
|
||||||
let cbor = serde_cbor::to_vec(inner_cert)?;
|
|
||||||
|
let cbor = serde_cbor::to_vec(read_guard.deref())?;
|
||||||
|
|
||||||
Ok(cbor)
|
Ok(cbor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clone the bytes of the public key
|
||||||
pub fn public_key(&self) -> Vec<u8> {
|
pub fn public_key(&self) -> Vec<u8> {
|
||||||
self.inner.read().public_key.as_ref().to_owned()
|
pk!(self).as_ref().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clone the bytes of the private key
|
||||||
#[cfg(feature = "export")]
|
#[cfg(feature = "export")]
|
||||||
pub fn secret_key(&self) -> Vec<u8> {
|
pub fn secret_key(&self) -> Vec<u8> {
|
||||||
self.inner.read().private_key.as_ref().to_owned()
|
sk!(self).as_ref().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a [sealed box](https://download.libsodium.org/doc/public-key_cryptography/sealed_boxes)
|
||||||
pub fn open(&self, c: &[u8]) -> Result<Vec<u8>, CertificateError> {
|
pub fn open(&self, c: &[u8]) -> Result<Vec<u8>, CertificateError> {
|
||||||
let pk = &self.inner.read().public_key;
|
sealedbox::open(c, pk!{self}, sk!{self})
|
||||||
let sk = &self.inner.read().private_key;
|
|
||||||
|
|
||||||
sealedbox::open(c, pk, sk)
|
|
||||||
.map_err(|_| CertificateError::Crypto)
|
.map_err(|_| CertificateError::Crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seal a message in a [sealed box](https://download.libsodium.org/doc/public-key_cryptography/sealed_boxes)
|
||||||
pub fn seal(&self, message: &[u8]) -> Result<Vec<u8>, CertificateError> {
|
pub fn seal(&self, message: &[u8]) -> Result<Vec<u8>, CertificateError> {
|
||||||
let pk = &self.inner.read().public_key;
|
Ok(sealedbox::seal(message, pk!{self}))
|
||||||
|
|
||||||
Ok(sealedbox::seal(message, pk))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <T: AsRef<Path>> From<T> for Certificate {
|
|
||||||
fn from(path: T) -> Self {
|
|
||||||
Certificate::new_from_cbor(&path)
|
|
||||||
.expect(&format!{"Could not read certificate from {}", path.as_ref().display()})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,85 @@
|
||||||
//! A storage container for client data
|
//! A storage container for client data
|
||||||
|
//!
|
||||||
|
//! Coffer supports separated client data by `CofferShard`s. Content of a shard
|
||||||
|
//! is a key-value store with typed values.
|
||||||
|
//!
|
||||||
|
//! # Coffer files
|
||||||
|
//! A `Coffer` can be read from a [toml](https://github.com/toml-lang/)
|
||||||
|
//! file in a specific format.
|
||||||
|
//!
|
||||||
|
//! ## Shards
|
||||||
|
//! A `CofferShard` is identified by a toml table with a field `id` containing
|
||||||
|
//! the unique shard id.
|
||||||
|
//!
|
||||||
|
//! Tables can be nested for grouping shards together. The grouping is not
|
||||||
|
//! necessarily reflected in the deserialized `Coffer`, as shards can be
|
||||||
|
//! uniquely identified by their id.
|
||||||
|
//!
|
||||||
|
//! Shards (tables with an id) cannot be nested.
|
||||||
|
//!
|
||||||
|
//! A simple shard with no data
|
||||||
|
//! ```toml
|
||||||
|
//! [app]
|
||||||
|
//! id = "1"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Grouped shard
|
||||||
|
//! ```toml
|
||||||
|
//! [app]
|
||||||
|
//! [app.frontend]
|
||||||
|
//! id = "1"
|
||||||
|
//!
|
||||||
|
//! [app.backend]
|
||||||
|
//! id = "2"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Nested shards (invalid)
|
||||||
|
//! ```toml
|
||||||
|
//! [app]
|
||||||
|
//! id = "1" # app is a shard since it has an id
|
||||||
|
//! [app.frontend] # invalid, can't nest shards inside other shards
|
||||||
|
//! id = "2"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Values
|
||||||
|
//! Shards can contain a subset of toml values. The currently supported toml
|
||||||
|
//! values are:
|
||||||
|
//! - [String](https://github.com/toml-lang/toml#user-content-string)
|
||||||
|
//! - [Integer](https://github.com/toml-lang/toml#user-content-integer)
|
||||||
|
//! - [Float](https://github.com/toml-lang/toml#user-content-float)
|
||||||
|
//! - [Boolean](https://github.com/toml-lang/toml#user-content-boolean)
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//! ```toml
|
||||||
|
//! [app]
|
||||||
|
//! [app.frontend]
|
||||||
|
//! id = "1"
|
||||||
|
//! password = "admin"
|
||||||
|
//! font_size = 1.4
|
||||||
|
//!
|
||||||
|
//! [app.backend]
|
||||||
|
//! id = "2"
|
||||||
|
//! cors = true
|
||||||
|
//!
|
||||||
|
//! [database]
|
||||||
|
//! id = "0"
|
||||||
|
//! user = "root"
|
||||||
|
//! passwort = "toor"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::{
|
||||||
use std::path::Path;
|
fmt::Debug,
|
||||||
use std::fs::File;
|
fs::File,
|
||||||
use std::io::{BufReader, Read};
|
io::{BufReader, Read},
|
||||||
|
path::Path,
|
||||||
use toml::Value as TomlValue;
|
};
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use quick_error::quick_error;
|
use quick_error::quick_error;
|
||||||
|
use toml::Value as TomlValue;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
quick_error! {
|
quick_error! {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -28,7 +96,7 @@ quick_error! {
|
||||||
|
|
||||||
pub type CofferResult<T> = Result<T, CofferError>;
|
pub type CofferResult<T> = Result<T, CofferError>;
|
||||||
|
|
||||||
/// Values supported by a `Coffer`
|
/// Values supported by `Coffer`
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum CofferValue {
|
pub enum CofferValue {
|
||||||
/// A UTF-8 encoded string
|
/// A UTF-8 encoded string
|
||||||
|
@ -37,35 +105,38 @@ pub enum CofferValue {
|
||||||
Integer(i32),
|
Integer(i32),
|
||||||
/// A 32-bit float
|
/// A 32-bit float
|
||||||
Float(f32),
|
Float(f32),
|
||||||
// A boolean
|
/// A boolean value
|
||||||
Boolean(bool)
|
Boolean(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `CofferKey` defining the shard and the key into the kv-store
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub struct CofferKey {
|
pub struct CofferKey {
|
||||||
pub shard: String,
|
pub shard: String,
|
||||||
pub key: String
|
pub key: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
/// A key-value store for client data
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct CofferShard(pub Vec<(String, CofferValue)>);
|
pub struct CofferShard(pub Vec<(String, CofferValue)>);
|
||||||
|
|
||||||
/// Interface for interacting with a `Coffer`
|
/// Trait for `Coffer`
|
||||||
pub trait Coffer {
|
pub trait Coffer {
|
||||||
/// Put `value` at `path`. Errors if there is already a value at `path`.
|
/// Put `value` at `key`. Errors if there is already a value for `key`.
|
||||||
fn put(&mut self, key: CofferKey, value: CofferValue) -> CofferResult<()>;
|
fn put(&mut self, key: CofferKey, value: CofferValue) -> CofferResult<()>;
|
||||||
|
|
||||||
/// Push `value` to `path`. Replaces existing values.
|
/// Push `value` to `key`. Replaces existing values.
|
||||||
fn push(&mut self, key: CofferKey, value: CofferValue);
|
fn push(&mut self, key: CofferKey, value: CofferValue);
|
||||||
|
|
||||||
/// Retrieve `value` at path. Errors if there is no `value` at path.
|
/// Retrieve `value` at path. `None` if there is no `value` for `key`.
|
||||||
fn get(&self, key: &CofferKey) -> CofferResult<CofferValue>;
|
fn get(&self, key: &CofferKey) -> Option<CofferValue>;
|
||||||
|
|
||||||
/// Retrieve `value` at path. Errors if there is no `value` at path.
|
/// Retrieve an entire shard. `None` if there is no `CofferShard` for `shard`.
|
||||||
fn get_shard<T>(&self, shard: T) -> CofferResult<CofferShard>
|
fn get_shard<T>(&self, shard: T) -> Option<CofferShard>
|
||||||
where T: AsRef<str>;
|
where T: AsRef<str>;
|
||||||
|
|
||||||
fn from_toml_file(toml_path: &Path) -> Self
|
/// Deserializes a `Coffer` from a toml file
|
||||||
|
fn from_toml_path(toml_path: &Path) -> Self
|
||||||
where Self: Coffer + Default
|
where Self: Coffer + Default
|
||||||
{
|
{
|
||||||
// read the secrets file into a temporary string
|
// read the secrets file into a temporary string
|
||||||
|
@ -135,4 +206,5 @@ pub trait Coffer {
|
||||||
self.put(CofferKey{shard, key}, value).unwrap();
|
self.put(CofferKey{shard, key}, value).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//! Certificate of the keyring owner plus known and trusted public keys
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
|
@ -36,28 +37,36 @@ quick_error! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keyring container
|
||||||
|
///
|
||||||
|
/// A keyring constists of the owner's certificate and
|
||||||
|
/// the known and trusted public keys of the keyring owner
|
||||||
pub struct Keyring {
|
pub struct Keyring {
|
||||||
certificate: Certificate,
|
certificate: Certificate,
|
||||||
known_keys: HashMap<Vec<u8>, box_::PublicKey>
|
known_keys: HashMap<Vec<u8>, box_::PublicKey>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keyring {
|
impl Keyring {
|
||||||
|
/// Create a new keyring
|
||||||
pub fn new(certificate: Certificate) -> Keyring {
|
pub fn new(certificate: Certificate) -> Keyring {
|
||||||
Keyring {
|
Keyring {
|
||||||
certificate: certificate,
|
certificate,
|
||||||
known_keys: HashMap::new()
|
known_keys: HashMap::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a keyring from a file in [cbor](https://cbor.io) format
|
||||||
pub fn new_from_path<T>(certificate_path: T) -> Keyring
|
pub fn new_from_path<T>(certificate_path: T) -> Keyring
|
||||||
where T: AsRef<Path>
|
where T: AsRef<Path>
|
||||||
{
|
{
|
||||||
Keyring {
|
Keyring {
|
||||||
certificate: Certificate::from(certificate_path),
|
certificate: Certificate::new_from_cbor(certificate_path).unwrap(),
|
||||||
known_keys: HashMap::new()
|
known_keys: HashMap::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add the table ids of a `Coffer` in toml format as known keys to the keyring
|
||||||
|
// TODO: This needs to be refactored. Keyring shouldn't be that tightly bound to coffer format
|
||||||
pub fn add_known_keys_toml(&mut self, toml: &str) -> Result<(), KeyringError> {
|
pub fn add_known_keys_toml(&mut self, toml: &str) -> Result<(), KeyringError> {
|
||||||
// parse the string into a toml Table
|
// parse the string into a toml Table
|
||||||
let clients: toml::value::Table = match toml.parse::<TomlValue>().unwrap() {
|
let clients: toml::value::Table = match toml.parse::<TomlValue>().unwrap() {
|
||||||
|
@ -100,11 +109,21 @@ impl Keyring {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a sealed message with the keyring owner's certificate
|
||||||
pub fn open(&self, message: &[u8]) -> Result<Vec<u8>, KeyringError> {
|
pub fn open(&self, message: &[u8]) -> Result<Vec<u8>, KeyringError> {
|
||||||
self.certificate.open(message)
|
self.certificate.open(message)
|
||||||
.map_err(KeyringError::from)
|
.map_err(KeyringError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seal a message for a client in the keyring
|
||||||
|
// TODO: Does this make sense?
|
||||||
|
// - What is a client in context of a keyring?
|
||||||
|
// - Why do we need to store trusted public keys in a keyring and not just
|
||||||
|
// encrypt for a pub key. Sealing does not need certificate.
|
||||||
|
// => We need authenticated encryption here. Sealed boxes can be tampered with by a MITM
|
||||||
|
// https://download.libsodium.org/doc/public-key_cryptography/authenticated_encryption
|
||||||
|
// I.e. a client could retrieve forged secrets by a attacker-controlled server, even if the
|
||||||
|
// real server's certificate was not exposed.
|
||||||
pub fn seal(&self, client: &[u8], message: &[u8]) -> Result<Vec<u8>, KeyringError> {
|
pub fn seal(&self, client: &[u8], message: &[u8]) -> Result<Vec<u8>, KeyringError> {
|
||||||
let client_key = self.known_keys.get(client)
|
let client_key = self.known_keys.get(client)
|
||||||
.ok_or(KeyringError::UnkownClientKey)?;
|
.ok_or(KeyringError::UnkownClientKey)?;
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
//! Common base for coffer binaries
|
//! Common traits and function for coffer implementations
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use log::{debug, error, info, trace, warn};
|
|
||||||
|
|
||||||
pub mod certificate;
|
pub mod certificate;
|
||||||
pub mod coffer;
|
pub mod coffer;
|
||||||
pub mod keyring;
|
pub mod keyring;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
assert_eq!(2 + 2, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub fn generate_key(out: PathBuf) {
|
||||||
let cert = certificate.to_cbor().unwrap();
|
let cert = certificate.to_cbor().unwrap();
|
||||||
|
|
||||||
let mut writer = File::create(&out)
|
let mut writer = File::create(&out)
|
||||||
.expect(&format!{"Could not create out file {}", &out.display()});
|
.unwrap_or_else(|_| panic!{"Could not create out file {}", &out.display()});
|
||||||
|
|
||||||
writer.write_all(&cert).unwrap();
|
writer.write_all(&cert).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::sync::RwLock;
|
||||||
use std::sync::RwLockReadGuard;
|
use std::sync::RwLockReadGuard;
|
||||||
use std::sync::RwLockWriteGuard;
|
use std::sync::RwLockWriteGuard;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::hash_map::{HashMap, Entry};
|
||||||
|
|
||||||
use coffer_common::coffer::*;
|
use coffer_common::coffer::*;
|
||||||
|
|
||||||
|
@ -29,13 +29,15 @@ impl CofferMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Coffer for CofferMap {
|
impl Coffer for CofferMap {
|
||||||
fn put(&mut self, key: CofferKey, value: CofferValue) -> CofferResult<()> {
|
fn put(&mut self, key: CofferKey, value: CofferValue) -> CofferResult<()> {
|
||||||
let mut lock = self.write();
|
let mut lock = self.write();
|
||||||
|
|
||||||
match lock.get_mut(&key.shard) {
|
match lock.get_mut(&key.shard) {
|
||||||
Some(shard) => {
|
Some(shard) => {
|
||||||
if shard.contains_key(&key.key) { Err(CofferError::Msg("Key exists")) }
|
match shard.entry(key.key) {
|
||||||
else { shard.insert(key.key, value); Ok(()) }
|
Entry::Occupied(_) => Err(CofferError::Msg("Key exists")),
|
||||||
|
Entry::Vacant(v) => { v.insert(value); Ok(()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
lock.insert(key.shard.clone(), HashMap::new());
|
lock.insert(key.shard.clone(), HashMap::new());
|
||||||
|
@ -59,33 +61,29 @@ impl Coffer for CofferMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, key: &CofferKey) -> CofferResult<CofferValue> {
|
fn get(&self, key: &CofferKey) -> Option<CofferValue> {
|
||||||
let lock = self.read();
|
let lock = self.read();
|
||||||
|
|
||||||
let res = lock.get(&key.shard)
|
lock.get(&key.shard)
|
||||||
.and_then( |shard| { shard.get(&key.key) } )
|
.and_then( |shard| { shard.get(&key.key) } )
|
||||||
.ok_or(CofferError::Msg("Key not found"))?;
|
.map(|o| o.clone())
|
||||||
|
|
||||||
Ok(res.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_shard<T>(&self, shard: T) -> CofferResult<CofferShard>
|
fn get_shard<T>(&self, shard: T) -> Option<CofferShard>
|
||||||
where T: AsRef<str>
|
where T: AsRef<str>
|
||||||
{
|
{
|
||||||
let lock = self.read();
|
let lock = self.read();
|
||||||
|
|
||||||
debug!{"Coffer {:?}", *lock}
|
debug!{"Coffer {:?}", *lock}
|
||||||
|
|
||||||
let coffer_shard = lock.get(shard.as_ref())
|
let map_to_vec = |map: &HashMap<String, CofferValue>| {
|
||||||
.ok_or(CofferError::Msg("Shard not found"))?;
|
map.iter()
|
||||||
|
.map(|(k,v)| (k.clone(), v.clone()))
|
||||||
|
.collect::<Vec<(String, CofferValue)>>()
|
||||||
|
};
|
||||||
|
|
||||||
let mut res = CofferShard(Vec::new());
|
lock.get(shard.as_ref())
|
||||||
|
.and_then(|s| Some(CofferShard(map_to_vec(s))))
|
||||||
for (k,v) in coffer_shard {
|
|
||||||
res.0.push((k.clone(), v.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ where C: Coffer + Send + Sync + 'static
|
||||||
|
|
||||||
debug!{"Binding to socket {:?}", socket}
|
debug!{"Binding to socket {:?}", socket}
|
||||||
let mut listener = TcpListener::bind(socket).await
|
let mut listener = TcpListener::bind(socket).await
|
||||||
.expect(format!{"Could not bind to socket {}", socket}.as_str());
|
.expect("Could not bind to socket");
|
||||||
|
|
||||||
let server = async move {
|
let server = async move {
|
||||||
let mut incoming = listener.incoming();
|
let mut incoming = listener.incoming();
|
||||||
|
|
BIN
overview.png
(Stored with Git LFS)
Normal file
BIN
overview.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
overview.svg
(Stored with Git LFS)
Normal file
BIN
overview.svg
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Reference in a new issue