feat(ueps): initial push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
38087671de
2 changed files with 196 additions and 0 deletions
187
consent.js
Normal file
187
consent.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* UEPS Consent Token — wire-level consent for LetherNet traffic
|
||||
*
|
||||
* A consent token is an Ed25519-signed message that authorises a specific
|
||||
* data flow between two parties for a limited time. Every hop in LetherNet
|
||||
* checks the consent token before forwarding.
|
||||
*
|
||||
* Token format:
|
||||
* {
|
||||
* version: 1,
|
||||
* issuer: "iTHN...", // wallet address of consent giver
|
||||
* subject: "iTHN...", // wallet address of consent recipient
|
||||
* level: 2, // consent level (0-4)
|
||||
* capabilities: ["vpn","dns"], // what the subject can do
|
||||
* issued: 1712000000, // unix timestamp
|
||||
* expires: 1712003600, // unix timestamp (1 hour default)
|
||||
* scope: "charon.lthn", // gateway scope
|
||||
* nonce: "random-hex", // replay protection
|
||||
* signature: "ed25519-sig" // signed by issuer's chain key
|
||||
* }
|
||||
*
|
||||
* Consent levels:
|
||||
* 0 = None (reject all)
|
||||
* 1 = Minimal (routing only, no data retention)
|
||||
* 2 = Standard (service provision, ephemeral data)
|
||||
* 3 = Extended (analytics, scoped retention)
|
||||
* 4 = Full (complete trust, persistent data)
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
class ConsentToken {
|
||||
/**
|
||||
* Create a new consent token
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.issuer - iTHN address of consent giver
|
||||
* @param {string} opts.subject - iTHN address of consent recipient
|
||||
* @param {number} opts.level - Consent level (0-4)
|
||||
* @param {string[]} opts.capabilities - Allowed capabilities
|
||||
* @param {string} opts.scope - Gateway scope (e.g., "charon.lthn")
|
||||
* @param {number} opts.ttl - Time to live in seconds (default: 3600)
|
||||
*/
|
||||
constructor(opts) {
|
||||
this.version = 1;
|
||||
this.issuer = opts.issuer;
|
||||
this.subject = opts.subject;
|
||||
this.level = Math.max(0, Math.min(4, opts.level || 0));
|
||||
this.capabilities = opts.capabilities || [];
|
||||
this.scope = opts.scope || '';
|
||||
this.issued = Math.floor(Date.now() / 1000);
|
||||
this.expires = this.issued + (opts.ttl || 3600);
|
||||
this.nonce = crypto.randomBytes(16).toString('hex');
|
||||
this.signature = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signable payload (everything except signature)
|
||||
*/
|
||||
payload() {
|
||||
return JSON.stringify({
|
||||
v: this.version,
|
||||
iss: this.issuer,
|
||||
sub: this.subject,
|
||||
lvl: this.level,
|
||||
cap: this.capabilities,
|
||||
scp: this.scope,
|
||||
iat: this.issued,
|
||||
exp: this.expires,
|
||||
non: this.nonce,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the token with an Ed25519 private key
|
||||
* In production, this uses the chain wallet's spend key
|
||||
*/
|
||||
sign(privateKey) {
|
||||
const payload = Buffer.from(this.payload());
|
||||
this.signature = crypto.sign(null, payload, privateKey).toString('hex');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the token signature against a public key
|
||||
* In production, the public key comes from the chain alias lookup
|
||||
*/
|
||||
verify(publicKey) {
|
||||
if (!this.signature) return false;
|
||||
const payload = Buffer.from(this.payload());
|
||||
const sig = Buffer.from(this.signature, 'hex');
|
||||
return crypto.verify(null, payload, publicKey, sig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is currently valid (not expired, not future-dated)
|
||||
*/
|
||||
isValid() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return now >= this.issued && now < this.expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token grants a specific capability
|
||||
*/
|
||||
hasCapability(cap) {
|
||||
return this.capabilities.includes(cap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialise to compact wire format
|
||||
*/
|
||||
toWire() {
|
||||
return Buffer.from(JSON.stringify({
|
||||
...JSON.parse(this.payload()),
|
||||
sig: this.signature,
|
||||
})).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialise from wire format
|
||||
*/
|
||||
static fromWire(wire) {
|
||||
const data = JSON.parse(Buffer.from(wire, 'base64').toString());
|
||||
const token = new ConsentToken({
|
||||
issuer: data.iss,
|
||||
subject: data.sub,
|
||||
level: data.lvl,
|
||||
capabilities: data.cap,
|
||||
scope: data.scp,
|
||||
ttl: data.exp - data.iat,
|
||||
});
|
||||
token.issued = data.iat;
|
||||
token.expires = data.exp;
|
||||
token.nonce = data.non;
|
||||
token.signature = data.sig;
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable display
|
||||
*/
|
||||
toString() {
|
||||
const levelNames = ['None', 'Minimal', 'Standard', 'Extended', 'Full'];
|
||||
return `ConsentToken{${this.issuer?.slice(0,10)}→${this.subject?.slice(0,10)} L${this.level}(${levelNames[this.level]}) caps=[${this.capabilities}] scope=${this.scope} valid=${this.isValid()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
// === Test / Demo ===
|
||||
if (require.main === module) {
|
||||
// Generate test keypair (in production, these come from the chain wallet)
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
||||
|
||||
// Create a consent token: user authorises gateway for VPN + DNS
|
||||
const token = new ConsentToken({
|
||||
issuer: 'iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY',
|
||||
subject: 'iTHNQGNCbu8BH7FhQuzsv5CVsE3veDX1CFCxk9jsqRfA4tFcUPNwQa4DWLFvyaKvs3HGb8xfGDGQq871c9P4iRpt8SFusoqSzJ',
|
||||
level: 2,
|
||||
capabilities: ['vpn', 'dns'],
|
||||
scope: 'charon.lthn',
|
||||
ttl: 3600,
|
||||
});
|
||||
|
||||
// Sign it
|
||||
token.sign(privateKey);
|
||||
console.log('Created:', token.toString());
|
||||
console.log('');
|
||||
|
||||
// Verify it
|
||||
const valid = token.verify(publicKey);
|
||||
console.log('Signature valid:', valid);
|
||||
console.log('Token valid (not expired):', token.isValid());
|
||||
console.log('Has VPN capability:', token.hasCapability('vpn'));
|
||||
console.log('Has compute capability:', token.hasCapability('compute'));
|
||||
console.log('');
|
||||
|
||||
// Wire format roundtrip
|
||||
const wire = token.toWire();
|
||||
console.log('Wire format:', wire.slice(0, 60) + '...');
|
||||
console.log('Wire size:', wire.length, 'bytes');
|
||||
|
||||
const restored = ConsentToken.fromWire(wire);
|
||||
console.log('Roundtrip:', restored.toString());
|
||||
console.log('Roundtrip verify:', restored.verify(publicKey));
|
||||
}
|
||||
|
||||
module.exports = { ConsentToken };
|
||||
9
package.json
Normal file
9
package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "lthn-ueps",
|
||||
"version": "0.1.0",
|
||||
"description": "UEPS consent tokens for LetherNet wire-level consent",
|
||||
"main": "consent.js",
|
||||
"scripts": {
|
||||
"test": "node consent.js"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue