You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/05/22 08:47:42 UTC

(opendal-reqsign) branch xuanwo/remove-jsonwebtoken created (now b1b9cc6)

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a change to branch xuanwo/remove-jsonwebtoken
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git


      at b1b9cc6  Remove jsonwebtoken dependency

This branch includes the following new commits:

     new b1b9cc6  Remove jsonwebtoken dependency

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(opendal-reqsign) 01/01: Remove jsonwebtoken dependency

Posted by xu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/remove-jsonwebtoken
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git

commit b1b9cc6886d7ee1721cb67d58b6cbfa9c7a53385
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Fri May 22 16:47:28 2026 +0800

    Remove jsonwebtoken dependency
---
 .github/workflows/ci.yml                           |   2 +-
 AGENTS.md                                          |   3 +-
 Cargo.toml                                         |   1 -
 core/Cargo.toml                                    |   5 +
 core/src/jwt.rs                                    | 153 +++++++++++++++++++++
 core/src/lib.rs                                    |   2 +
 services/azure-storage/Cargo.toml                  |   1 -
 .../src/provide_credential/client_certificate.rs   |  27 ++--
 services/google/Cargo.toml                         |   1 -
 services/google/src/sign_request.rs                |  31 +++--
 10 files changed, 196 insertions(+), 30 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd3390c..c278dac 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -71,7 +71,7 @@ jobs:
       - name: Build
         run: cargo build --workspace
 
-  # gcs has jsonwebtoken which doesn't support wasm
+  # Keep the facade wasm build focused on services with supported wasm signing paths.
   build_under_wasm:
     runs-on: ubuntu-latest
     steps:
diff --git a/AGENTS.md b/AGENTS.md
index cd4dc01..b46ca75 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,8 +63,7 @@ servers checked into `services/aws-v4/tests/mocks/`.
 ## WASM Compatibility
 
 `reqsign-core` and a subset of services (`aws`, `azure`, `aliyun`, `tencent`)
-must compile for `wasm32-unknown-unknown`. Google is excluded because
-`jsonwebtoken` does not support WASM. CI verifies this.
+must compile for `wasm32-unknown-unknown`. CI verifies this supported subset.
 
 ## Versioning
 
diff --git a/Cargo.toml b/Cargo.toml
index 7009b54..652585f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,7 +53,6 @@ hex = "0.4"
 hmac = "0.13"
 http = "1"
 jiff = "0.2"
