Skip to content

Commit 269036f

Browse files
authored
Move gel-jwt from main geldata repo (#408)
1 parent db050c0 commit 269036f

36 files changed

+4337
-2
lines changed
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
on:
2+
push:
3+
tags:
4+
- releases/gel-jwt/v*
5+
6+
name: Release gel-jwt
7+
8+
jobs:
9+
test_and_publish:
10+
name: Test and publish
11+
runs-on: ubuntu-latest
12+
permissions:
13+
id-token: "write"
14+
contents: "read"
15+
steps:
16+
- name: checkout and env setup
17+
uses: actions/checkout@v3
18+
19+
- name: Extract project name and version
20+
run: |
21+
set -x
22+
PROJECT_NAME=$(echo $GITHUB_REF | sed -E 's|refs/tags/releases/([^/]+)/v.*|\1|')
23+
VERSION=$(echo $GITHUB_REF | sed -E 's|.*/v(.*)|\1|')
24+
echo "PROJECT_NAME=$PROJECT_NAME" >> $GITHUB_ENV
25+
echo "VERSION=$VERSION" >> $GITHUB_ENV
26+
27+
# verify that git tag matches cargo version
28+
- run: |
29+
set -x
30+
cargo_version="$(cargo metadata --format-version 1 \
31+
| jq -r '.packages[] | select(.name=="${{ env.PROJECT_NAME }}") | .version')"
32+
test "$cargo_version" = "${{ env.VERSION }}"
33+
34+
- name: Install Rust
35+
uses: dtolnay/rust-toolchain@master
36+
with:
37+
toolchain: stable
38+
components: rustfmt, clippy
39+
40+
- working-directory: ./${{ env.PROJECT_NAME }}
41+
run: |
42+
cargo publish --token=${{ secrets.CARGO_REGISTRY_TOKEN }}

