-
Notifications
You must be signed in to change notification settings - Fork 6
Description
Support did:key Authentication (LWS10 Spec)
🎯 Motivation
Backend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS protocol suite defines did:key authentication - a modern approach using self-issued JWTs signed with cryptographic keypairs.
Current pain point: WebID-TLS requires generating X.509 certificates, embedding public keys in WebID profiles, and configuring TLS. This is heavyweight for simple CLI tools and bot accounts.
What we want: Agent generates an ed25519 keypair, derives a did:key:z6Mk... identifier, signs a JWT, done. No certificates, no profile updates needed.
📋 Background
What is did:key?
A Decentralized Identifier method where the public key IS the identifier:
did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH
└─────────────────┬──────────────────────┘
base58btc(public key)
The public key is embedded in the DID itself - no need to fetch it from anywhere.
How it works
- Agent generates keypair (ed25519 or P-256)
- Derives did:key from public key
- Creates self-signed JWT with claims:
{ "sub": "did:key:z6Mk...", "iss": "did:key:z6Mk...", "client_id": "did:key:z6Mk...", "aud": "https://solid.social/", "exp": 1234567890, "iat": 1234567800 } - Signs JWT with private key
- Sends
Authorization: Bearer <jwt> - Server extracts public key from DID
- Verifies JWT signature
Why this is better than WebID-TLS
| WebID-TLS | did:key |
|---|---|
| Generate X.509 certificate | Generate keypair |
| Add cert to WebID profile | DID is self-describing |
| HTTPS required | Works over HTTP |
| Complex setup | Simple CLI workflow |
| RSA (large keys) | Ed25519 (compact) |
🔧 Technical Specification
LWS10 Requirements
Per W3C LWS Authentication Suite:
Required JWT Claims:
sub,iss,client_id: All MUST use samedid:key:URIaud: MUST include target authorization serverexp: Token expiration timestampiat: Token creation timestampalg: Cannot be "none"
Validation:
- Extract public key from
did:keyidentifier (per DID Key spec) - Verify JWT signature using extracted key (RFC 7515)
- Validate claims (timestamps, audience, triple equality of sub/iss/client_id)
- Optional: Allow clock skew tolerance (±5 min)
Supported Key Types
- Ed25519 (recommended):
did:key:z6Mk...(multicodec0xed) - P-256:
did:key:zDna...(multicodec0x1200) - secp256k1:
did:key:zQ3s...(multicodec0xe7)
🛠️ Implementation Plan
Phase 1: Core Authentication (~2-3 hours)
New file: src/auth/did-key.js
import { jwtVerify, importJWK } from 'jose';
import { base58btc } from 'multiformats/bases/base58';
import * as ed25519 from '@noble/ed25519';
/**
* Verify did:key JWT authentication
* @param {string} token - Bearer token
* @param {string} audience - Expected audience (server URL)
* @returns {string} - WebID derived from did:key
*/
export async function verifyDidKeyJWT(token, audience) {
// Decode without verification to get DID
const { payload } = await jwtVerify(token, async (header, token) => {
const did = token.payload.sub;
if (!did?.startsWith('did:key:')) {
throw new Error('Invalid DID format');
}
// Extract public key from did:key
const publicKey = await didKeyToPublicKey(did);
return publicKey;
});
// Validate LWS10 constraints
if (payload.sub !== payload.iss || payload.sub !== payload.client_id) {
throw new Error('sub, iss, and client_id must be identical');
}
if (!Array.isArray(payload.aud) || !payload.aud.includes(audience)) {
throw new Error('Invalid audience');
}
if (!payload.exp || !payload.iat) {
throw new Error('Missing exp or iat claims');
}
// Return WebID (in our case, same as DID for now)
return payload.sub;
}
/**
* Extract public key from did:key identifier
* Supports ed25519, P-256, secp256k1
*/
async function didKeyToPublicKey(did) {
const multibaseKey = did.replace('did:key:', '');
const bytes = base58btc.decode(multibaseKey);
// First 2 bytes are multicodec prefix
const codec = (bytes[0] << 8) | bytes[1];
const publicKeyBytes = bytes.slice(2);
switch (codec) {
case 0xed: // Ed25519
return await ed25519PublicKeyToJWK(publicKeyBytes);
case 0x1200: // P-256
return await p256PublicKeyToJWK(publicKeyBytes);
default:
throw new Error(`Unsupported key type: 0x${codec.toString(16)}`);
}
}
async function ed25519PublicKeyToJWK(publicKeyBytes) {
return {
kty: 'OKP',
crv: 'Ed25519',
x: Buffer.from(publicKeyBytes).toString('base64url')
};
}Update: src/auth/middleware.js
import { verifyDidKeyJWT } from './did-key.js';
// In authorize() function, add did:key support:
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.slice(7);
try {
// Try did:key JWT first
const webId = await verifyDidKeyJWT(token, request.hostname);
return { authorized: true, webId };
} catch (didKeyErr) {
// Fall through to existing token/OIDC validation
}
}Phase 2: CLI Tool Support (~1 hour)
New command: jss auth generate-did-key
// bin/jss.js
program
.command('auth')
.command('generate-did-key')
.description('Generate did:key credentials for agent authentication')
.action(async () => {
const { privateKey, publicKey } = await ed25519.utils.randomPrivateKey();
const did = await publicKeyToDidKey(publicKey);
console.log('Generated did:key credentials:');
console.log(`\nDID: ${did}`);
console.log(`Private Key (hex): ${Buffer.from(privateKey).toString('hex')}`);
console.log('\nSave private key securely. Use it to sign JWTs for authentication.');
});Phase 3: Documentation (~30 min)
Add to README.md:
### did:key Authentication (Agents & Bots)
For autonomous agents, CLI tools, and backend services:
**Generate credentials:**
```bash
jss auth generate-did-key
# DID: did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH
# Private Key: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60Create JWT and authenticate:
import { SignJWT } from 'jose';
import * as ed25519 from '@noble/ed25519';
const privateKeyHex = '9d61b19...';
const did = 'did:key:z6Mk...';
const jwt = await new SignJWT({})
.setProtectedHeader({ alg: 'EdDSA' })
.setSubject(did)
.setIssuer(did)
.setAudience('https://solid.social/')
.claim('client_id', did)
.setExpirationTime('1h')
.setIssuedAt()
.sign(privateKeyFromHex(privateKeyHex));
// Use JWT
const res = await fetch('https://solid.social/alice/private/', {
headers: { 'Authorization': `Bearer ${jwt}` }
});Benefits:
- No certificates or profile setup
- Self-describing identifiers
- Standard JWT format
- Works over HTTP or HTTPS
### Phase 4: Testing (~1 hour)
**New file:** `test/did-key.test.js`
```javascript
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { SignJWT, generateKeyPair } from 'jose';
import { verifyDidKeyJWT } from '../src/auth/did-key.js';
describe('did:key Authentication', () => {
it('should verify valid did:key JWT', async () => {
const { publicKey, privateKey } = await generateKeyPair('EdDSA');
const did = await publicKeyToDidKey(publicKey);
const jwt = await new SignJWT({})
.setProtectedHeader({ alg: 'EdDSA' })
.setSubject(did)
.setIssuer(did)
.setAudience('https://example.com/')
.claim('client_id', did)
.setExpirationTime('1h')
.setIssuedAt()
.sign(privateKey);
const webId = await verifyDidKeyJWT(jwt, 'https://example.com/');
assert.strictEqual(webId, did);
});
it('should reject mismatched sub/iss/client_id', async () => {
// Test validation logic
});
it('should reject invalid audience', async () => {
// Test audience validation
});
});
📦 Dependencies
New:
multiformats- Multibase/multicodec decoding@noble/ed25519- Ed25519 crypto operations
Existing (already installed):
jose- JWT verification
🎯 Use Cases
- CLI tools -
jss-cliauthenticates to remote pods - Backend bots - Automated agents that manage resources
- Server-to-server - Microservices accessing pod data
- IoT devices - Resource-constrained agents (Ed25519 is tiny)
- LWS protocol compliance - Interop with other LWS implementations
🔗 References
🚀 Acceptance Criteria
- Verify Ed25519 did:key JWTs
- Support P-256 did:key JWTs
- Validate all LWS10 required claims
- CLI command to generate did:key credentials
- Integration with existing auth middleware
- Tests covering validation rules
- Documentation with examples
- No breaking changes to existing auth methods
Estimated effort: 4-5 hours total
Priority: Medium (nice alternative to WebID-TLS)
Dependencies: None (adds to existing auth stack)