-
Notifications
You must be signed in to change notification settings - Fork 119
Description
This proposal follows up discussion about additional authorization modes during TLSN office hours and face-to-face meeting with @th4s. CCing @yuroitaki since you seem to be handling most if not all server-side things in TLSN.
Premise
Currently, authorization with TLS notary server is done via a whitelist mechanism. The whitelist has a simple format such that:
| Name | ApiKey | CreatedAt |
|---|---|---|
| John Doe | my_secret_key | 1970-01-01 |
Next, authentication via the whitelist can be enabled by specifying in the server’s config.yaml
authorization:
enabled: true
whitelist_csv_path: "path/to/whitelist.csv"Note that by default authorization is disabled. If enabled however, it protects all routes except /notarize which is automatically protected by a session random key which is generated by accessing /session route.
While the above mechanism is simple to use and most likely sufficient for server-based communications with the notary server, the problems appear once we move the sending end (the end that request a notarization) into the browser. Then, the browser has to somehow gain access to the secret API key, and do so in such a way as not to accidentally leak the secret to the outside world. This is finicky and error-prone.
In this issue, we would like to propose an alternative authorization mechanism that is based on JSON web token authorization. This mode of authorization would exist side-by-side as an alternative to the whitelist mechanism, and the user of the TLS notary server would be able to select the most appropriate mode of authorization for their setup.
User perspective
From the user perspective, ideally they would be able to enable JWT authorization in the config file, specify path to the decoding key (public key for validating JWTs’ signatures), and optionally specify a list of claims the JWT should carry for successful authorization. This config would co-exist with the existing whitelist config mechanism, however, only one should be possible at any one time.
Here’s the proposed format of JWT config in config.yaml:
authorization:
enabled: true
jwt:
public_key_pem_path: "./fixture/auth/jwt.pub"
claims:
- name: sub
values: [test]
- name: host
values: [api.x.com]
value_type: stringpublic_key_pem_path- path to the decoding keyclaims- list of required claims that each JWT has to carry
In this particular example, we require each JWT to carry sub and host claims such that
{
"sub": "test",
"host": "api.x.com"
}If any of those claims is not present in the JWT, or has a different value, fails validation and ultimately ends with failure to correctly authorize with the server. Additionally, we would always validate JWT’s expiration.
The proposed format for describing the required claims is based upon a similar mechanism found in Envoy Gateway configuration.
The proposed scheme is best summarised by the following Rust target structs:
#[derive(Deserialize)]
pub struct JwtClaim {
pub name: String,
#[serde(default)]
pub values: Vec<String>,
#[serde(default)]
pub value_type: JwtClaimValueType,
}
#[derive(Deserialize)]
#[serde(rename-all = "kebab-case")]
pub enum JwtClaimValueType {
#[default]
String,
}JwtClaimValueType would initially include some basic primitives such as string and int but could of course be augmented to include more value types. Also, please note that the only required field of JwtClaim is name , while the rest of the fields assume default values such that values := Vec::new() and value_type := JwtClaimValueType::String. Finally, name is expected to be nested with . as a separator, therefore sub would mean we expect sub field at the root of the JWT claims object
{
"exp": 12345,
"sub": "test"
}while my.new.sub would mean we expect three indentation levels in the JWT claims object
{
"exp": 12345,
"sub": "test",
"my": {
"new": {
"sub": "test"
}
}
}Some examples of valid claims configurations are:
- no claims - accept anything
authorization:
jwt:
public_key_pem_path: "..."- require claim with any value
authorization:
jwt:
public_key_pem_path: "..."
claims:
- name: identity- require claim with some particular values and of type
string
authorization:
jwt:
public_key_pem_path: "..."
claims:
- name: identity
values: [Me, Him]
value_type: String- require nested claim with some particular values and of type
string
authorization:
jwt:
public_key_pem_path: "..."
claims:
- name: my.super.identity
values: [Me, Him]
value_type: stringImplementation perspective
From the implementation perspective, we would re-use the implementation of the existing AuthorizationMiddleware with one gotcha - JWT claims would be validated at runtime (with the exception of expiration date). This can be achieved by extracting a raw serde_json::Value as raw claims from the underlying JWT (via jsonwebtoken crate), and then we would walk the user-specified claims set in the config file to perform validation by selectively accessing elements in the extract JSON Value.
I envision the logic for validating claims to reside in crates/notary/server/src/auth.rs (or thereabouts).
Additionally, the server would always have access to user-specified claims as well as the decoding key in NotaryGlobals just like it now has access to the authorization whitelist (if present). This way, addition of JWT-based authorization mechanism would be least invasive on the notary server implementation.
An example PoC implementation is available in my fork as dff299f commit.
Demo
Example config.yaml:
# config.yaml
server:
name: "notary-server"
host: "0.0.0.0"
port: 7047
html_info: |
<head>
<meta charset="UTF-8">
<meta name="author" content="tlsnotary">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<svg width="86" height="88" viewBox="0 0 86 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z" fill="#243F5F"/>
<path d="M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z" fill="#243F5F"/>
<path d="M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z" fill="#243F5F"/>
<path d="M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z" fill="#243F5F"/>
</svg>
<h1>Notary Server {version}!</h1>
<ul>
<li>public key: <pre>{public_key}</pre></li>
<li>git commit hash: <a href="https://github.com/tlsnotary/tlsn/commit/{git_commit_hash}">{git_commit_hash}</a></li>
<li><a href="healthcheck">health check</a></li>
<li><a href="info">info</a></li>
</ul>
</body>
notarization:
max_sent_data: 4096
max_recv_data: 16384
timeout: 1800
tls:
enabled: false
private_key_pem_path: "./fixture/tls/notary.key"
certificate_pem_path: "./fixture/tls/notary.crt"
notary_key:
private_key_pem_path: "./fixture/notary/notary.key"
public_key_pem_path: "./fixture/notary/notary.pub"
logging:
level: DEBUG
authorization:
enabled: true
jwt:
public_key_pem_path: "./fixture/auth/jwt.pub"
claims:
- name: sub
values: [tlsn]
concurrency: 32- sending a CURL with no JWT token in Authorization header:
- sending a CURL with JWT token with
subclaim set astestrather than the requiredtlsnvalue:
- sending a CURL with correct JWT token:
Concluding remarks
Since this feature is desired by us (the vlayer team), I would of course champion it and work with you (the maintainers) in landing it in the best shape possible.