@mysten/sui 1.16.2 → 1.17.0
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/CHANGELOG.md +6 -0
- package/dist/cjs/bcs/bcs.d.ts +13 -0
- package/dist/cjs/bcs/bcs.js +6 -0
- package/dist/cjs/bcs/bcs.js.map +2 -2
- package/dist/cjs/bcs/index.d.ts +13 -0
- package/dist/cjs/bcs/index.js +2 -1
- package/dist/cjs/bcs/index.js.map +2 -2
- package/dist/cjs/cryptography/signature-scheme.d.ts +3 -1
- package/dist/cjs/cryptography/signature-scheme.js +4 -2
- package/dist/cjs/cryptography/signature-scheme.js.map +2 -2
- package/dist/cjs/cryptography/signature.d.ts +8 -0
- package/dist/cjs/cryptography/signature.js +5 -2
- package/dist/cjs/cryptography/signature.js.map +3 -3
- package/dist/cjs/keypairs/passkey/index.d.ts +3 -0
- package/dist/cjs/keypairs/passkey/index.js +28 -0
- package/dist/cjs/keypairs/passkey/index.js.map +7 -0
- package/dist/cjs/keypairs/passkey/keypair.d.ts +59 -0
- package/dist/cjs/keypairs/passkey/keypair.js +168 -0
- package/dist/cjs/keypairs/passkey/keypair.js.map +7 -0
- package/dist/cjs/keypairs/passkey/publickey.d.ts +72 -0
- package/dist/cjs/keypairs/passkey/publickey.js +168 -0
- package/dist/cjs/keypairs/passkey/publickey.js.map +7 -0
- package/dist/cjs/verify/verify.js +12 -9
- package/dist/cjs/verify/verify.js.map +2 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/version.js.map +1 -1
- package/dist/esm/bcs/bcs.d.ts +13 -0
- package/dist/esm/bcs/bcs.js +6 -0
- package/dist/esm/bcs/bcs.js.map +2 -2
- package/dist/esm/bcs/index.d.ts +13 -0
- package/dist/esm/bcs/index.js +3 -1
- package/dist/esm/bcs/index.js.map +2 -2
- package/dist/esm/cryptography/signature-scheme.d.ts +3 -1
- package/dist/esm/cryptography/signature-scheme.js +4 -2
- package/dist/esm/cryptography/signature-scheme.js.map +2 -2
- package/dist/esm/cryptography/signature.d.ts +8 -0
- package/dist/esm/cryptography/signature.js +3 -0
- package/dist/esm/cryptography/signature.js.map +2 -2
- package/dist/esm/keypairs/passkey/index.d.ts +3 -0
- package/dist/esm/keypairs/passkey/index.js +8 -0
- package/dist/esm/keypairs/passkey/index.js.map +7 -0
- package/dist/esm/keypairs/passkey/keypair.d.ts +59 -0
- package/dist/esm/keypairs/passkey/keypair.js +153 -0
- package/dist/esm/keypairs/passkey/keypair.js.map +7 -0
- package/dist/esm/keypairs/passkey/publickey.d.ts +72 -0
- package/dist/esm/keypairs/passkey/publickey.js +148 -0
- package/dist/esm/keypairs/passkey/publickey.js.map +7 -0
- package/dist/esm/verify/verify.js +3 -0
- package/dist/esm/verify/verify.js.map +2 -2
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/keypairs/passkey/package.json +6 -0
- package/package.json +6 -1
- package/src/bcs/bcs.ts +6 -0
- package/src/bcs/index.ts +2 -0
- package/src/cryptography/signature-scheme.ts +9 -1
- package/src/cryptography/signature.ts +3 -0
- package/src/keypairs/passkey/index.ts +5 -0
- package/src/keypairs/passkey/keypair.ts +209 -0
- package/src/keypairs/passkey/publickey.ts +188 -0
- package/src/verify/verify.ts +3 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"author": "Mysten Labs <build@mystenlabs.com>",
|
|
4
4
|
"description": "Sui TypeScript API(Work in Progress)",
|
|
5
5
|
"homepage": "https://sdk.mystenlabs.com",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.17.0",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"files": [
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"import": "./dist/esm/keypairs/secp256r1/index.js",
|
|
63
63
|
"require": "./dist/cjs/keypairs/secp256r1/index.js"
|
|
64
64
|
},
|
|
65
|
+
"./keypairs/passkey": {
|
|
66
|
+
"import": "./dist/esm/keypairs/passkey/index.js",
|
|
67
|
+
"require": "./dist/cjs/keypairs/passkey/index.js"
|
|
68
|
+
},
|
|
65
69
|
"./multisig": {
|
|
66
70
|
"import": "./dist/esm/multisig/index.js",
|
|
67
71
|
"require": "./dist/cjs/multisig/index.js"
|
|
@@ -126,6 +130,7 @@
|
|
|
126
130
|
"@noble/hashes": "^1.4.0",
|
|
127
131
|
"@scure/bip32": "^1.4.0",
|
|
128
132
|
"@scure/bip39": "^1.3.0",
|
|
133
|
+
"@simplewebauthn/typescript-types": "^7.4.0",
|
|
129
134
|
"@suchipi/femver": "^1.0.0",
|
|
130
135
|
"bech32": "^2.0.0",
|
|
131
136
|
"gql.tada": "^1.8.2",
|
package/src/bcs/bcs.ts
CHANGED
|
@@ -298,3 +298,9 @@ export const SenderSignedTransaction = bcs.struct('SenderSignedTransaction', {
|
|
|
298
298
|
export const SenderSignedData = bcs.vector(SenderSignedTransaction, {
|
|
299
299
|
name: 'SenderSignedData',
|
|
300
300
|
});
|
|
301
|
+
|
|
302
|
+
export const PasskeyAuthenticator = bcs.struct('PasskeyAuthenticator', {
|
|
303
|
+
authenticatorData: bcs.vector(bcs.u8()),
|
|
304
|
+
clientDataJson: bcs.string(),
|
|
305
|
+
userSignature: bcs.vector(bcs.u8()),
|
|
306
|
+
});
|
package/src/bcs/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
MultiSigPublicKey,
|
|
21
21
|
ObjectArg,
|
|
22
22
|
ObjectDigest,
|
|
23
|
+
PasskeyAuthenticator,
|
|
23
24
|
ProgrammableMoveCall,
|
|
24
25
|
ProgrammableTransaction,
|
|
25
26
|
PublicKey,
|
|
@@ -82,6 +83,7 @@ const suiBcs = {
|
|
|
82
83
|
TransactionKind,
|
|
83
84
|
TypeTag,
|
|
84
85
|
TransactionEffects,
|
|
86
|
+
PasskeyAuthenticator,
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
export { suiBcs as bcs };
|
|
@@ -7,6 +7,7 @@ export const SIGNATURE_SCHEME_TO_FLAG = {
|
|
|
7
7
|
Secp256r1: 0x02,
|
|
8
8
|
MultiSig: 0x03,
|
|
9
9
|
ZkLogin: 0x05,
|
|
10
|
+
Passkey: 0x06,
|
|
10
11
|
} as const;
|
|
11
12
|
|
|
12
13
|
export const SIGNATURE_SCHEME_TO_SIZE = {
|
|
@@ -21,8 +22,15 @@ export const SIGNATURE_FLAG_TO_SCHEME = {
|
|
|
21
22
|
0x02: 'Secp256r1',
|
|
22
23
|
0x03: 'MultiSig',
|
|
23
24
|
0x05: 'ZkLogin',
|
|
25
|
+
0x06: 'Passkey',
|
|
24
26
|
} as const;
|
|
25
27
|
|
|
26
|
-
export type SignatureScheme =
|
|
28
|
+
export type SignatureScheme =
|
|
29
|
+
| 'ED25519'
|
|
30
|
+
| 'Secp256k1'
|
|
31
|
+
| 'Secp256r1'
|
|
32
|
+
| 'MultiSig'
|
|
33
|
+
| 'ZkLogin'
|
|
34
|
+
| 'Passkey';
|
|
27
35
|
|
|
28
36
|
export type SignatureFlag = keyof typeof SIGNATURE_FLAG_TO_SCHEME;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { fromBase64, toBase64 } from '@mysten/bcs';
|
|
5
5
|
|
|
6
6
|
import { bcs } from '../bcs/index.js';
|
|
7
|
+
import { parseSerializedPasskeySignature } from '../keypairs/passkey/publickey.js';
|
|
7
8
|
import type { MultiSigStruct } from '../multisig/publickey.js';
|
|
8
9
|
import { parseSerializedZkLoginSignature } from '../zklogin/publickey.js';
|
|
9
10
|
import type { PublicKey } from './publickey.js';
|
|
@@ -55,6 +56,8 @@ export function parseSerializedSignature(serializedSignature: string) {
|
|
|
55
56
|
SIGNATURE_FLAG_TO_SCHEME[bytes[0] as keyof typeof SIGNATURE_FLAG_TO_SCHEME];
|
|
56
57
|
|
|
57
58
|
switch (signatureScheme) {
|
|
59
|
+
case 'Passkey':
|
|
60
|
+
return parseSerializedPasskeySignature(serializedSignature);
|
|
58
61
|
case 'MultiSig':
|
|
59
62
|
const multisig: MultiSigStruct = bcs.MultiSig.parse(bytes.slice(1));
|
|
60
63
|
return {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
export { PasskeyKeypair, BrowserPasskeyProvider } from './keypair.js';
|
|
4
|
+
export type { PasskeyProvider, BrowserPasswordProviderOptions } from './keypair.js';
|
|
5
|
+
export { PasskeyPublicKey } from './publickey.js';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { toBase64 } from '@mysten/bcs';
|
|
5
|
+
import { secp256r1 } from '@noble/curves/p256';
|
|
6
|
+
import { blake2b } from '@noble/hashes/blake2b';
|
|
7
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
8
|
+
import type {
|
|
9
|
+
AuthenticationCredential,
|
|
10
|
+
RegistrationCredential,
|
|
11
|
+
} from '@simplewebauthn/typescript-types';
|
|
12
|
+
|
|
13
|
+
import { PasskeyAuthenticator } from '../../bcs/bcs.js';
|
|
14
|
+
import type { IntentScope, SignatureWithBytes } from '../../cryptography/index.js';
|
|
15
|
+
import { messageWithIntent, SIGNATURE_SCHEME_TO_FLAG, Signer } from '../../cryptography/index.js';
|
|
16
|
+
import type { PublicKey } from '../../cryptography/publickey.js';
|
|
17
|
+
import type { SignatureScheme } from '../../cryptography/signature-scheme.js';
|
|
18
|
+
import {
|
|
19
|
+
parseDerSPKI,
|
|
20
|
+
PASSKEY_PUBLIC_KEY_SIZE,
|
|
21
|
+
PASSKEY_SIGNATURE_SIZE,
|
|
22
|
+
PasskeyPublicKey,
|
|
23
|
+
} from './publickey.js';
|
|
24
|
+
|
|
25
|
+
type DeepPartialConfigKeys = 'rp' | 'user' | 'authenticatorSelection';
|
|
26
|
+
|
|
27
|
+
type DeepPartial<T> = T extends object
|
|
28
|
+
? {
|
|
29
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
30
|
+
}
|
|
31
|
+
: T;
|
|
32
|
+
|
|
33
|
+
export type BrowserPasswordProviderOptions = Pick<
|
|
34
|
+
DeepPartial<PublicKeyCredentialCreationOptions>,
|
|
35
|
+
DeepPartialConfigKeys
|
|
36
|
+
> &
|
|
37
|
+
Omit<
|
|
38
|
+
Partial<PublicKeyCredentialCreationOptions>,
|
|
39
|
+
DeepPartialConfigKeys | 'pubKeyCredParams' | 'challenge'
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
export interface PasskeyProvider {
|
|
43
|
+
create(): Promise<RegistrationCredential>;
|
|
44
|
+
get(challenge: Uint8Array): Promise<AuthenticationCredential>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default browser implementation
|
|
48
|
+
export class BrowserPasskeyProvider implements PasskeyProvider {
|
|
49
|
+
#name: string;
|
|
50
|
+
#options: BrowserPasswordProviderOptions;
|
|
51
|
+
|
|
52
|
+
constructor(name: string, options: BrowserPasswordProviderOptions) {
|
|
53
|
+
this.#name = name;
|
|
54
|
+
this.#options = options;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async create(): Promise<RegistrationCredential> {
|
|
58
|
+
return (await navigator.credentials.create({
|
|
59
|
+
publicKey: {
|
|
60
|
+
timeout: this.#options.timeout ?? 60000,
|
|
61
|
+
...this.#options,
|
|
62
|
+
rp: {
|
|
63
|
+
name: this.#name,
|
|
64
|
+
...this.#options.rp,
|
|
65
|
+
},
|
|
66
|
+
user: {
|
|
67
|
+
name: this.#name,
|
|
68
|
+
displayName: this.#name,
|
|
69
|
+
...this.#options.user,
|
|
70
|
+
id: randomBytes(10),
|
|
71
|
+
},
|
|
72
|
+
challenge: new TextEncoder().encode('Create passkey wallet on Sui'),
|
|
73
|
+
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
|
|
74
|
+
authenticatorSelection: {
|
|
75
|
+
authenticatorAttachment: 'cross-platform',
|
|
76
|
+
residentKey: 'required',
|
|
77
|
+
requireResidentKey: true,
|
|
78
|
+
userVerification: 'required',
|
|
79
|
+
...this.#options.authenticatorSelection,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
})) as RegistrationCredential;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async get(challenge: Uint8Array): Promise<AuthenticationCredential> {
|
|
86
|
+
return (await navigator.credentials.get({
|
|
87
|
+
publicKey: {
|
|
88
|
+
challenge,
|
|
89
|
+
userVerification: this.#options.authenticatorSelection?.userVerification || 'required',
|
|
90
|
+
timeout: this.#options.timeout ?? 60000,
|
|
91
|
+
},
|
|
92
|
+
})) as AuthenticationCredential;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @experimental
|
|
98
|
+
* A passkey signer used for signing transactions. This is a client side implementation for [SIP-9](https://github.com/sui-foundation/sips/blob/main/sips/sip-9.md).
|
|
99
|
+
*/
|
|
100
|
+
export class PasskeyKeypair extends Signer {
|
|
101
|
+
private publicKey: Uint8Array;
|
|
102
|
+
private provider: PasskeyProvider;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the key scheme of passkey,
|
|
106
|
+
*/
|
|
107
|
+
getKeyScheme(): SignatureScheme {
|
|
108
|
+
return 'Passkey';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates an instance of Passkey signer. It's expected to call the static `getPasskeyInstance` method to create an instance.
|
|
113
|
+
* For example:
|
|
114
|
+
* ```
|
|
115
|
+
* const signer = await PasskeyKeypair.getPasskeyInstance();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
constructor(publicKey: Uint8Array, provider: PasskeyProvider) {
|
|
119
|
+
super();
|
|
120
|
+
this.publicKey = publicKey;
|
|
121
|
+
this.provider = provider;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates an instance of Passkey signer invoking the passkey from navigator.
|
|
126
|
+
*/
|
|
127
|
+
static async getPasskeyInstance(provider: PasskeyProvider): Promise<PasskeyKeypair> {
|
|
128
|
+
// create a passkey secp256r1 with the provider.
|
|
129
|
+
const credential = await provider.create();
|
|
130
|
+
|
|
131
|
+
if (!credential.response.getPublicKey()) {
|
|
132
|
+
throw new Error('Invalid credential create response');
|
|
133
|
+
} else {
|
|
134
|
+
const derSPKI = credential.response.getPublicKey()!;
|
|
135
|
+
const pubkeyUncompressed = parseDerSPKI(new Uint8Array(derSPKI));
|
|
136
|
+
const pubkey = secp256r1.ProjectivePoint.fromHex(pubkeyUncompressed);
|
|
137
|
+
const pubkeyCompressed = pubkey.toRawBytes(true);
|
|
138
|
+
return new PasskeyKeypair(pubkeyCompressed, provider);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Return the public key for this passkey.
|
|
144
|
+
*/
|
|
145
|
+
getPublicKey(): PublicKey {
|
|
146
|
+
return new PasskeyPublicKey(this.publicKey);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Return the signature for the provided data (i.e. blake2b(intent_message)).
|
|
151
|
+
* This is sent to passkey as the challenge field.
|
|
152
|
+
*/
|
|
153
|
+
async sign(data: Uint8Array) {
|
|
154
|
+
// sendss the passkey to sign over challenge as the data.
|
|
155
|
+
const credential = await this.provider.get(data);
|
|
156
|
+
|
|
157
|
+
// parse authenticatorData (as bytes), clientDataJSON (decoded as string).
|
|
158
|
+
const authenticatorData = new Uint8Array(credential.response.authenticatorData);
|
|
159
|
+
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON); // response.clientDataJSON is already UTF-8 encoded JSON
|
|
160
|
+
const decoder = new TextDecoder();
|
|
161
|
+
const clientDataJSONString: string = decoder.decode(clientDataJSON);
|
|
162
|
+
|
|
163
|
+
// parse the signature from DER format, normalize and convert to compressed format (33 bytes).
|
|
164
|
+
const sig = secp256r1.Signature.fromDER(new Uint8Array(credential.response.signature));
|
|
165
|
+
const normalized = sig.normalizeS().toCompactRawBytes();
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
normalized.length !== PASSKEY_SIGNATURE_SIZE ||
|
|
169
|
+
this.publicKey.length !== PASSKEY_PUBLIC_KEY_SIZE
|
|
170
|
+
) {
|
|
171
|
+
throw new Error('Invalid signature or public key length');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// construct userSignature as flag || sig || pubkey for the secp256r1 signature.
|
|
175
|
+
const arr = new Uint8Array(1 + normalized.length + this.publicKey.length);
|
|
176
|
+
arr.set([SIGNATURE_SCHEME_TO_FLAG['Secp256r1']]);
|
|
177
|
+
arr.set(normalized, 1);
|
|
178
|
+
arr.set(this.publicKey, 1 + normalized.length);
|
|
179
|
+
|
|
180
|
+
// serialize all fields into a passkey signature according to https://github.com/sui-foundation/sips/blob/main/sips/sip-9.md#signature-encoding
|
|
181
|
+
return PasskeyAuthenticator.serialize({
|
|
182
|
+
authenticatorData: authenticatorData,
|
|
183
|
+
clientDataJson: clientDataJSONString,
|
|
184
|
+
userSignature: arr,
|
|
185
|
+
}).toBytes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* This overrides the base class implementation that accepts the raw bytes and signs its
|
|
190
|
+
* digest of the intent message, then serialize it with the passkey flag.
|
|
191
|
+
*/
|
|
192
|
+
async signWithIntent(bytes: Uint8Array, intent: IntentScope): Promise<SignatureWithBytes> {
|
|
193
|
+
// prepend it into an intent message and computes the digest.
|
|
194
|
+
const intentMessage = messageWithIntent(intent, bytes);
|
|
195
|
+
const digest = blake2b(intentMessage, { dkLen: 32 });
|
|
196
|
+
|
|
197
|
+
// sign the digest.
|
|
198
|
+
const signature = await this.sign(digest);
|
|
199
|
+
|
|
200
|
+
// prepend with the passkey flag.
|
|
201
|
+
const serializedSignature = new Uint8Array(1 + signature.length);
|
|
202
|
+
serializedSignature.set([SIGNATURE_SCHEME_TO_FLAG[this.getKeyScheme()]]);
|
|
203
|
+
serializedSignature.set(signature, 1);
|
|
204
|
+
return {
|
|
205
|
+
signature: toBase64(serializedSignature),
|
|
206
|
+
bytes: toBase64(bytes),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { fromBase64, toBase64 } from '@mysten/bcs';
|
|
5
|
+
import { secp256r1 } from '@noble/curves/p256';
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
7
|
+
|
|
8
|
+
import { PasskeyAuthenticator } from '../../bcs/bcs.js';
|
|
9
|
+
import { bytesEqual, PublicKey } from '../../cryptography/publickey.js';
|
|
10
|
+
import type { PublicKeyInitData } from '../../cryptography/publickey.js';
|
|
11
|
+
import { SIGNATURE_SCHEME_TO_FLAG } from '../../cryptography/signature-scheme.js';
|
|
12
|
+
|
|
13
|
+
export const PASSKEY_PUBLIC_KEY_SIZE = 33;
|
|
14
|
+
export const PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE = 65;
|
|
15
|
+
export const PASSKEY_SIGNATURE_SIZE = 64;
|
|
16
|
+
/** Fixed DER header for secp256r1 SubjectPublicKeyInfo
|
|
17
|
+
DER structure for P-256 SPKI:
|
|
18
|
+
30 -- SEQUENCE
|
|
19
|
+
59 -- length (89 bytes)
|
|
20
|
+
30 -- SEQUENCE
|
|
21
|
+
13 -- length (19 bytes)
|
|
22
|
+
06 -- OBJECT IDENTIFIER
|
|
23
|
+
07 -- length
|
|
24
|
+
2A 86 48 CE 3D 02 01 -- id-ecPublicKey
|
|
25
|
+
06 -- OBJECT IDENTIFIER
|
|
26
|
+
08 -- length
|
|
27
|
+
2A 86 48 CE 3D 03 01 07 -- secp256r1/prime256v1
|
|
28
|
+
03 -- BIT STRING
|
|
29
|
+
42 -- length (66 bytes)
|
|
30
|
+
00 -- padding
|
|
31
|
+
===== above bytes are considered header =====
|
|
32
|
+
04 || x || y -- uncompressed point (65 bytes: 0x04 || 32-byte x || 32-byte y)
|
|
33
|
+
*/
|
|
34
|
+
export const SECP256R1_SPKI_HEADER = new Uint8Array([
|
|
35
|
+
0x30,
|
|
36
|
+
0x59, // SEQUENCE, length 89
|
|
37
|
+
0x30,
|
|
38
|
+
0x13, // SEQUENCE, length 19
|
|
39
|
+
0x06,
|
|
40
|
+
0x07, // OID, length 7
|
|
41
|
+
0x2a,
|
|
42
|
+
0x86,
|
|
43
|
+
0x48,
|
|
44
|
+
0xce,
|
|
45
|
+
0x3d,
|
|
46
|
+
0x02,
|
|
47
|
+
0x01, // OID: 1.2.840.10045.2.1 (ecPublicKey)
|
|
48
|
+
0x06,
|
|
49
|
+
0x08, // OID, length 8
|
|
50
|
+
0x2a,
|
|
51
|
+
0x86,
|
|
52
|
+
0x48,
|
|
53
|
+
0xce,
|
|
54
|
+
0x3d,
|
|
55
|
+
0x03,
|
|
56
|
+
0x01,
|
|
57
|
+
0x07, // OID: 1.2.840.10045.3.1.7 (prime256v1/secp256r1)
|
|
58
|
+
0x03,
|
|
59
|
+
0x42, // BIT STRING, length 66
|
|
60
|
+
0x00, // no unused bits
|
|
61
|
+
] as const);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A passkey public key
|
|
65
|
+
*/
|
|
66
|
+
export class PasskeyPublicKey extends PublicKey {
|
|
67
|
+
static SIZE = PASSKEY_PUBLIC_KEY_SIZE;
|
|
68
|
+
private data: Uint8Array;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new PasskeyPublicKey object
|
|
72
|
+
* @param value passkey public key as buffer or base-64 encoded string
|
|
73
|
+
*/
|
|
74
|
+
constructor(value: PublicKeyInitData) {
|
|
75
|
+
super();
|
|
76
|
+
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
this.data = fromBase64(value);
|
|
79
|
+
} else if (value instanceof Uint8Array) {
|
|
80
|
+
this.data = value;
|
|
81
|
+
} else {
|
|
82
|
+
this.data = Uint8Array.from(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.data.length !== PASSKEY_PUBLIC_KEY_SIZE) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid public key input. Expected ${PASSKEY_PUBLIC_KEY_SIZE} bytes, got ${this.data.length}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks if two passkey public keys are equal
|
|
94
|
+
*/
|
|
95
|
+
override equals(publicKey: PasskeyPublicKey): boolean {
|
|
96
|
+
return super.equals(publicKey);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return the byte array representation of the Secp256r1 public key
|
|
101
|
+
*/
|
|
102
|
+
toRawBytes(): Uint8Array {
|
|
103
|
+
return this.data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return the Sui address associated with this Secp256r1 public key
|
|
108
|
+
*/
|
|
109
|
+
flag(): number {
|
|
110
|
+
return SIGNATURE_SCHEME_TO_FLAG['Passkey'];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Verifies that the signature is valid for for the provided message
|
|
115
|
+
*/
|
|
116
|
+
async verify(message: Uint8Array, signature: Uint8Array | string): Promise<boolean> {
|
|
117
|
+
const parsed = parseSerializedPasskeySignature(signature);
|
|
118
|
+
const clientDataJSON = JSON.parse(parsed.clientDataJson);
|
|
119
|
+
|
|
120
|
+
if (clientDataJSON.type !== 'webauthn.get') {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// parse challenge from base64 url
|
|
125
|
+
const parsedChallenge = fromBase64(
|
|
126
|
+
clientDataJSON.challenge.replace(/-/g, '+').replace(/_/g, '/'),
|
|
127
|
+
);
|
|
128
|
+
if (!bytesEqual(message, parsedChallenge)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const pk = parsed.userSignature.slice(1 + PASSKEY_SIGNATURE_SIZE);
|
|
133
|
+
if (!bytesEqual(this.toRawBytes(), pk)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const payload = new Uint8Array([...parsed.authenticatorData, ...sha256(parsed.clientDataJson)]);
|
|
138
|
+
const sig = parsed.userSignature.slice(1, PASSKEY_SIGNATURE_SIZE + 1);
|
|
139
|
+
return secp256r1.verify(sig, sha256(payload), pk);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parses a DER SubjectPublicKeyInfo into an uncompressed public key. This also verifies
|
|
145
|
+
* that the curve used is P-256 (secp256r1).
|
|
146
|
+
*
|
|
147
|
+
* @param data: DER SubjectPublicKeyInfo
|
|
148
|
+
* @returns uncompressed public key (`0x04 || x || y`)
|
|
149
|
+
*/
|
|
150
|
+
export function parseDerSPKI(derBytes: Uint8Array): Uint8Array {
|
|
151
|
+
// Verify length and header bytes are expected
|
|
152
|
+
if (derBytes.length !== SECP256R1_SPKI_HEADER.length + PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE) {
|
|
153
|
+
throw new Error('Invalid DER length');
|
|
154
|
+
}
|
|
155
|
+
for (let i = 0; i < SECP256R1_SPKI_HEADER.length; i++) {
|
|
156
|
+
if (derBytes[i] !== SECP256R1_SPKI_HEADER[i]) {
|
|
157
|
+
throw new Error('Invalid spki header');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (derBytes[SECP256R1_SPKI_HEADER.length] !== 0x04) {
|
|
162
|
+
throw new Error('Invalid point marker');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Returns the last 65 bytes `04 || x || y`
|
|
166
|
+
return derBytes.slice(SECP256R1_SPKI_HEADER.length);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse signature from bytes or base64 string into the following fields.
|
|
171
|
+
*/
|
|
172
|
+
export function parseSerializedPasskeySignature(signature: Uint8Array | string) {
|
|
173
|
+
const bytes = typeof signature === 'string' ? fromBase64(signature) : signature;
|
|
174
|
+
|
|
175
|
+
if (bytes[0] !== SIGNATURE_SCHEME_TO_FLAG.Passkey) {
|
|
176
|
+
throw new Error('Invalid signature scheme');
|
|
177
|
+
}
|
|
178
|
+
const dec = PasskeyAuthenticator.parse(bytes.slice(1));
|
|
179
|
+
return {
|
|
180
|
+
signatureScheme: 'Passkey' as const,
|
|
181
|
+
serializedSignature: toBase64(bytes),
|
|
182
|
+
signature: bytes,
|
|
183
|
+
authenticatorData: dec.authenticatorData,
|
|
184
|
+
clientDataJson: dec.clientDataJson,
|
|
185
|
+
userSignature: new Uint8Array(dec.userSignature),
|
|
186
|
+
publicKey: new Uint8Array(dec.userSignature.slice(1 + PASSKEY_SIGNATURE_SIZE)),
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/verify/verify.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PublicKey, SignatureFlag, SignatureScheme } from '../cryptography/
|
|
|
7
7
|
import { parseSerializedSignature, SIGNATURE_FLAG_TO_SCHEME } from '../cryptography/index.js';
|
|
8
8
|
import type { SuiGraphQLClient } from '../graphql/client.js';
|
|
9
9
|
import { Ed25519PublicKey } from '../keypairs/ed25519/publickey.js';
|
|
10
|
+
import { PasskeyPublicKey } from '../keypairs/passkey/publickey.js';
|
|
10
11
|
import { Secp256k1PublicKey } from '../keypairs/secp256k1/publickey.js';
|
|
11
12
|
import { Secp256r1PublicKey } from '../keypairs/secp256r1/publickey.js';
|
|
12
13
|
// eslint-disable-next-line import/no-cycle
|
|
@@ -98,6 +99,8 @@ export function publicKeyFromRawBytes(
|
|
|
98
99
|
return new MultiSigPublicKey(bytes);
|
|
99
100
|
case 'ZkLogin':
|
|
100
101
|
return new ZkLoginPublicIdentifier(bytes, options);
|
|
102
|
+
case 'Passkey':
|
|
103
|
+
return new PasskeyPublicKey(bytes);
|
|
101
104
|
default:
|
|
102
105
|
throw new Error(`Unsupported signature scheme ${signatureScheme}`);
|
|
103
106
|
}
|
package/src/version.ts
CHANGED