# JWT Algorithm Investigation Results **Date**: 2025-10-31 **Investigator**: Auto (AI Assistant) **Status**: ✅ **INVESTIGATION COMPLETE** --- ## 🔴 CRITICAL FINDING: DID-Based JWT Signing Required (ES256K) ### Conclusion **The endorser-ch API expects DID-based JWTs signed with ES256K (or ES256K-R), NOT HMAC-SHA256 (HS256).** The current implementation in `TestNativeFetcher.java` using HMAC-SHA256 is **INCORRECT** and will fail authentication. --- ## Investigation Details ### TimeSafari Repository Findings **Location**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` **Function**: `createEndorserJwtForKey()` **Implementation**: ```typescript export async function createEndorserJwtForKey( account: KeyMetaWithPrivate, payload: object, expiresIn?: number, ) { if (account?.identity) { const identity: IIdentifier = JSON.parse(account.identity!); const privateKeyHex = identity.keys[0].privateKeyHex; const signer = await SimpleSigner(privateKeyHex as string); const options = { // alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests issuer: account.did, signer: signer, expiresIn: undefined as number | undefined, }; if (expiresIn) { options.expiresIn = expiresIn; } return didJwt.createJWT(payload, options); } // ... passkey handling ... } ``` **Key Points**: - Uses `did-jwt.createJWT()` library - Uses `SimpleSigner(privateKeyHex)` - signs with Ethereum private key - Algorithm is **ES256K** (default) - Signs with DID private key, NOT a shared secret **Library Used**: `did-jwt` (DID-based JWT library) --- ### endorser-ch Repository Findings **Location**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` **Function**: `decodeAndVerifyJwt()` **Implementation**: ```javascript export async function decodeAndVerifyJwt(jwt) { const pieces = jwt.split('.') const header = JSON.parse(base64url.decode(pieces[0])) const payload = JSON.parse(base64url.decode(pieces[1])) const issuerDid = payload.iss if (issuerDid.startsWith(ETHR_DID_PREFIX)) { try { const verifiedResult = await didJwt.verifyJWT(jwt, {resolver}) return verifiedResult } catch (e) { return Promise.reject({ clientError: { message: `JWT failed verification: ` + e.toString(), code: JWT_VERIFY_FAILED_CODE } }) } } // ... other DID methods ... } ``` **Key Points**: - Uses `did-jwt.verifyJWT(jwt, {resolver})` - DID-based verification - Verifies signature using DID resolver (resolves DID to public key) - **NO shared secret used** - uses DID public key from resolver - Algorithm: **ES256K** (implicit from did-jwt library) **Middleware Location**: `~/projects/timesafari/endorser-ch/src/common/server.js` **Authentication Flow**: ```javascript decodeAndVerifyJwt(authorizationJwt) .then((result) => { const { header, issuer, payload, verified } = result if (!verified) { res.status(400).json({ error: { message: "Signature failed validation.", code: ERROR_CODES.JWT_VERIFY_FAILED } }).end() } else { res.locals.authTokenIssuer = issuer next() } }) ``` --- ## Impact on Current Implementation ### Current Implementation (WRONG) **File**: `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java` **Current Approach**: HMAC-SHA256 with shared secret ```java Mac hmac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec( jwtSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256" ); hmac.init(secretKey); byte[] signatureBytes = hmac.doFinal(unsignedToken.getBytes(StandardCharsets.UTF_8)); ``` **Problem**: Server expects ES256K with DID private key, NOT HMAC-SHA256 with shared secret --- ## Required Changes ### Action Required 1. **Remove HMAC-SHA256 implementation** - This is completely wrong 2. **Implement DID-based signing (ES256K)** - Sign with Ethereum private key 3. **Access DID private key** - Need to retrieve private key from DID/account storage 4. **Use did-jwt-java or web3j** - Java library for DID-based JWT signing ### Implementation Options #### Option 1: Use did-jwt-java Library (Recommended) ```java // Add dependency to build.gradle implementation 'io.uport:uport-did-jwt:3.1.0' // Sign with DID private key import io.uport.sdk.did.jwt.DIDJWT; import io.uport.sdk.did.jwt.SimpleSigner; String privateKeyHex = getPrivateKeyForDid(activeDid); // Need to implement SimpleSigner signer = new SimpleSigner(privateKeyHex); String jwt = DIDJWT.createJWT(payload, signer, issuer: activeDid); ``` #### Option 2: Use web3j for Ethereum Signing ```java // Add dependency implementation 'org.web3j:core:4.9.8' import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Sign; ECKeyPair keyPair = getKeyPairForDid(activeDid); // Need to implement Sign.SignatureData signature = Sign.signMessage( unsignedToken.getBytes(StandardCharsets.UTF_8), keyPair ); // Then encode signature according to ES256K format ``` --- ## Next Steps 1. **Remove `jwtSecret` parameter** - No longer needed (shared secret not used) 2. **Add DID private key retrieval** - Need mechanism to get private key for `activeDid` 3. **Implement ES256K signing** - Using did-jwt-java or web3j 4. **Update `configureNativeFetcher()`** - Remove `jwtSecret`, add private key retrieval mechanism 5. **Test with real API** - Verify JWTs are accepted by endorser-ch server --- ## References - **TimeSafari Implementation**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` - **endorser-ch Verification**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` - **did-jwt Library**: https://github.com/decentralized-identity/did-jwt - **did-jwt-java**: https://github.com/uport-project/uport-did-jwt (if available) or alternative Java DID libraries --- ## Evidence Summary | Component | Finding | Evidence | |-----------|---------|----------| | **TimeSafari JWT Creation** | ✅ DID-based (ES256K) | Uses `didJwt.createJWT()` with `SimpleSigner(privateKeyHex)` | | **endorser-ch JWT Verification** | ✅ DID-based (ES256K) | Uses `didJwt.verifyJWT(jwt, {resolver})` | | **Current TestNativeFetcher** | ❌ HMAC-SHA256 | Uses `Mac.getInstance("HmacSHA256")` with shared secret | | **Shared Secret Config** | ❌ Not Used | No `JWT_SECRET` found in endorser-ch, no shared secret in TimeSafari | --- **Status**: Investigation complete. Implementation changes required.