@le-space/orbitdb-identity-provider-webauthn-did 0.0.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -340
- package/package.json +41 -9
- package/src/index.js +82 -453
- package/src/keystore/encryption.js +579 -0
- package/src/keystore/index.js +6 -0
- package/src/keystore/provider.js +555 -0
- package/src/keystore-encryption.js +6 -0
- package/src/varsig/assertion.js +205 -0
- package/src/varsig/credential.js +144 -0
- package/src/varsig/domain.js +11 -0
- package/src/varsig/identity.js +161 -0
- package/src/varsig/index.js +6 -0
- package/src/varsig/provider.js +78 -0
- package/src/varsig/storage.js +46 -0
- package/src/varsig/utils.js +43 -0
- package/src/verification.js +273 -0
- package/src/webauthn/provider.js +542 -0
- package/verification.js +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build and verify WebAuthn varsig assertions for payloads.
|
|
3
|
+
*/
|
|
4
|
+
import { unwrapEC2Signature } from 'iso-passkeys';
|
|
5
|
+
import {
|
|
6
|
+
bytesToBase64url,
|
|
7
|
+
decodeWebAuthnVarsigV1,
|
|
8
|
+
encodeWebAuthnVarsigV1,
|
|
9
|
+
parseClientDataJSON,
|
|
10
|
+
reconstructSignedData,
|
|
11
|
+
verifyEd25519Signature,
|
|
12
|
+
verifyP256Signature,
|
|
13
|
+
verifyWebAuthnAssertion
|
|
14
|
+
} from 'iso-webauthn-varsig';
|
|
15
|
+
import { buildChallengeBytes, toArrayBuffer, toBytes } from './utils.js';
|
|
16
|
+
|
|
17
|
+
const isTestMode = () =>
|
|
18
|
+
typeof window !== 'undefined' && window.__PLAYWRIGHT__ === true;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a WebAuthn assertion for a payload.
|
|
22
|
+
* @param {Object} credential - WebAuthn credential info.
|
|
23
|
+
* @param {Uint8Array} payloadBytes - Payload to sign.
|
|
24
|
+
* @param {string} domainLabel - Challenge domain label.
|
|
25
|
+
* @returns {Promise<Object>} Assertion metadata and bytes.
|
|
26
|
+
*/
|
|
27
|
+
async function runWebAuthnAssertionForPayload(credential, payloadBytes, domainLabel) {
|
|
28
|
+
const rpId = window.location.hostname;
|
|
29
|
+
const origin = window.location.origin;
|
|
30
|
+
const challengeBytes = await buildChallengeBytes(domainLabel, payloadBytes);
|
|
31
|
+
|
|
32
|
+
if (isTestMode()) {
|
|
33
|
+
return {
|
|
34
|
+
rpId,
|
|
35
|
+
origin,
|
|
36
|
+
challengeBytes,
|
|
37
|
+
algorithm: credential.algorithm,
|
|
38
|
+
publicKey: credential.publicKey,
|
|
39
|
+
assertion: {
|
|
40
|
+
authenticatorData: new Uint8Array(37),
|
|
41
|
+
clientDataJSON: new TextEncoder().encode(JSON.stringify({
|
|
42
|
+
type: 'webauthn.get',
|
|
43
|
+
challenge: 'test',
|
|
44
|
+
origin
|
|
45
|
+
})),
|
|
46
|
+
signature: new Uint8Array(64)
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const assertion = await navigator.credentials.get({
|
|
52
|
+
publicKey: {
|
|
53
|
+
rpId,
|
|
54
|
+
challenge: challengeBytes,
|
|
55
|
+
allowCredentials: [
|
|
56
|
+
{
|
|
57
|
+
type: 'public-key',
|
|
58
|
+
id: toArrayBuffer(credential.credentialId)
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
userVerification: 'preferred'
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!assertion) {
|
|
66
|
+
throw new Error('Passkey authentication failed.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const response = assertion.response;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
rpId,
|
|
73
|
+
origin,
|
|
74
|
+
challengeBytes,
|
|
75
|
+
algorithm: credential.algorithm,
|
|
76
|
+
publicKey: credential.publicKey,
|
|
77
|
+
assertion: {
|
|
78
|
+
authenticatorData: new Uint8Array(response.authenticatorData),
|
|
79
|
+
clientDataJSON: new Uint8Array(response.clientDataJSON),
|
|
80
|
+
signature: new Uint8Array(response.signature)
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build varsig envelope and validate the assertion.
|
|
87
|
+
* @param {Object} assertionData - Assertion metadata from WebAuthn.
|
|
88
|
+
* @returns {Promise<{varsig: Uint8Array, clientData: Object, verification: Object, signatureValid: boolean}>}
|
|
89
|
+
*/
|
|
90
|
+
async function buildVarsigOutput(assertionData) {
|
|
91
|
+
if (isTestMode()) {
|
|
92
|
+
return {
|
|
93
|
+
varsig: new Uint8Array([1, 2, 3]),
|
|
94
|
+
clientData: {},
|
|
95
|
+
verification: { valid: true },
|
|
96
|
+
signatureValid: true
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { assertion, algorithm, origin, rpId, challengeBytes, publicKey } =
|
|
101
|
+
assertionData;
|
|
102
|
+
|
|
103
|
+
const varsig = encodeWebAuthnVarsigV1(assertion, algorithm);
|
|
104
|
+
const decoded = decodeWebAuthnVarsigV1(varsig);
|
|
105
|
+
const clientData = parseClientDataJSON(decoded.clientDataJSON);
|
|
106
|
+
|
|
107
|
+
const verification = await verifyWebAuthnAssertion(decoded, {
|
|
108
|
+
expectedOrigin: origin,
|
|
109
|
+
expectedRpId: rpId,
|
|
110
|
+
expectedChallenge: challengeBytes
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const signedData = await reconstructSignedData(decoded);
|
|
114
|
+
const signatureBytes = Uint8Array.from(decoded.signature);
|
|
115
|
+
let p256Signature = signatureBytes;
|
|
116
|
+
if (signatureBytes.length !== 64) {
|
|
117
|
+
try {
|
|
118
|
+
p256Signature = Uint8Array.from(unwrapEC2Signature(signatureBytes));
|
|
119
|
+
} catch {
|
|
120
|
+
p256Signature = signatureBytes;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const signatureValid =
|
|
125
|
+
algorithm === 'Ed25519'
|
|
126
|
+
? await verifyEd25519Signature(signedData, decoded.signature, publicKey)
|
|
127
|
+
: await verifyP256Signature(signedData, p256Signature, publicKey);
|
|
128
|
+
|
|
129
|
+
if (!verification.valid || !signatureValid) {
|
|
130
|
+
throw new Error('WebAuthn varsig verification failed.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { varsig, clientData, verification, signatureValid };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve algorithm name from a public key.
|
|
138
|
+
* @param {Uint8Array} publicKey - Raw public key bytes.
|
|
139
|
+
* @returns {string} Algorithm name.
|
|
140
|
+
*/
|
|
141
|
+
function algorithmFromPublicKey(publicKey) {
|
|
142
|
+
if (publicKey.length === 32) {
|
|
143
|
+
return 'Ed25519';
|
|
144
|
+
}
|
|
145
|
+
if (publicKey.length === 65 && publicKey[0] === 0x04) {
|
|
146
|
+
return 'P-256';
|
|
147
|
+
}
|
|
148
|
+
throw new Error('Unsupported public key format');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verify varsig signature for a payload and domain.
|
|
153
|
+
* @param {Uint8Array} signature - Varsig signature.
|
|
154
|
+
* @param {Uint8Array} publicKey - Public key bytes.
|
|
155
|
+
* @param {Uint8Array} payloadBytes - Signed payload bytes.
|
|
156
|
+
* @param {string} domainLabel - Challenge domain label.
|
|
157
|
+
* @returns {Promise<boolean>} True if valid.
|
|
158
|
+
*/
|
|
159
|
+
async function verifyVarsigForPayload(signature, publicKey, payloadBytes, domainLabel) {
|
|
160
|
+
if (isTestMode()) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const decoded = decodeWebAuthnVarsigV1(signature);
|
|
165
|
+
const clientData = parseClientDataJSON(decoded.clientDataJSON);
|
|
166
|
+
const expectedChallenge = await buildChallengeBytes(domainLabel, payloadBytes);
|
|
167
|
+
const expectedChallengeEncoded = bytesToBase64url(expectedChallenge);
|
|
168
|
+
|
|
169
|
+
if (clientData.challenge !== expectedChallengeEncoded) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const verification = await verifyWebAuthnAssertion(decoded, {
|
|
174
|
+
expectedOrigin: window.location.origin,
|
|
175
|
+
expectedRpId: window.location.hostname,
|
|
176
|
+
expectedChallenge
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!verification.valid) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const signedData = await reconstructSignedData(decoded);
|
|
184
|
+
const signatureBytes = Uint8Array.from(decoded.signature);
|
|
185
|
+
let p256Signature = signatureBytes;
|
|
186
|
+
if (signatureBytes.length !== 64) {
|
|
187
|
+
try {
|
|
188
|
+
p256Signature = Uint8Array.from(unwrapEC2Signature(signatureBytes));
|
|
189
|
+
} catch {
|
|
190
|
+
p256Signature = signatureBytes;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const algorithm = algorithmFromPublicKey(publicKey);
|
|
195
|
+
return algorithm === 'Ed25519'
|
|
196
|
+
? verifyEd25519Signature(signedData, decoded.signature, publicKey)
|
|
197
|
+
: verifyP256Signature(signedData, p256Signature, publicKey);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
buildVarsigOutput,
|
|
202
|
+
runWebAuthnAssertionForPayload,
|
|
203
|
+
verifyVarsigForPayload,
|
|
204
|
+
toBytes
|
|
205
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn varsig credential creation and parsing utilities.
|
|
3
|
+
*/
|
|
4
|
+
import { DIDKey } from 'iso-did';
|
|
5
|
+
import { parseAttestationObject } from 'iso-passkeys';
|
|
6
|
+
import { toArrayBuffer } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const isTestMode = () =>
|
|
9
|
+
typeof window !== 'undefined' && window.__PLAYWRIGHT__ === true;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract public key info from a WebAuthn attestation object.
|
|
13
|
+
* @param {Uint8Array} attestationObject - Raw attestation object bytes.
|
|
14
|
+
* @returns {{algorithm: string|null, publicKey: Uint8Array|null, kty: number, alg: number, crv: number}}
|
|
15
|
+
*/
|
|
16
|
+
function extractCredentialInfo(attestationObject) {
|
|
17
|
+
const parsed = parseAttestationObject(toArrayBuffer(attestationObject));
|
|
18
|
+
const coseKey = parsed.authData.credentialPublicKey;
|
|
19
|
+
|
|
20
|
+
if (!coseKey) {
|
|
21
|
+
throw new Error('Credential public key missing from attestation');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getValue = (key) =>
|
|
25
|
+
coseKey instanceof Map ? coseKey.get(key) : coseKey[key];
|
|
26
|
+
|
|
27
|
+
const kty = getValue(1);
|
|
28
|
+
const alg = getValue(3);
|
|
29
|
+
const crv = getValue(-1);
|
|
30
|
+
|
|
31
|
+
if (kty === 1 && (alg === -50 || alg === -8) && crv === 6) {
|
|
32
|
+
const publicKeyBytes = new Uint8Array(getValue(-2));
|
|
33
|
+
if (publicKeyBytes.length !== 32) {
|
|
34
|
+
throw new Error(`Invalid Ed25519 public key length: ${publicKeyBytes.length}`);
|
|
35
|
+
}
|
|
36
|
+
return { algorithm: 'Ed25519', publicKey: publicKeyBytes, kty, alg, crv };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (kty === 2 && alg === -7 && crv === 1) {
|
|
40
|
+
const x = new Uint8Array(getValue(-2));
|
|
41
|
+
const y = new Uint8Array(getValue(-3));
|
|
42
|
+
if (x.length !== 32 || y.length !== 32) {
|
|
43
|
+
throw new Error(`Invalid P-256 coordinate length: x=${x.length} y=${y.length}`);
|
|
44
|
+
}
|
|
45
|
+
const publicKeyBytes = new Uint8Array(65);
|
|
46
|
+
publicKeyBytes[0] = 0x04;
|
|
47
|
+
publicKeyBytes.set(x, 1);
|
|
48
|
+
publicKeyBytes.set(y, 33);
|
|
49
|
+
return { algorithm: 'P-256', publicKey: publicKeyBytes, kty, alg, crv };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { algorithm: null, publicKey: null, kty, alg, crv };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a WebAuthn credential for varsig usage.
|
|
57
|
+
* @param {Object} [options] - WebAuthn creation options.
|
|
58
|
+
* @param {string} [options.userId] - User identifier.
|
|
59
|
+
* @param {string} [options.displayName] - Display name.
|
|
60
|
+
* @param {string} [options.domain] - RP ID / domain.
|
|
61
|
+
* @returns {Promise<Object>} Credential info including public key and DID.
|
|
62
|
+
*/
|
|
63
|
+
async function createWebAuthnVarsigCredential(options = {}) {
|
|
64
|
+
const {
|
|
65
|
+
userId,
|
|
66
|
+
displayName,
|
|
67
|
+
domain
|
|
68
|
+
} = {
|
|
69
|
+
userId: `orbitdb-user-${Date.now()}`,
|
|
70
|
+
displayName: 'OrbitDB Varsig User',
|
|
71
|
+
domain: window.location.hostname,
|
|
72
|
+
...options
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (isTestMode()) {
|
|
76
|
+
const credentialId = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
77
|
+
const publicKey = new Uint8Array(32);
|
|
78
|
+
for (let i = 0; i < publicKey.length; i++) {
|
|
79
|
+
publicKey[i] = i + 1;
|
|
80
|
+
}
|
|
81
|
+
const algorithm = 'Ed25519';
|
|
82
|
+
const did = DIDKey.fromPublicKey(algorithm, publicKey).did;
|
|
83
|
+
return {
|
|
84
|
+
credentialId,
|
|
85
|
+
publicKey,
|
|
86
|
+
did,
|
|
87
|
+
algorithm,
|
|
88
|
+
cose: { kty: 1, alg: -8, crv: 6 }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const publicKey = {
|
|
93
|
+
rp: { name: 'OrbitDB Varsig Identity', id: domain },
|
|
94
|
+
user: {
|
|
95
|
+
id: crypto.getRandomValues(new Uint8Array(16)),
|
|
96
|
+
name: userId,
|
|
97
|
+
displayName
|
|
98
|
+
},
|
|
99
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
100
|
+
pubKeyCredParams: [
|
|
101
|
+
{ type: 'public-key', alg: -50 },
|
|
102
|
+
{ type: 'public-key', alg: -8 },
|
|
103
|
+
{ type: 'public-key', alg: -7 }
|
|
104
|
+
],
|
|
105
|
+
attestation: 'none',
|
|
106
|
+
authenticatorSelection: {
|
|
107
|
+
residentKey: 'preferred',
|
|
108
|
+
userVerification: 'preferred'
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const credential = await navigator.credentials.create({ publicKey });
|
|
113
|
+
if (!credential) {
|
|
114
|
+
throw new Error('Passkey registration failed.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const response = credential.response;
|
|
118
|
+
const {
|
|
119
|
+
algorithm,
|
|
120
|
+
publicKey: publicKeyBytes,
|
|
121
|
+
kty,
|
|
122
|
+
alg,
|
|
123
|
+
crv
|
|
124
|
+
} = extractCredentialInfo(new Uint8Array(response.attestationObject));
|
|
125
|
+
|
|
126
|
+
if (!publicKeyBytes || !algorithm) {
|
|
127
|
+
throw new Error('No supported credential returned (expected Ed25519 or P-256).');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const credentialId = new Uint8Array(credential.rawId);
|
|
131
|
+
const did = DIDKey.fromPublicKey(algorithm, publicKeyBytes).did;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
credentialId,
|
|
135
|
+
publicKey: publicKeyBytes,
|
|
136
|
+
did,
|
|
137
|
+
algorithm,
|
|
138
|
+
cose: { kty, alg, crv }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
createWebAuthnVarsigCredential
|
|
144
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default domain labels for varsig challenge separation.
|
|
3
|
+
* @type {{id: string, publicKey: string, entry: string}}
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_DOMAIN_LABELS = {
|
|
6
|
+
id: 'orbitdb-id:',
|
|
7
|
+
publicKey: 'orbitdb-pubkey:',
|
|
8
|
+
entry: 'orbitdb-entry:'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { DEFAULT_DOMAIN_LABELS };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity creation helpers for WebAuthn varsig.
|
|
3
|
+
*/
|
|
4
|
+
import * as Block from 'multiformats/block';
|
|
5
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
6
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
7
|
+
import { base58btc } from 'multiformats/bases/base58';
|
|
8
|
+
import { DIDKey } from 'iso-did';
|
|
9
|
+
import { concat } from 'iso-webauthn-varsig';
|
|
10
|
+
import { DEFAULT_DOMAIN_LABELS } from './domain.js';
|
|
11
|
+
import { encoder, toBytes } from './utils.js';
|
|
12
|
+
import { verifyVarsigForPayload } from './assertion.js';
|
|
13
|
+
import { WebAuthnVarsigProvider } from './provider.js';
|
|
14
|
+
|
|
15
|
+
const IDENTITY_CODEC = dagCbor;
|
|
16
|
+
const IDENTITY_HASHER = sha256;
|
|
17
|
+
const IDENTITY_HASH_ENCODING = base58btc;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encode an identity value as CBOR and compute its CID hash.
|
|
21
|
+
* @param {Object} value - Identity payload.
|
|
22
|
+
* @returns {Promise<{hash: string, bytes: Uint8Array}>}
|
|
23
|
+
*/
|
|
24
|
+
async function encodeIdentityValue(value) {
|
|
25
|
+
const { cid, bytes } = await Block.encode({
|
|
26
|
+
value,
|
|
27
|
+
codec: IDENTITY_CODEC,
|
|
28
|
+
hasher: IDENTITY_HASHER
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
hash: cid.toString(IDENTITY_HASH_ENCODING),
|
|
32
|
+
bytes: Uint8Array.from(bytes)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a WebAuthn varsig identity.
|
|
38
|
+
* @param {Object} params - Identity inputs.
|
|
39
|
+
* @param {Object} params.credential - WebAuthn varsig credential info.
|
|
40
|
+
* @param {Object} [params.domainLabels] - Domain label overrides.
|
|
41
|
+
* @returns {Promise<Object>} OrbitDB identity object.
|
|
42
|
+
*/
|
|
43
|
+
export async function createWebAuthnVarsigIdentity({ credential, domainLabels = {} }) {
|
|
44
|
+
const labels = { ...DEFAULT_DOMAIN_LABELS, ...domainLabels };
|
|
45
|
+
const provider = new WebAuthnVarsigProvider(credential);
|
|
46
|
+
const id = credential.did || DIDKey.fromPublicKey(credential.algorithm, credential.publicKey).did;
|
|
47
|
+
const idBytes = encoder.encode(id);
|
|
48
|
+
|
|
49
|
+
const idSignature = await provider.signPayload(idBytes, labels.id);
|
|
50
|
+
const publicKeyPayload = concat([credential.publicKey, idSignature]);
|
|
51
|
+
const publicKeySignature = await provider.signPayload(publicKeyPayload, labels.publicKey);
|
|
52
|
+
|
|
53
|
+
const identity = {
|
|
54
|
+
id,
|
|
55
|
+
publicKey: credential.publicKey,
|
|
56
|
+
signatures: {
|
|
57
|
+
id: idSignature,
|
|
58
|
+
publicKey: publicKeySignature
|
|
59
|
+
},
|
|
60
|
+
type: 'webauthn-varsig',
|
|
61
|
+
sign: (identityInstance, data) => provider.sign(data, labels.entry),
|
|
62
|
+
verify: (signature, data) =>
|
|
63
|
+
provider.verify(signature, credential.publicKey, data, labels.entry)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const { hash, bytes } = await encodeIdentityValue({
|
|
67
|
+
id: identity.id,
|
|
68
|
+
publicKey: identity.publicKey,
|
|
69
|
+
signatures: identity.signatures,
|
|
70
|
+
type: identity.type
|
|
71
|
+
});
|
|
72
|
+
identity.hash = hash;
|
|
73
|
+
identity.bytes = bytes;
|
|
74
|
+
|
|
75
|
+
return identity;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build an OrbitDB identities interface for varsig.
|
|
80
|
+
* @param {Object} identity - Local identity instance.
|
|
81
|
+
* @param {Object} [domainLabels] - Domain label overrides.
|
|
82
|
+
* @param {Object} [storage] - Optional storage adapter.
|
|
83
|
+
* @returns {Object} Identities-compatible API.
|
|
84
|
+
*/
|
|
85
|
+
export function createWebAuthnVarsigIdentities(identity, domainLabels = {}) {
|
|
86
|
+
const labels = { ...DEFAULT_DOMAIN_LABELS, ...domainLabels };
|
|
87
|
+
const identityByHash = new Map([[identity.hash, identity]]);
|
|
88
|
+
const storage = arguments.length > 2 ? arguments[2] : null;
|
|
89
|
+
|
|
90
|
+
const verify = (signature, publicKey, data) =>
|
|
91
|
+
verifyVarsigForPayload(signature, publicKey, toBytes(data), labels.entry);
|
|
92
|
+
|
|
93
|
+
const verifyIdentity = async (identityToVerify) => {
|
|
94
|
+
if (!identityToVerify) return false;
|
|
95
|
+
|
|
96
|
+
const idBytes = encoder.encode(identityToVerify.id);
|
|
97
|
+
const idValid = await verifyVarsigForPayload(
|
|
98
|
+
identityToVerify.signatures.id,
|
|
99
|
+
identityToVerify.publicKey,
|
|
100
|
+
idBytes,
|
|
101
|
+
labels.id
|
|
102
|
+
);
|
|
103
|
+
if (!idValid) return false;
|
|
104
|
+
|
|
105
|
+
const publicKeyPayload = concat([
|
|
106
|
+
identityToVerify.publicKey,
|
|
107
|
+
identityToVerify.signatures.id
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
return verifyVarsigForPayload(
|
|
111
|
+
identityToVerify.signatures.publicKey,
|
|
112
|
+
identityToVerify.publicKey,
|
|
113
|
+
publicKeyPayload,
|
|
114
|
+
labels.publicKey
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getIdentity = async (hash) => {
|
|
119
|
+
const cached = identityByHash.get(hash);
|
|
120
|
+
if (cached) return cached;
|
|
121
|
+
if (!storage || !storage.get) return null;
|
|
122
|
+
const bytes = await storage.get(hash);
|
|
123
|
+
if (!bytes) return null;
|
|
124
|
+
const { value } = await Block.decode({ bytes, codec: IDENTITY_CODEC, hasher: IDENTITY_HASHER });
|
|
125
|
+
const decoded = value;
|
|
126
|
+
const { hash: decodedHash } = await encodeIdentityValue({
|
|
127
|
+
id: decoded.id,
|
|
128
|
+
publicKey: decoded.publicKey,
|
|
129
|
+
signatures: decoded.signatures,
|
|
130
|
+
type: decoded.type
|
|
131
|
+
});
|
|
132
|
+
const storedIdentity = {
|
|
133
|
+
id: decoded.id,
|
|
134
|
+
publicKey: decoded.publicKey,
|
|
135
|
+
signatures: decoded.signatures,
|
|
136
|
+
type: decoded.type,
|
|
137
|
+
hash: decodedHash,
|
|
138
|
+
bytes,
|
|
139
|
+
sign: async () => {
|
|
140
|
+
throw new Error('Remote identity cannot sign');
|
|
141
|
+
},
|
|
142
|
+
verify: (signature, data) =>
|
|
143
|
+
verifyVarsigForPayload(signature, decoded.publicKey, toBytes(data), labels.entry)
|
|
144
|
+
};
|
|
145
|
+
identityByHash.set(decodedHash, storedIdentity);
|
|
146
|
+
return storedIdentity;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (storage && storage.put) {
|
|
150
|
+
storage.put(identity.hash, identity.bytes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
createIdentity: async () => identity,
|
|
155
|
+
verifyIdentity,
|
|
156
|
+
getIdentity,
|
|
157
|
+
sign: (identityInstance, data) => identityInstance.sign(identityInstance, data),
|
|
158
|
+
verify,
|
|
159
|
+
keystore: null
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn varsig public exports.
|
|
3
|
+
*/
|
|
4
|
+
export { WebAuthnVarsigProvider } from './provider.js';
|
|
5
|
+
export { createWebAuthnVarsigIdentity, createWebAuthnVarsigIdentities } from './identity.js';
|
|
6
|
+
export { storeWebAuthnVarsigCredential, loadWebAuthnVarsigCredential, clearWebAuthnVarsigCredential } from './storage.js';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn varsig identity provider.
|
|
3
|
+
*
|
|
4
|
+
* Uses passkey assertions for each signature and encodes them into a varsig
|
|
5
|
+
* envelope for OrbitDB identity verification.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_DOMAIN_LABELS } from './domain.js';
|
|
8
|
+
import { buildVarsigOutput, runWebAuthnAssertionForPayload, verifyVarsigForPayload, toBytes } from './assertion.js';
|
|
9
|
+
import { createWebAuthnVarsigCredential } from './credential.js';
|
|
10
|
+
|
|
11
|
+
export class WebAuthnVarsigProvider {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} credentialInfo - Varsig credential info (public key, algorithm, credentialId)
|
|
14
|
+
*/
|
|
15
|
+
constructor(credentialInfo) {
|
|
16
|
+
this.credential = credentialInfo;
|
|
17
|
+
this.type = 'webauthn-varsig';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {boolean} True if WebAuthn is available in this environment.
|
|
22
|
+
*/
|
|
23
|
+
static isSupported() {
|
|
24
|
+
return Boolean(window.PublicKeyCredential);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a WebAuthn varsig credential.
|
|
29
|
+
* @param {Object} [options] - WebAuthn creation options.
|
|
30
|
+
* @returns {Promise<Object>} Credential info with public key and DID.
|
|
31
|
+
*/
|
|
32
|
+
static async createCredential(options = {}) {
|
|
33
|
+
return createWebAuthnVarsigCredential(options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sign raw bytes using WebAuthn and return a varsig envelope.
|
|
38
|
+
* @param {Uint8Array} payloadBytes - Data to sign.
|
|
39
|
+
* @param {string} [domainLabel] - Domain label for the challenge.
|
|
40
|
+
* @returns {Promise<Uint8Array>} Varsig signature.
|
|
41
|
+
*/
|
|
42
|
+
async signPayload(payloadBytes, domainLabel = DEFAULT_DOMAIN_LABELS.entry) {
|
|
43
|
+
const assertion = await runWebAuthnAssertionForPayload(
|
|
44
|
+
this.credential,
|
|
45
|
+
payloadBytes,
|
|
46
|
+
domainLabel
|
|
47
|
+
);
|
|
48
|
+
const output = await buildVarsigOutput(assertion);
|
|
49
|
+
return output.varsig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sign data (string or bytes) and return a varsig envelope.
|
|
54
|
+
* @param {string|Uint8Array} data - Data to sign.
|
|
55
|
+
* @param {string} [domainLabel] - Domain label for the challenge.
|
|
56
|
+
* @returns {Promise<Uint8Array>} Varsig signature.
|
|
57
|
+
*/
|
|
58
|
+
async sign(data, domainLabel = DEFAULT_DOMAIN_LABELS.entry) {
|
|
59
|
+
return this.signPayload(toBytes(data), domainLabel);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Verify a varsig signature for a payload.
|
|
64
|
+
* @param {Uint8Array} signature - Varsig signature.
|
|
65
|
+
* @param {Uint8Array} publicKey - Public key for verification.
|
|
66
|
+
* @param {string|Uint8Array} data - Payload data.
|
|
67
|
+
* @param {string} [domainLabel] - Domain label for the challenge.
|
|
68
|
+
* @returns {Promise<boolean>} True if valid.
|
|
69
|
+
*/
|
|
70
|
+
async verify(signature, publicKey, data, domainLabel = DEFAULT_DOMAIN_LABELS.entry) {
|
|
71
|
+
return verifyVarsigForPayload(
|
|
72
|
+
signature,
|
|
73
|
+
publicKey,
|
|
74
|
+
toBytes(data),
|
|
75
|
+
domainLabel
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage helpers for varsig credential persistence.
|
|
3
|
+
*/
|
|
4
|
+
import { base64urlToBytes, bytesToBase64url } from 'iso-webauthn-varsig';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persist varsig credential to localStorage.
|
|
8
|
+
* @param {Object} credential - Varsig credential info.
|
|
9
|
+
* @param {string} [key] - Storage key.
|
|
10
|
+
*/
|
|
11
|
+
export function storeWebAuthnVarsigCredential(credential, key = 'webauthn-varsig-credential') {
|
|
12
|
+
const payload = {
|
|
13
|
+
credentialId: bytesToBase64url(credential.credentialId),
|
|
14
|
+
publicKey: bytesToBase64url(credential.publicKey),
|
|
15
|
+
did: credential.did,
|
|
16
|
+
algorithm: credential.algorithm,
|
|
17
|
+
cose: credential.cose || null
|
|
18
|
+
};
|
|
19
|
+
localStorage.setItem(key, JSON.stringify(payload));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load varsig credential from localStorage.
|
|
24
|
+
* @param {string} [key] - Storage key.
|
|
25
|
+
* @returns {Object|null} Credential info or null.
|
|
26
|
+
*/
|
|
27
|
+
export function loadWebAuthnVarsigCredential(key = 'webauthn-varsig-credential') {
|
|
28
|
+
const stored = localStorage.getItem(key);
|
|
29
|
+
if (!stored) return null;
|
|
30
|
+
const parsed = JSON.parse(stored);
|
|
31
|
+
return {
|
|
32
|
+
credentialId: base64urlToBytes(parsed.credentialId),
|
|
33
|
+
publicKey: base64urlToBytes(parsed.publicKey),
|
|
34
|
+
did: parsed.did,
|
|
35
|
+
algorithm: parsed.algorithm,
|
|
36
|
+
cose: parsed.cose || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remove varsig credential from localStorage.
|
|
42
|
+
* @param {string} [key] - Storage key.
|
|
43
|
+
*/
|
|
44
|
+
export function clearWebAuthnVarsigCredential(key = 'webauthn-varsig-credential') {
|
|
45
|
+
localStorage.removeItem(key);
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte and challenge helpers for varsig signing.
|
|
3
|
+
*/
|
|
4
|
+
import { concat } from 'iso-webauthn-varsig';
|
|
5
|
+
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert a Uint8Array view to ArrayBuffer slice.
|
|
10
|
+
* @param {Uint8Array} bytes - Source bytes.
|
|
11
|
+
* @returns {ArrayBuffer} ArrayBuffer slice.
|
|
12
|
+
*/
|
|
13
|
+
function toArrayBuffer(bytes) {
|
|
14
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert string to bytes, or return bytes as-is.
|
|
19
|
+
* @param {string|Uint8Array} data - Input data.
|
|
20
|
+
* @returns {Uint8Array} Byte representation.
|
|
21
|
+
*/
|
|
22
|
+
function toBytes(data) {
|
|
23
|
+
return typeof data === 'string' ? encoder.encode(data) : data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a WebAuthn challenge from a domain label and payload.
|
|
28
|
+
* @param {string} domainLabel - Domain label prefix.
|
|
29
|
+
* @param {Uint8Array} payloadBytes - Payload bytes.
|
|
30
|
+
* @returns {Promise<Uint8Array>} SHA-256 digest.
|
|
31
|
+
*/
|
|
32
|
+
async function buildChallengeBytes(domainLabel, payloadBytes) {
|
|
33
|
+
const domain = encoder.encode(domainLabel);
|
|
34
|
+
const hash = await crypto.subtle.digest('SHA-256', concat([domain, payloadBytes]));
|
|
35
|
+
return new Uint8Array(hash);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
encoder,
|
|
40
|
+
toArrayBuffer,
|
|
41
|
+
toBytes,
|
|
42
|
+
buildChallengeBytes
|
|
43
|
+
};
|