-jsonwebtoken = { version = "10", features = ["aws_lc_rs"] }
 log = "0.4"
 percent-encoding = "2"
 pretty_assertions = "1.3"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 5341ba8..98b0fac 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -42,6 +42,11 @@ percent-encoding = { workspace = true }
 sha1 = { workspace = true }
 sha2 = { workspace = true }
 
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+rsa = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+
 [target.'cfg(target_os = "windows")'.dependencies]
 windows-sys = { version = "0.61.0", features = [
   "Win32_Foundation",
diff --git a/core/src/jwt.rs b/core/src/jwt.rs
new file mode 100644
index 0000000..413ff8f
--- /dev/null
+++ b/core/src/jwt.rs
@@ -0,0 +1,153 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! JWT encoding helpers.
+
+use base64::Engine;
+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
+use rsa::RsaPrivateKey;
+use rsa::pkcs1v15::SigningKey;
+use rsa::pkcs8::DecodePrivateKey;
+use rsa::rand_core::OsRng;
+use rsa::sha2::Sha256;
+use rsa::signature::{RandomizedSigner, SignatureEncoding};
+use serde::Serialize;
+
+use crate::{Error, Result};
+
+/// Encode a JWS compact JWT using the RS256 algorithm.
+///
+/// RS256 is RSA PKCS#1 v1.5 with SHA-256. The caller owns the JWT header and
+/// claims shape so service-specific fields such as `x5t` stay service-local.
+pub fn encode_rs256<H, C>(header: &H, claims: &C, private_key: &RsaPrivateKey) -> Result<String>
+where
+    H: Serialize,
+    C: Serialize,
+{
+    let encoded_header = encode_json(header)?;
+    let encoded_claims = encode_json(claims)?;
+    let signing_input = format!("{encoded_header}.{encoded_claims}");
+
+    let mut rng = OsRng;
+    let signing_key = SigningKey::<Sha256>::new(private_key.clone());
+    let signature = signing_key.sign_with_rng(&mut rng, signing_input.as_bytes());
+    let encoded_signature = URL_SAFE_NO_PAD.encode(signature.to_bytes());
+
+    Ok(format!("{signing_input}.{encoded_signature}"))
+}
+
+/// Encode a JWS compact JWT using an RSA private key in PKCS#8 PEM format.
+pub fn encode_rs256_pem<H, C>(header: &H, claims: &C, private_key_pem: &[u8]) -> Result<String>
+where
+    H: Serialize,
+    C: Serialize,
+{
+    let private_key_pem = std::str::from_utf8(private_key_pem).map_err(|e| {
+        Error::credential_invalid("RSA private key PEM is not valid UTF-8").with_source(e)
+    })?;
+    let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| {
+        Error::credential_invalid("failed to parse PKCS#8 RSA private key PEM").with_source(e)
+    })?;
+
+    encode_rs256(header, claims, &private_key)
+}
+
+fn encode_json<T>(value: &T) -> Result<String>
+where
+    T: Serialize,
+{
+    let json = serde_json::to_vec(value)
+        .map_err(|e| Error::unexpected("failed to serialize JWT JSON").with_source(e))?;
+    Ok(URL_SAFE_NO_PAD.encode(json))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
+    use rsa::pkcs1v15::{Signature, VerifyingKey};
+    use rsa::rand_core::OsRng;
+    use rsa::signature::Verifier;
+    use serde_json::json;
+
+    #[derive(Serialize)]
+    struct Header<'a> {
+        alg: &'a str,
+        typ: &'a str,
+        x5t: &'a str,
+    }
+
+    #[derive(Serialize)]
+    struct Claims<'a> {
+        iss: &'a str,
+        sub: &'a str,
+        aud: &'a str,
+        exp: u64,
+    }
+
+    #[test]
+    fn encode_rs256_keeps_jws_shape_and_verifiable_signature() -> Result<()> {
+        let mut rng = OsRng;
+        let private_key = RsaPrivateKey::new(&mut rng, 2048)
+            .map_err(|e| Error::unexpected("failed to generate test RSA key").with_source(e))?;
+
+        let jwt = encode_rs256(
+            &Header {
+                alg: "RS256",
+                typ: "JWT",
+                x5t: "thumbprint",
+            },
+            &Claims {
+                iss: "client",
+                sub: "client",
+                aud: "https://example.com/token",
+                exp: 1,
+            },
+            &private_key,
+        )?;
+
+        let parts = jwt.split('.').collect::<Vec<_>>();
+        assert_eq!(parts.len(), 3);
+
+        let header: serde_json::Value = serde_json::from_slice(
+            &URL_SAFE_NO_PAD
+                .decode(parts[0])
+                .map_err(|e| Error::unexpected("failed to decode JWT header").with_source(e))?,
+        )
+        .map_err(|e| Error::unexpected("failed to parse JWT header").with_source(e))?;
+        assert_eq!(
+            header,
+            json!({
+                "alg": "RS256",
+                "typ": "JWT",
+                "x5t": "thumbprint"
+            })
+        );
+
+        let signature = URL_SAFE_NO_PAD
+            .decode(parts[2])
+            .map_err(|e| Error::unexpected("failed to decode JWT signature").with_source(e))?;
+        let signature = Signature::try_from(signature.as_slice())
+            .map_err(|e| Error::unexpected("failed to parse JWT signature").with_source(e))?;
+        let verifying_key = VerifyingKey::<Sha256>::new(private_key.to_public_key());
+        verifying_key
+            .verify(format!("{}.{}", parts[0], parts[1]).as_bytes(), &signature)
+            .map_err(|e| Error::unexpected("failed to verify JWT signature").with_source(e))?;
+
+        Ok(())
+    }
+}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index dd827da..9ed35c5 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -153,6 +153,8 @@
 pub mod error;
 mod futures_util;
 pub mod hash;
+#[cfg(not(target_arch = "wasm32"))]
+pub mod jwt;
 pub mod time;
 pub mod utils;
 
