You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

6.5 KiB

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:

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:

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:

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

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

// 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

// 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


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.