Skip to content

Commit

Permalink
docs: Add credentials to the docs
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Neuroth <[email protected]>
  • Loading branch information
nepet authored and cdecker committed Mar 1, 2024
1 parent 92f8319 commit 737acd9
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 21 deletions.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ nav:
- Reference:
- Overview: reference/index.md
- Client Libraries: reference/client-libraries.md
- Credentials: reference/creds.md
- Certificates: reference/certs.md
- Security: reference/security.md
- LSP Integration: reference/lsp.md
Expand Down
229 changes: 229 additions & 0 deletions docs/src/reference/creds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Client Credentials

Greenlight utilizes various components, such as mTLS certificates and
runes, to provide secure access to a node.
Various pieces of data must be persisted and made available to the client
at runtime. Check out the [Security][security] page for more information.

To simplify data management for users and developers, Greenlight uses
`Credentials` as a centralized location for the necessary information.
Credentials can be reconstructed in various ways and exported in
byte-encoded format for persistence.

## Ways to Retrieve Credentials

### Nobody Identity

How to build Credentials for the *default* `Nobody` identity?

=== "Rust"
```rust
use gl_client::credentials::Builder;

// Builds the default nobody identity.
let creds = Builder::as_nobody()
.with_default()
.expect("Failed to create default Nobody credentials")
.build()
.expect("Failed to build Nobody credentials");

// Access the tls config.
let tls_config = creds.tls_config()
.expect("Failed to create TlsConfig");
```

=== "Python"
```python
from glclient import Credentials, TlsConfig

# Builds the default nobody identity.
creds = Credentials.as_nobody().with_default().build();

# Access the TlsConfig
tls = TlsConfig(creds)
```

How to build Credentials for a *custom* `Nobody` identity?

=== "Rust"
```rust
use gl_client::credentials::Builder;

let ca = std::fs::read("ca.pem").expect("Failed to read from file");
let cert = std::fs::read("nobody.pem").expect("Failed to read from file");
let key = std::fs::read("nobody-key.pem").expect("Failed to read from file");

// Builds nobody credentials from custom values.
let creds = Builder::as_nobody()
.with_ca(ca)
.with_identity(cert, key)
.build()
.expect("Failed to build Nobody credentials");

// Access the tls config.
let tls_config: gl_client::tls::TlsConfig = creds.tls_config()
.expect("Failed to create TlsConfig");
```

=== "Python"
```python
from pathlib import Path
from glclient import Credentials, TlsConfig

capath = Path("ca.pem")
certpath = Path("nobody.pem")
keypath = Path("nobody-key.pem")

# Builds the default nobody identity.
creds = Credentials.as_nobody()
.with_ca(capath.open(mode="rb").read())
.with_identity(
certpath.open(mode="rb").read(),
keypath.open(mode="rb").read(),
)
.build()

# Access the TlsConfig
tls = TlsConfig(creds)
```

### Device Identity

The `Credentials` for a device can be retrieved in numberous ways. They can be restored from a path to a encoded credentials file, as well as from a byte array that carries the same data. `Credentials` for the device can also be constructed by their components or a combination of all of the above.

How to build `Credentials` from encoded formats?

=== "Rust"
```rust
use gl_client::credentials::Builder;

// Restore device credentials from a file.
let creds = Builder::as_device()
.from_path("path/to/credentials/file")
.expect("Failed to read credentials file")
.build()
.expect("Failed to build Device credentials");


// Alternatively restore from byte encoded data;
let enc_creds: [u8] = vec![...] // Some useful data here.
let creds = Builder::as_device()
.from_bytes(&enc_creds)
.expect("Faild to decode credentials")
.build()
.expect("Failed to build Device credentials");

// Access the tls config.
let tls_config = creds.tls_config()
.expect("Failed to create TlsConfig");

// Access the rune.
let rune = creds.rune();
```

=== "Python"
```python
from glclient import Credentials, TlsConfig

# Restore device credentials from a file.
creds = Credentials.as_device()
.from_path("/path/to/credentials/file")
.build()

# Alternatively restore from byte encoded data.
creds = Credentials.as_device()
.from_bytes(b('...')) # Some meaningful data
.build()

# Access the TlsConfig
tls = TlsConfig(creds)
```

How to build `Credentials` step by step?

=== "Rust"
```rust
use gl_client::credentials::Builder;

let ca = std::fs::read("ca.pem").expect("Failed to read from file");
let cert = std::fs::read("device.pem").expect("Failed to read from file");
let key = std::fs::read("device-key.pem").expect("Failed to read from file");
let rune = std::fs::read("rune").expect("Failed to read from file");

// Build device credentials step by step.
let creds = Builder::as_device()
.with_ca(ca)
.with_identity(cert, key)
.with_rune(rune)
.build()
.expect("Failed to build Device credentials");

// Access the tls config.
let tls_config = creds.tls_config()
.expect("Failed to create TlsConfig");

// Access the rune.
let rune = creds.rune();
```