diff --git a/services/azure-storage/Cargo.toml b/services/azure-storage/Cargo.toml
index f2f475e..4e6789c 100644
--- a/services/azure-storage/Cargo.toml
+++ b/services/azure-storage/Cargo.toml
@@ -40,7 +40,6 @@ serde_json = { workspace = true }
 sha1 = { workspace = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-jsonwebtoken = { workspace = true }
 pem = "3.0"
 rsa = { workspace = true }
 
diff --git a/services/azure-storage/src/provide_credential/client_certificate.rs b/services/azure-storage/src/provide_credential/client_certificate.rs
index 6715b1e..314e63c 100644
--- a/services/azure-storage/src/provide_credential/client_certificate.rs
+++ b/services/azure-storage/src/provide_credential/client_certificate.rs
@@ -21,7 +21,6 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
 use crate::credential::Credential;
 use base64::Engine;
 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-use jsonwebtoken::{Algorithm, EncodingKey, Header};
 use reqsign_core::time::Timestamp;
 use reqsign_core::{Context, ProvideCredential};
 use rsa::RsaPrivateKey;
@@ -179,20 +178,13 @@ impl ClientCertificateCredentialProvider {
             sub: client_id.to_string(),
         };
 
-        let mut header = Header::new(Algorithm::RS256);
-        header.x5t = Some(self.calculate_thumbprint(cert_der));
-
-        let pem_private_key =
-            rsa::pkcs8::EncodePrivateKey::to_pkcs8_pem(private_key, Default::default()).map_err(
-                |e| reqsign_core::Error::unexpected(format!("Failed to encode private key: {e}")),
-            )?;
-
-        let encoding_key = EncodingKey::from_rsa_pem(pem_private_key.as_bytes()).map_err(|e| {
-            reqsign_core::Error::unexpected(format!("Failed to create encoding key: {e}"))
-        })?;
+        let header = ClientAssertionHeader {
+            alg: "RS256",
+            typ: "JWT",
+            x5t: self.calculate_thumbprint(cert_der),
+        };
 
-        jsonwebtoken::encode(&header, &claims, &encoding_key)
-            .map_err(|e| reqsign_core::Error::unexpected(format!("Failed to create JWT: {e}")))
+        reqsign_core::jwt::encode_rs256(&header, &claims, private_key)
     }
 
     /// Exchange client assertion for access token
@@ -257,6 +249,13 @@ struct ClientAssertionClaims {
     sub: String,
 }
 
+#[derive(Debug, Serialize)]
+struct ClientAssertionHeader {
+    alg: &'static str,
+    typ: &'static str,
+    x5t: String,
+}
+
 #[derive(Debug, Deserialize)]
 struct TokenResponse {
     access_token: String,
diff --git a/services/google/Cargo.toml b/services/google/Cargo.toml
index 8f6dd01..6b8483e 100644
--- a/services/google/Cargo.toml
+++ b/services/google/Cargo.toml
@@ -29,7 +29,6 @@ rust-version.workspace = true
 [dependencies]
 form_urlencoded = { workspace = true }
 http = { workspace = true }
-jsonwebtoken = { workspace = true }
 log = { workspace = true }
 percent-encoding = { workspace = true }
 reqsign-aws-v4 = { workspace = true }
diff --git a/services/google/src/sign_request.rs b/services/google/src/sign_request.rs
index e6d0c2f..583382e 100644
--- a/services/google/src/sign_request.rs
+++ b/services/google/src/sign_request.rs
@@ -16,7 +16,6 @@
 // under the License.
 
 use http::header;
-use jsonwebtoken::{Algorithm, EncodingKey, Header as JwtHeader};
 use log::debug;
 use percent_encoding::{percent_decode_str, utf8_percent_encode};
 use rsa::pkcs1v15::SigningKey;
@@ -29,7 +28,7 @@ use std::time::Duration;
 
 use reqsign_core::{
     Context, Result, SignRequest, SigningCredential, SigningMethod, SigningRequest,
-    hash::hex_sha256, time::*,
+    hash::hex_sha256, jwt, time::*,
 };
 
 use crate::constants::{DEFAULT_SCOPE, GOOG_QUERY_ENCODE_SET, GOOG_URI_ENCODE_SET, GOOGLE_SCOPE};
@@ -59,6 +58,22 @@ impl Claims {
     }
 }
 
+/// Header is used to build RS256 JWT for Google Cloud OAuth2.
+#[derive(Debug, Serialize)]
+struct JwtHeader {
+    alg: &'static str,
+    typ: &'static str,
+}
+
+impl JwtHeader {
+    fn rs256() -> Self {
+        Self {
+            alg: "RS256",
+            typ: "JWT",
+        }
+    }
+}
+
 /// OAuth2 token response.
 #[derive(Deserialize)]
 struct TokenResponse {
@@ -132,15 +147,11 @@ impl RequestSigner {
 
         debug!("exchanging service account for token with scope: {scope}");
 
-        // Create JWT
-        let jwt = jsonwebtoken::encode(
-            &JwtHeader::new(Algorithm::RS256),
+        let jwt = jwt::encode_rs256_pem(
+            &JwtHeader::rs256(),
             &Claims::new(&sa.client_email, &scope),
-            &EncodingKey::from_rsa_pem(sa.private_key.as_bytes()).map_err(|e| {
-                reqsign_core::Error::unexpected("failed to parse RSA private key").with_source(e)
-            })?,
-        )
-        .map_err(|e| reqsign_core::Error::unexpected("failed to encode JWT").with_source(e))?;
+            sa.private_key.as_bytes(),
+        )?;
 
         // Exchange JWT for access token
         let body =