feat: enhance WebAuthn integration with new put options and improved signature handling

This commit is contained in:
x 2025-02-23 17:29:11 +07:00
parent 374fd592d8
commit e2820a1888
6 changed files with 175 additions and 76 deletions

View File

@ -13,7 +13,8 @@
<button id="create">Create keypass to get public key</button>
<button id="sign">Sign data using passkey</button>
<button id="verify">Verify signature of passkey</button>
<button id="put">Put to self's graph</button>
<button id="put">Put to self's graph with WebAuthn</button>
<button id="put-with-pair">Put to self's graph with pair</button>
<script src="./webauthn.js"></script>
</body>
</html>

View File

@ -72,12 +72,16 @@ document.getElementById('create').onclick = async () => {
name: "Example User",
displayName: "Example User"
},
// See the list of algos: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// The 2 algos below are required in order to work with SEA
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 },
{ type: "public-key", alg: -37 }
{ type: "public-key", alg: -7 }, // ECDSA, P-256 curve, for signing
{ type: "public-key", alg: -25 }, // ECDH, P-256 curve, for creating shared secrets using SEA.secret
{ type: "public-key", alg: -257 }
],
authenticatorSelection: { userVerification: "preferred" },
authenticatorSelection: {
userVerification: "preferred"
},
timeout: 60000,
attestation: "none"
}
@ -118,9 +122,9 @@ const authenticator = async (data) => {
timeout: 60000
}
};
console.log("Auth options:", options);
const assertion = await navigator.credentials.get(options);
console.log("SIGNED:", {options, assertion});
return assertion.response;
};
@ -159,4 +163,14 @@ document.getElementById('put').onclick = async () => {
console.log("Data:", data);
})
}, 2000)
}
document.getElementById('put-with-pair').onclick = async () => {
const bob = await SEA.pair()
gun.get(`~${bob.pub}`).get('test').put("this is bob", null, { opt: { authenticator: bob }})
setTimeout(() => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
console.log("Data:", data);
})
}, 2000)
}

105
sea.js
View File

