221 lines
6.5 KiB
Markdown
221 lines
6.5 KiB
Markdown
# 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.
|
|
|