=== "Python"
```python
from pathlib import Path
from glclient import Credentials, TlsConfig

capath = Path("ca.pem")
certpath = Path("device.pem")
keypath = Path("device-key.pem")
runepath = Path("rune")

# Builds the default nobody identity.
creds = Credentials.as_nobody()
.with_ca(capath.open(mode="rb").read())
.with_identity(
certpath.open(mode="rb").read(),
keypath.open(mode="rb").read(),
)
.with_rune(runepath.open(mode="r").read())
.build()

# Access the TlsConfig
tls = TlsConfig(creds)
```

!!! tip
One can use a combination of the methods showed above to override the configuration. This example overrides the CA certificate.

=== "Rust"
```rust
use gl_client::credentials::Builder;

let ca = std::fs::read("ca.pem").expect("Failed to read from file");

// Builds the default nobody identity, overrides ca certificate.
let creds = Builder::as_nobody()
.with_default()
.expect("Failed to create default Nobody credentials")
.with_ca(ca) // CA certificate gets overriden.
.build()
.expect("Failed to build Nobody credentials");
```

=== "Python"
```python
from glclient import Credentials, TlsConfig

capath = Path("ca.pem")

# Builds the default nobody identity, overrides ca certificate.
creds = Credentials
.as_nobody()
.with_default()
.with_ca(capath.open(mode="rb").read()) # CA certificate gets overriden.
.build();
# Access the TlsConfig
tls = TlsConfig(creds)
```


[security]: ./security.md
60 changes: 39 additions & 21 deletions docs/src/reference/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ attacker, whether internal or external, by checking authorization on
both the node as well as the signer level.



## Client &rlarr; Node authentication
## Client &rlarr; Node Authentication

This is a direct connection from a client to the node, or the signer
to the node. The mTLS certificate hierarchy is under the control of
Expand Down Expand Up @@ -50,27 +49,28 @@ control of the CA hierarchy, could create a bogus client certificate
and use that to issue commands to the node. More on this in the next
section.

## Client &rlarr; Signer authentication
## Client &rlarr; Signer Authentication

The signer cannot rely on the mTLS CA structure, since that is under
control of the Greenlight team. As such it uses an attestation scheme
in which the signer-identity attests to itself (and other signers)
that a given client is permitted to perform some operations.

Upon registering a new client, the signer will provide an attestation
signature. This attestation signature is what tells other signers that
commands originating from that client are to be accepted. This is
important because it allows multiple signers to recognize which
clients are authorized, even if the attesting signer is not the signer
that is currently verifying the authorization.
control of the Greenlight team. Instead, Greenlight employs an
attestation scheme in which the signer identity attests to itself and
other signers that a particular client is authorized to perform
certain operations.

When registering a new client, the signer will submit a [`rune`][rune]
as an access token. The rune serves mainly as an authentication while the
client's identity in form of a mTLS key-pair provides authorization.
The run is bound to the client's identity; it is presented to the signer
on each request. This will allow multiple signers to recognize which
clients are authorized, even if the signer who carved the rune is not the
signer verifying the authorization.

Before signing a request the signer independently verifies that:

1. The operations that it is asked to sign off on match pending RPC
commands, and are safe to perform.
2. The pending RPC commands are all signed by a valid client-identity
3. The client-identities all have a matching attestation signature
from the signer
3. The client-identity has a valid rune that qualifies for the request.
4. None of the pending RPC commands is a replay of a previously
completed RPC command.

Expand All @@ -82,10 +82,28 @@ preventing read-access that doesn't involve the signer, while the
latter ensures funds are not moved without a client authorizing it.

The client-identity pubkey, its signature of the command payload, and
the signer-attestation are all passed to the node via grpc
headers. The node extracts them, alongside the call itself, and adds
it to a request context which will itself be attached to requests that
are sent to the signer, so it can verify the validity and authenticity
of the operations. An attacker that gains access to the node is unable
to provide either these signatures and will therefore fail to convince
the rune are all passed to the node via grpc headers. The node
extracts them, alongside the call itself, and adds it to a request
context which will itself be attached to requests that are sent to the
signer, so it can verify the validity and authenticity of the
operations. An attacker that gains access to the node is unable to
provide either these signatures and will therefore fail to convince
the signer of its injected commands.

## Client &rlarr; Signer Authorization

The [`rune`][rune]-based signer authentication verifies a client's
identity and their authorization to execute commands. To achieve more
granular control over command authorization for individual clients, a
rune can be created with specific restrictions. When verifying client
identity, the signer additionally ensures that specific conditions,
such as the requested command, align with the limitations of the given
rune.

This enables a user to share access to the signer across multiple
clients that are restricted to a subset of commands, for example
read-only clients, or invoice-only clients, or clients that can only
create on-chain addresses and so on.

<!-- FIXME: Should I point to the library that we use here (which is futhark, that has a link to rusty's library)? Maybe we should give runes a Wikipedia article ^^ -->
[rune]: https://github.com/nepet/runeauth

0 comments on commit 737acd9

Please sign in to comment.