@ -388,58 +388,81 @@
opt = opt || {};
// Format and return the final response
async function next(r, opt, cb) {
try {
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){} }
return r;
} catch(e) {
console.warn('SEA.sign response error', e);
return r;
}
async function next(r) {
try {
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){} }
return r;
} catch(e) {
console.warn('SEA.sign response error', e);
return r;
}
}
// Validate inputs
if(u === data){ throw '`undefined` not allowed.' }
if(!(pair||opt).priv && typeof pair !== 'function'){
if(!SEA.I){ throw 'No signing key.' }
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
// WebAuthn
async function wa(res, json) {
var r = {
m: json,
s: res.signature ? shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64') : undefined,
a: shim.Buffer.from(res.authenticatorData, 'binary').toString('base64'),
c: shim.Buffer.from(res.clientDataJSON, 'binary').toString('base64')
};
if (!r.s || !r.a || !r.c) throw "WebAuthn signature invalid";
return next(r);
}
// External auth fn
async function ea(res, json) {
if (!res) throw new Error('Empty auth response');
if (typeof res === 'string') {
return next({ m: json, s: res });
}
var json = await S.parse(data);
var check = opt.check = opt.check || json;
// Return early if already signed
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
&& u !== await SEA.verify(check, pair)){
return next(await S.parse(check), opt, cb);
}
// Handle WebAuthn
if(typeof pair === 'function'){
const response = await pair(data);
var r = {
if (res.signature) {
return next({
m: json,
s: response.signature ? shim.Buffer.from(response.signature, 'binary').toString(opt.encode || 'base64') : undefined,
a: response.authenticatorData ? shim.Buffer.from(response.authenticatorData, 'binary').toString('base64') : undefined,
c: response.clientDataJSON ? shim.Buffer.from(response.clientDataJSON, 'binary').toString('base64') : undefined
};
if (!r.s || !r.a || !r.c) { throw "WebAuthn signature invalid"; }
return next(r, opt, cb);
s: shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64')
});
}
throw new Error('Invalid auth format');
}
// Handle regular signing
// Key pair
async function kp(pair, json) {
var jwk = S.jwk(pair.pub, pair.priv);
if (!jwk) throw new Error('Invalid key pair');
var hash = await sha(json);
var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign'])
.then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash)))
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
return next({
m: json,
m: json,
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
}, opt, cb);
});
}
if(u === data) throw '`undefined` not allowed.';
if(!(pair||opt).priv && typeof pair !== 'function'){
if(!SEA.I) throw 'No signing key.';
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
}
var json = await S.parse(data);
var check = opt.check = opt.check || json;
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
&& u !== await SEA.verify(check, pair)){
return next(await S.parse(check));
}
if(typeof pair === 'function') {
const response = await pair(data);
const fn = response.authenticatorData ? wa : ea;
return fn(response, json);
}
return kp(pair, json);
} catch(e) {
SEA.err = e;
if(SEA.throw){ throw e }
@ -1537,10 +1560,16 @@
const raw = await S.parse(val) || {}
if ((user.is || opt.authenticator) && (tmp = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub) && tmp && !raw['*'] && !raw['+'] && (pub === tmp || (pub !== tmp && opt.cert))){
SEA.opt.pack(msg.put, packed => {
const authenticator = opt.authenticator || (user._).sea;
// Determine the authenticator to use - external or user's
const authenticator = typeof opt.authenticator === 'function' ? opt.authenticator : (opt.authenticator || (user._).sea);
const upub = tmp;
// Validate authenticator
if (!authenticator) return no("Missing authenticator");
SEA.sign(packed, authenticator, async function(data) {
if (u === data) return no(SEA.err || 'Signature fail.')
// Validate signature format
if (!data.m || !data.s) return no('Invalid signature format')
msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s}
msg.put['='] = tmp

View File

@ -147,10 +147,16 @@
const raw = await S.parse(val) || {}
if ((user.is || opt.authenticator) && (tmp = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub) && tmp && !raw['*'] && !raw['+'] && (pub === tmp || (pub !== tmp && opt.cert))){
SEA.opt.pack(msg.put, packed => {
const authenticator = opt.authenticator || (user._).sea;
// Determine the authenticator to use - external or user's
const authenticator = typeof opt.authenticator === 'function' ? opt.authenticator : (opt.authenticator || (user._).sea);
const upub = tmp;
// Validate authenticator
if (!authenticator) return no("Missing authenticator");
SEA.sign(packed, authenticator, async function(data) {
if (u === data) return no(SEA.err || 'Signature fail.')
// Validate signature format
if (!data.m || !data.s) return no('Invalid signature format')
msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s}
msg.put['='] = tmp
@ -254,4 +260,4 @@
var fl = Math.floor; // TODO: Still need to fix inconsistent state issue.
// TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible.
}());
}());

View File

@ -9,7 +9,7 @@
opt = opt || {};
// Format and return the final response
async function next(r, opt, cb) {
async function next(r) {
try {
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){} }
@ -19,48 +19,71 @@
return r;
}
}
// Validate inputs
if(u === data){ throw '`undefined` not allowed.' }
// WebAuthn
async function wa(res, json) {
var r = {
m: json,
s: res.signature ? shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64') : undefined,
a: shim.Buffer.from(res.authenticatorData, 'binary').toString('base64'),
c: shim.Buffer.from(res.clientDataJSON, 'binary').toString('base64')
};
if (!r.s || !r.a || !r.c) throw "WebAuthn signature invalid";
return next(r);
}
// External auth fn
async function ea(res, json) {
if (!res) throw new Error('Empty auth response');
if (typeof res === 'string') {
return next({ m: json, s: res });
}
if (res.signature) {
return next({
m: json,
s: shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64')
});
}
throw new Error('Invalid auth format');
}
// Key pair
async function kp(pair, json) {
var jwk = S.jwk(pair.pub, pair.priv);
if (!jwk) throw new Error('Invalid key pair');
var hash = await sha(json);
var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign'])
.then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash)))
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
return next({
m: json,
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
});
}
if(u === data) throw '`undefined` not allowed.';
if(!(pair||opt).priv && typeof pair !== 'function'){
if(!SEA.I){ throw 'No signing key.' }
if(!SEA.I) throw 'No signing key.';
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
}
var json = await S.parse(data);
var check = opt.check = opt.check || json;
// Return early if already signed
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
&& u !== await SEA.verify(check, pair)){
return next(await S.parse(check), opt, cb);
return next(await S.parse(check));
}
// Handle WebAuthn
if(typeof pair === 'function'){
if(typeof pair === 'function') {
const response = await pair(data);
var r = {
m: json,
s: response.signature ? shim.Buffer.from(response.signature, 'binary').toString(opt.encode || 'base64') : undefined,
a: response.authenticatorData ? shim.Buffer.from(response.authenticatorData, 'binary').toString('base64') : undefined,
c: response.clientDataJSON ? shim.Buffer.from(response.clientDataJSON, 'binary').toString('base64') : undefined
};
if (!r.s || !r.a || !r.c) { throw "WebAuthn signature invalid"; }
return next(r, opt, cb);
const fn = response.authenticatorData ? wa : ea;
return fn(response, json);
}
// Handle regular signing
var jwk = S.jwk(pair.pub, pair.priv);
var hash = await sha(json);
var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign'])
.then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash)))
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
return next({
m: json,
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
}, opt, cb);
return kp(pair, json);
} catch(e) {
SEA.err = e;
if(SEA.throw){ throw e }

View File

@ -328,6 +328,32 @@ describe('SEA', function(){
describe('User', function(){
var gun = Gun(), gtmp;
it("put to user graph without having to be authenticated (provide pair)", function(done){(async function(){
var bob = await SEA.pair();
gun.get(`~${bob.pub}`).get('test').put('this is Bob', (ack) => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
expect(ack.err).to.not.be.ok()
expect(data).to.be('this is Bob')
done();
})
}, {opt: {authenticator: bob}})
})()});
it("put to user graph using external authenticator (nested SEA.sign)", function(done){(async function(){
var bob = await SEA.pair();
async function authenticator(data) {
const sig = await SEA.sign(data, bob)
return sig
}
gun.get(`~${bob.pub}`).get('test').put('this is Bob', (ack) => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
expect(ack.err).to.not.be.ok()
expect(data).to.be('this is Bob')
done();
})
}, {opt: {authenticator: authenticator}})
})()});
it('test', function(done){
var g = Gun();
user = g.user();