Cargo.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
resolver = "2"
33
members = [
44
"gel-auth",
5+
"gel-derive",
56
"gel-dsn",
67
"gel-errors",
7-
"gel-derive",
8+
"gel-jwt",
89
"gel-protocol",
910
"gel-stream",
1011
"gel-tokio",
@@ -18,8 +19,9 @@ debug = true
1819
lto = true
1920

2021
[workspace.dependencies]
21-
tokio = { version = "1.43" }
22+
tokio = { version = "1.44" }
2223
tracing = { version = "0.1" }
24+
pyo3 = { version = "0.23", features = ["extension-module", "serde", "macros"] }
2325

2426
[workspace.package]
2527
rust-version = "1.81" # keep in sync with flake.nix

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ docs can currently be found on docs.rs:
88
|-------|--------|-------------|
99
| [gel-auth](https://docs.rs/gel-auth) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-auth) | Authentication and authorization utilities |
1010
| [gel-derive](https://docs.rs/gel-derive) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-derive) | Derive macro for data structures fetched from the database |
11+
| [gel-dsn](https://docs.rs/gel-dsn) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-dsn) | Data-source name (DSN) parser for Gel and PostgreSQL databases |
12+
| [gel-jwt](https://docs.rs/gel-jwt) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-jwt) | JSON Web Token (JWT) utilities |
1113
| [gel-errors](https://docs.rs/gel-errors) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-errors) | Error handling utilities |
1214
| [gel-protocol](https://docs.rs/gel-protocol) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-protocol) | Low-level definitions for data model of the Gel protocol |
1315
| [gel-stream](https://docs.rs/gel-stream) | [Source](https://github.com/edgedb/edgedb-rust/tree/main/gel-stream) | Library for streaming TLS/plaintext data between clients and servers |

gel-jwt/Cargo.toml

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
[package]
2+
name = "gel-jwt"
3+
license = "MIT/Apache-2.0"
4+
version = "0.1.0"
5+
authors = ["MagicStack Inc. <[email protected]>"]
6+
edition = "2021"
7+
description = """
8+
Low-level protocol implementation for Gel database client.
9+
For applications, use gel-tokio.
10+
Formerly published as edgedb-protocol.
11+
"""
12+
readme = "README.md"
13+
rust-version.workspace = true
14+
15+
[features]
16+
python_extension = ["pyo3/extension-module"]
17+
18+
[dependencies]
19+
pyo3 = { workspace = true, optional = true }
20+
tracing.workspace = true
21+
22+
# This is required to be in sync w/jsonwebtoken
23+
rand = "0.8.5"
24+
25+
md5 = "0.7.0"
26+
sha2 = "0.10.8"
27+
constant_time_eq = "0.3"
28+
base64 = "0.22"
29+
thiserror = "2"
30+
hmac = "0.12.1"
31+
derive_more = { version = "2", features = ["debug", "from", "display"] }
32+
33+
rustls-pki-types = "1"
34+
serde = "1"
35+
serde_derive = "1"
36+
serde_json = "1"
37+
jsonwebtoken = { version = "9", default-features = false }
38+
ring = { version = "0.17", default-features = false }
39+
rsa = { version = "0.9.7", default-features = false, features = ["std"] }
40+
pkcs1 = "0.7.5"
41+
pkcs8 = "0.10.2"
42+
sec1 = { version = "0.7.3", features = ["der", "pkcs8", "alloc"] }
43+
pem = "3"
44+
const-oid = { version ="0.9.6", features = ["db"] }
45+
p256 = { version = "0.13.2", features = ["jwk"] }
46+
base64ct = { version = "1", features = ["alloc"] }
47+
der = "0.7.9"
48+
libc = "0.2"
49+
elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
50+
num-bigint-dig = "0.8.4"
51+
zeroize = { version = "1", features = ["derive", "serde"] }
52+
uuid = { version = "1", features = ["v4", "serde"] }
53+
54+
[dev-dependencies]
55+
pretty_assertions = "1"
56+
rstest = "0.24.0"
57+
hex-literal = "0.4.1"
58+
divan = "0.1.17"
59+
60+
[[bench]]
61+
name = "encode"
62+
harness = false
63+
64+
[lib]

gel-jwt/benches/bench-jwcrypto.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from jwcrypto import jwt, jwk
2+
import time
3+
import statistics
4+
5+
6+
def generate_key(key_type):
7+
if key_type == "ES256":
8+
return jwk.JWK.generate(kty='EC', crv='P-256')
9+
elif key_type == "RS256":
10+
return jwk.JWK.generate(kty='RSA', size=2048)
11+
elif key_type == "HS256":
12+
return jwk.JWK.generate(kty='oct', size=256)
13+
raise ValueError(f"Unsupported key type: {key_type}")
14+
15+
16+
def benchmark_encode(key_type, iterations=100):
17+
# Generate key outside the loop
18+
key = generate_key(key_type)
19+
20+
# Benchmark full encoding process including claims creation
21+
times = []
22+
for _ in range(iterations):
23+
start = time.perf_counter_ns()
24+
25+
# Create claims and sign in the timed section
26+
claims = {"sub": "test"}
27+
token = jwt.JWT(
28+
header={"alg": key_type},
29+
claims=claims
30+
)
31+
token.make_signed_token(key)
32+
33+
end = time.perf_counter_ns()
34+
times.append(end - start)
35+
36+
mean = statistics.mean(times) / 1000 # Convert to microseconds
37+
median = statistics.median(times) / 1000
38+
return mean, median
39+
40+
41+
def benchmark_signing(key_type, iterations=100):
42+
# Generate key outside the loop
43+
key = generate_key(key_type)
44+
claims = {"sub": "test"}
45+
46+
# Benchmark signing
47+
times = []
48+
for _ in range(iterations):
49+
start = time.perf_counter_ns()
50+
51+
# Signing
52+
token = jwt.JWT(
53+
header={"alg": key_type},
54+
claims=claims
55+
)
56+
token.make_signed_token(key)
57+
58+
end = time.perf_counter_ns()
59+
times.append(end - start)
60+
61+
mean = statistics.mean(times) / 1000
62+
median = statistics.median(times) / 1000
63+
return mean, median
64+
65+
66+
def benchmark_validation(key_type, iterations=100):
67+
# Generate key and token outside the loop
68+
key = generate_key(key_type)
69+
token = jwt.JWT(
70+
header={"alg": key_type},
71+
claims={"sub": "test"}
72+
)
73+
token.make_signed_token(key)
74+
token_string = token.serialize()
75+
76+
# Benchmark validation
77+
times = []
78+
for _ in range(iterations):
79+
start = time.perf_counter_ns()
80+
81+
# Validation
82+
jwt.JWT(jwt=token_string, key=key)
83+
84+
end = time.perf_counter_ns()
85+
times.append(end - start)
86+
87+
mean = statistics.mean(times) / 1000
88+
median = statistics.median(times) / 1000
89+
return mean, median
90+
91+
92+
def main():
93+
key_types = ["ES256", "RS256", "HS256"]
94+
iterations = 100
95+
96+
print(f"Running {iterations} iterations for each algorithm")
97+
98+
print("\nFull encode benchmarks (including claims creation):")
99+
print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}")
100+
print("-" * 38)
101+
for key_type in key_types:
102+
mean, median = benchmark_encode(key_type, iterations)
103+
print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}")
104+
105+
print("\nSigning benchmarks (pre-created claims):")
106+
print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}")
107+
print("-" * 38)
108+
for key_type in key_types:
109+
mean, median = benchmark_signing(key_type, iterations)
110+
print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}")
111+
112+
print("\nValidation benchmarks:")
113+
print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}")
114+
print("-" * 38)
115+
for key_type in key_types:
116+
mean, median = benchmark_validation(key_type, iterations)
117+
print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}")
118+
119+
120+
if __name__ == "__main__":
121+
main()

gel-jwt/benches/encode.rs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::collections::HashMap;
2+
3+
use gel_jwt::{KeyType, PrivateKey, SigningContext, ValidationContext};
4+
5+
const KEY_TYPES: &[KeyType] = &[KeyType::ES256, KeyType::RS256, KeyType::HS256];
6+
7+
#[divan::bench(args = KEY_TYPES)]
8+
fn bench_jwt_signing(b: divan::Bencher, key_type: KeyType) {
9+
let key = PrivateKey::generate(None, key_type).unwrap();
10+
let claims = HashMap::from([("sub".to_string(), "test".into())]);
11+
let ctx = SigningContext::default();
12+
13+
b.bench_local(move || key.sign(claims.clone(), &ctx));
14+
}
15+
16+
#[divan::bench(args = KEY_TYPES)]
17+
fn bench_jwt_validation(b: divan::Bencher, key_type: KeyType) {
18+
let key = PrivateKey::generate(None, key_type).unwrap();
19+
let claims = HashMap::from([("sub".to_string(), "test".into())]);
20+
let ctx = SigningContext::default();
21+
let token = key.sign(claims, &ctx).unwrap();
22+
let ctx = ValidationContext::default();
23+
24+
b.bench_local(move || key.validate(&token, &ctx));
25+
}
26+
27+
#[divan::bench(args = KEY_TYPES)]
28+
fn bench_jwt_encode(b: divan::Bencher, key_type: KeyType) {
29+
let key = PrivateKey::generate(None, key_type).unwrap();
30+
31+
b.bench_local(move || {
32+
let claims = HashMap::from([("sub".to_string(), "test".into())]);
33+
let ctx = SigningContext::default();
34+
key.sign(claims, &ctx).unwrap()
35+
});
36+
}
37+
38+
fn main() {
39+
// Run registered benchmarks.
40+
divan::main();
41+
}

gel-jwt/src/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# JWT support
2+
3+
This crate provides support for JWT tokens. The JWT signing and verification is done
4+
using the `jsonwebtoken` crate, while the key loading is performed here via the
5+
`rsa`/`p256` crates.
6+
7+
## Key types
8+
9+
HS256: symmetric key
10+
RS256: asymmetric key (RSA 2048+ + SHA256)
11+
ES256: asymmetric key (P-256 + SHA256)
12+
13+
## Supported key formats
14+
15+
HS256: raw data
16+
RS256: PKCS1/PKCS8 PEM
17+
ES256: SEC1/PKCS8 PEM
18+

0 commit comments

Comments
 (0)