Skip to content

Proposal: JWT-based authorization for notary server #812

@kubkon

Description

@kubkon

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: string
  • public_key_pem_path - path to the decoding key
  • claims - 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: string

Implementation 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:
Image
  • sending a CURL with JWT token with sub claim set as test rather than the required tlsn value:
Image
  • sending a CURL with correct JWT token:
Image

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions