Skip to content

Commit b4250c2

Browse files
committed
wip RS256 jwt
1 parent 819c856 commit b4250c2

File tree

12 files changed

+711
-16
lines changed

12 files changed

+711
-16
lines changed

.vscode/launch.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@
9999
"cargo": {
100100
"args": [
101101
"test",
102-
"--no-run",
103-
"--lib",
104-
"--package=rust-photoacoustic"
102+
"--test",
103+
"rs256_jwt_test",
104+
"test_rs256_jwt_token_generation_and_validation"
105105
],
106106
"filter": {
107107
"name": "rust-photoacoustic",

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ serde_urlencoded = "0.7.1"
5050
url = "2.5.4" # URL parsing
5151
rcgen = "0.13.2" # Certificate generation
5252
time = "0.3.41" # Time handling for certificates
53-
rsa = "0.9.8"
53+
rsa = {version = "0.9.8", features=["pem","sha2"]}
5454
tokio = { version = "1.45.0", features = ["rt", "macros", "rt-multi-thread", "time"] }
5555
tokio-modbus = { version = "0.16.1", features = ["tcp", "tcp-server", "server"] }
5656

src/utility/mod.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ macro_rules! include_png_as_base64 {
3030
/// It is useful for embedding SVG images directly into web pages or stylesheets.
3131
#[macro_export]
3232
macro_rules! include_svg_as_base64 {
33-
($path:expr) => {
34-
{
35-
use ::base64::prelude::{Engine as _, BASE64_STANDARD};
36-
let svg_data = include_bytes!($path);
37-
let base64 = BASE64_STANDARD.encode(svg_data);
38-
format!("data:image/svg+xml;base64,{}", base64)
39-
}
40-
};
33+
($path:expr) => {{
34+
use ::base64::prelude::{Engine as _, BASE64_STANDARD};
35+
let svg_data = include_bytes!($path);
36+
let base64 = BASE64_STANDARD.encode(svg_data);
37+
format!("data:image/svg+xml;base64,{}", base64)
38+
}};
4139
}

src/visualization/jwt.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,36 @@ impl JwtTokenMap {
276276
}
277277
}
278278

279+
/// Create a new JWT token issuer with RS256 algorithm using PEM encoded keys
280+
///
281+
/// # Parameters
282+
///
283+
/// * `private_key_pem` - PEM encoded private key
284+
/// * `public_key_pem` - PEM encoded public key
285+
///
286+
/// # Returns
287+
///
288+
/// A new JwtTokenMap configured to use RS256 algorithm with the provided keys
289+
pub fn with_rs256_pem(
290+
private_key_pem: &[u8],
291+
public_key_pem: &[u8],
292+
) -> Result<Self, jsonwebtoken::errors::Error> {
293+
let encoding_key = EncodingKey::from_rsa_pem(private_key_pem)?;
294+
let decoding_key = DecodingKey::from_rsa_pem(public_key_pem)?;
295+
296+
Ok(JwtTokenMap {
297+
access_tokens: HashMap::new(),
298+
refresh_tokens: HashMap::new(),
299+
signing_key: encoding_key,
300+
verification_key: decoding_key,
301+
refresh_generator: RandomGenerator::new(16),
302+
token_duration: Some(Duration::hours(1)), // Default 1 hour
303+
issuer: "rust-photoacoustic".to_string(),
304+
usage_counter: 0,
305+
algorithm: Algorithm::RS256,
306+
})
307+
}
308+
279309
/// Sets the JWT signing algorithm
280310
pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
281311
self.algorithm = algorithm;
@@ -542,6 +572,24 @@ impl JwtIssuer {
542572
JwtIssuer(Arc::new(Mutex::new(JwtTokenMap::new(secret))))
543573
}
544574

575+
/// Create a new JwtIssuer with RS256 algorithm using PEM encoded keys
576+
///
577+
/// # Parameters
578+
///
579+
/// * `private_key_pem` - PEM encoded private key
580+
/// * `public_key_pem` - PEM encoded public key
581+
///
582+
/// # Returns
583+
///
584+
/// A new JwtIssuer configured to use RS256 algorithm with the provided keys
585+
pub fn with_rs256_pem(
586+
private_key_pem: &[u8],
587+
public_key_pem: &[u8],
588+
) -> Result<Self, jsonwebtoken::errors::Error> {
589+
let token_map = JwtTokenMap::with_rs256_pem(private_key_pem, public_key_pem)?;
590+
Ok(JwtIssuer(Arc::new(Mutex::new(token_map))))
591+
}
592+
545593
/// Sets the JWT signing algorithm
546594
pub fn with_algorithm(&mut self, algorithm: Algorithm) -> &mut Self {
547595
// Create a closure to modify the map

src/visualization/jwt_keys.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,18 @@
5959
//! ).unwrap();
6060
//! ```
6161
62-
use anyhow::{anyhow, Result};
62+
use anyhow::{anyhow, Context, Result};
63+
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
64+
use base64::prelude::*;
65+
use jsonwebtoken::jwk::{Jwk, PublicKeyUse};
6366
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
67+
use rsa::pkcs1::DecodeRsaPublicKey;
68+
use rsa::traits::PublicKeyParts;
69+
use rsa::RsaPublicKey;
70+
use rsa::sha2::Sha256;
71+
use rsa::sha2::Digest;
72+
use serde::{Deserialize, Serialize};
73+
use serde_json::json;
6474
use std::fs::File;
6575
use std::io::Read;
6676
use std::path::Path;
@@ -506,3 +516,115 @@ impl JwtKeyConfig {
506516
)
507517
}
508518
}
519+
520+
/// JSON Web Key Set
521+
///
522+
/// This structure represents a set of JSON Web Keys (JWKs) as defined in RFC 7517.
523+
/// It can be used to generate and manipulate JWK representations of RSA keys for
524+
/// use with OpenID Connect discovery endpoints.
525+
#[derive(Debug, Serialize, Deserialize)]
526+
pub struct JwkKeySet {
527+
/// The set of JWKs
528+
pub keys: Vec<Jwk>,
529+
}
530+
531+
impl JwkKeySet {
532+
/// Create a new JWK from a PEM encoded RSA public key
533+
///
534+
/// This function converts a PEM encoded RSA public key to a JWK (JSON Web Key)
535+
/// representation suitable for use with OpenID Connect discovery endpoints.
536+
///
537+
/// # Parameters
538+
///
539+
/// * `pem_data` - The PEM encoded RSA public key as bytes
540+
///
541+
/// # Returns
542+
///
543+
/// A JWK representing the RSA public key, or an error if parsing fails
544+
pub fn create_jwk_from_pem(pem_data: &[u8]) -> Result<Jwk> {
545+
// Parse the PEM key
546+
let public_key = DecodeRsaPublicKey::from_pkcs1_pem(std::str::from_utf8(pem_data)?)
547+
.context("Failed to parse RSA public key from PEM")?;
548+
549+
// Convert to JWK
550+
Self::create_jwk_from_public_key(&public_key)
551+
}
552+
553+
/// Create a JWK from an RSA public key
554+
///
555+
/// Converts an RSA public key to a JWK representation with the necessary
556+
/// parameters for use with OpenID Connect.
557+
///
558+
/// # Parameters
559+
///
560+
/// * `public_key` - The RSA public key
561+
///
562+
/// # Returns
563+
///
564+
/// A JWK representing the RSA public key
565+
pub fn create_jwk_from_public_key(public_key: &RsaPublicKey) -> Result<Jwk> {
566+
// Get the modulus (n) and exponent (e) from the public key
567+
let n = public_key.n();
568+
let n = BASE64_STANDARD.encode(&public_key.n().to_bytes_be());
569+
let e = BASE64_STANDARD.encode(&public_key.e().to_bytes_be());
570+
571+
// Calculate the key ID (kid) as a SHA-256 thumbprint
572+
let jwk_thumbprint = Self::calculate_jwk_thumbprint(&n, &e)?;
573+
574+
// Build the JWK
575+
let jwk = Jwk {
576+
common: jsonwebtoken::jwk::CommonParameters {
577+
public_key_use: Some(PublicKeyUse::Signature),
578+
key_id: Some(jwk_thumbprint),
579+
key_algorithm: Some(jsonwebtoken::jwk::KeyAlgorithm::RS256), // Correct field name and type
580+
..Default::default()
581+
},
582+
algorithm: jsonwebtoken::jwk::AlgorithmParameters::RSA(
583+
jsonwebtoken::jwk::RSAKeyParameters {
584+
key_type: jsonwebtoken::jwk::RSAKeyType::RSA,
585+
n,
586+
e,
587+
..Default::default()
588+
},
589+
),
590+
};
591+
592+
Ok(jwk)
593+
}
594+
595+
/// Calculate a JWK thumbprint according to RFC 7638
596+
///
597+
/// This function calculates a thumbprint for a JWK which can be used as
598+
/// a key ID (kid) parameter. The thumbprint is a SHA-256 hash of the
599+
/// canonical JSON representation of the JWK.
600+
///
601+
/// # Parameters
602+
///
603+
/// * `n` - Base64URL encoded modulus
604+
/// * `e` - Base64URL encoded exponent
605+
///
606+
/// # Returns
607+
///
608+
/// Base64URL encoded SHA-256 thumbprint
609+
fn calculate_jwk_thumbprint(n: &str, e: &str) -> Result<String> {
610+
// Create canonical JWK representation
611+
let canonical = json!({
612+
"e": e,
613+
"kty": "RSA",
614+
"n": n
615+
});
616+
617+
// Serialize to bytes in lexicographic order
618+
let canonical_bytes = serde_json::to_vec(&canonical)?;
619+
620+
// Calculate SHA-256 hash
621+
let mut hasher = Sha256::new();
622+
hasher.update(&canonical_bytes);
623+
let hash = hasher.finalize();
624+
625+
// Encode as Base64URL
626+
let thumbprint = URL_SAFE_NO_PAD.encode(hash);
627+
628+
Ok(thumbprint)
629+
}
630+
}

src/visualization/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ pub mod oxide_auth;
132132
/// Web server implementation
133133
pub mod server;
134134

135+
/// OIDC discovery and configuration
136+
pub mod oidc;
137+
135138
use crate::{config::Config, AnalysisResult};
136139
use anyhow::Result;
137140
use base64::{self, Engine};

0 commit comments

Comments
 (0)