@ember-finance/sdk 2.0.6 → 2.0.7
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/dist/src/evm-vaults/index.d.ts +1 -0
- package/dist/src/evm-vaults/index.js +1 -0
- package/dist/src/evm-vaults/signers/evm-kms-signer.d.ts +42 -0
- package/dist/src/evm-vaults/signers/evm-kms-signer.js +143 -0
- package/dist/src/evm-vaults/signers/index.d.ts +1 -0
- package/dist/src/evm-vaults/signers/index.js +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { KMSClient } from "@aws-sdk/client-kms";
|
|
2
|
+
import { AbstractSigner, Provider, TransactionRequest, TypedDataDomain, TypedDataField } from "ethers";
|
|
3
|
+
export interface EvmKmsSignerOptions {
|
|
4
|
+
/** KMS key ID, ARN, or alias (e.g. "alias/ember-vaults-ops-evm-manager-wallet"). */
|
|
5
|
+
keyId: string;
|
|
6
|
+
/** AWS region. Defaults to AWS_REGION env or the SDK default. */
|
|
7
|
+
region?: string;
|
|
8
|
+
/** Pre-built KMSClient. If supplied, `region` is ignored. */
|
|
9
|
+
kmsClient?: KMSClient;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* EvmKmsSigner is an ethers v6 Signer backed by an AWS KMS asymmetric key
|
|
13
|
+
* with usage `SIGN_VERIFY` and key spec `ECC_SECG_P256K1` (secp256k1). The
|
|
14
|
+
* private material never leaves KMS; every signing operation is one
|
|
15
|
+
* `kms:Sign` call.
|
|
16
|
+
*
|
|
17
|
+
* Caches the public key after first use so subsequent signs do not re-hit
|
|
18
|
+
* KMS for the address.
|
|
19
|
+
*
|
|
20
|
+
* Compatible with ethers v6. Pair with a Provider via `connect(provider)`
|
|
21
|
+
* before submitting transactions.
|
|
22
|
+
*/
|
|
23
|
+
export declare class EvmKmsSigner extends AbstractSigner {
|
|
24
|
+
private readonly kms;
|
|
25
|
+
private readonly keyId;
|
|
26
|
+
private addressCache;
|
|
27
|
+
private uncompressedPubKeyCache;
|
|
28
|
+
constructor(opts: EvmKmsSignerOptions, provider?: Provider | null);
|
|
29
|
+
connect(provider: Provider | null): EvmKmsSigner;
|
|
30
|
+
getAddress(): Promise<string>;
|
|
31
|
+
signMessage(message: string | Uint8Array): Promise<string>;
|
|
32
|
+
signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, unknown>): Promise<string>;
|
|
33
|
+
signTransaction(tx: TransactionRequest): Promise<string>;
|
|
34
|
+
/** Returns a 65-byte compact signature as a 0x-prefixed hex string. */
|
|
35
|
+
private signDigest;
|
|
36
|
+
/**
|
|
37
|
+
* Sends the digest to KMS, decodes the DER signature, normalises s,
|
|
38
|
+
* and recovers the y-parity by matching against our address.
|
|
39
|
+
*/
|
|
40
|
+
private signDigestRaw;
|
|
41
|
+
private getUncompressedPublicKey;
|
|
42
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { GetPublicKeyCommand, KMSClient, MessageType, SignCommand, SigningAlgorithmSpec } from "@aws-sdk/client-kms";
|
|
2
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
3
|
+
import asn1 from "asn1.js";
|
|
4
|
+
import { AbstractSigner, computeAddress, getBytes, hashMessage, resolveAddress, Signature, SigningKey, Transaction, TypedDataEncoder } from "ethers";
|
|
5
|
+
// ASN.1 schemas for the DER blobs AWS KMS returns.
|
|
6
|
+
//
|
|
7
|
+
// GetPublicKey returns SubjectPublicKeyInfo (RFC 5480 §2):
|
|
8
|
+
// SEQUENCE { algo: AlgorithmIdentifier, pubKey: BIT STRING }
|
|
9
|
+
// The bit string contains the SEC1 uncompressed point (0x04 || X || Y).
|
|
10
|
+
//
|
|
11
|
+
// Sign returns DER ECDSA signature (RFC 3279 §2.2.3):
|
|
12
|
+
// SEQUENCE { r: INTEGER, s: INTEGER }
|
|
13
|
+
const EcdsaPubKeyAsn = asn1.define("EcdsaPubKey", function () {
|
|
14
|
+
this.seq().obj(this.key("algo")
|
|
15
|
+
.seq()
|
|
16
|
+
.obj(this.key("algorithm").objid(), this.key("parameters").objid()), this.key("pubKey").bitstr());
|
|
17
|
+
});
|
|
18
|
+
const EcdsaSigAsn = asn1.define("EcdsaSig", function () {
|
|
19
|
+
this.seq().obj(this.key("r").int(), this.key("s").int());
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* EvmKmsSigner is an ethers v6 Signer backed by an AWS KMS asymmetric key
|
|
23
|
+
* with usage `SIGN_VERIFY` and key spec `ECC_SECG_P256K1` (secp256k1). The
|
|
24
|
+
* private material never leaves KMS; every signing operation is one
|
|
25
|
+
* `kms:Sign` call.
|
|
26
|
+
*
|
|
27
|
+
* Caches the public key after first use so subsequent signs do not re-hit
|
|
28
|
+
* KMS for the address.
|
|
29
|
+
*
|
|
30
|
+
* Compatible with ethers v6. Pair with a Provider via `connect(provider)`
|
|
31
|
+
* before submitting transactions.
|
|
32
|
+
*/
|
|
33
|
+
export class EvmKmsSigner extends AbstractSigner {
|
|
34
|
+
constructor(opts, provider = null) {
|
|
35
|
+
super(provider);
|
|
36
|
+
this.keyId = opts.keyId;
|
|
37
|
+
this.kms = opts.kmsClient ?? new KMSClient({ region: opts.region });
|
|
38
|
+
}
|
|
39
|
+
connect(provider) {
|
|
40
|
+
const next = new EvmKmsSigner({ keyId: this.keyId, kmsClient: this.kms }, provider);
|
|
41
|
+
// Carry caches over so reconnecting doesn't force a re-fetch.
|
|
42
|
+
next.uncompressedPubKeyCache = this.uncompressedPubKeyCache;
|
|
43
|
+
next.addressCache = this.addressCache;
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
async getAddress() {
|
|
47
|
+
if (this.addressCache)
|
|
48
|
+
return this.addressCache;
|
|
49
|
+
const pub = await this.getUncompressedPublicKey();
|
|
50
|
+
// ethers' computeAddress accepts a 0x04-prefixed uncompressed key
|
|
51
|
+
// and returns a checksummed address.
|
|
52
|
+
this.addressCache = computeAddress("0x" + Buffer.from(pub).toString("hex"));
|
|
53
|
+
return this.addressCache;
|
|
54
|
+
}
|
|
55
|
+
async signMessage(message) {
|
|
56
|
+
return this.signDigest(hashMessage(message));
|
|
57
|
+
}
|
|
58
|
+
async signTypedData(domain, types, value) {
|
|
59
|
+
// Resolve any ENS-style names that EIP-712 allows in the domain/types.
|
|
60
|
+
const populated = await TypedDataEncoder.resolveNames(domain, types, value, async (name) => {
|
|
61
|
+
if (!this.provider) {
|
|
62
|
+
throw new Error("EvmKmsSigner.signTypedData: name resolution requires a provider");
|
|
63
|
+
}
|
|
64
|
+
const addr = await this.provider.resolveName(name);
|
|
65
|
+
if (!addr)
|
|
66
|
+
throw new Error(`unable to resolve name: ${name}`);
|
|
67
|
+
return addr;
|
|
68
|
+
});
|
|
69
|
+
return this.signDigest(TypedDataEncoder.hash(populated.domain, types, populated.value));
|
|
70
|
+
}
|
|
71
|
+
async signTransaction(tx) {
|
|
72
|
+
// ethers requires `from` to be absent on a serialized signed tx —
|
|
73
|
+
// it gets recovered from the signature. Validate it matches us.
|
|
74
|
+
if (tx.from != null) {
|
|
75
|
+
const expected = await this.getAddress();
|
|
76
|
+
const from = await resolveAddress(tx.from, this.provider);
|
|
77
|
+
if (from.toLowerCase() !== expected.toLowerCase()) {
|
|
78
|
+
throw new Error(`EvmKmsSigner.signTransaction: from=${from} does not match signer=${expected}`);
|
|
79
|
+
}
|
|
80
|
+
tx = { ...tx, from: undefined };
|
|
81
|
+
}
|
|
82
|
+
// ethers' TransactionRequest and TransactionLike overlap structurally
|
|
83
|
+
// but aren't unifiable in the type system; Transaction.from accepts
|
|
84
|
+
// the looser shape at runtime.
|
|
85
|
+
const txObj = Transaction.from(tx);
|
|
86
|
+
const sig = await this.signDigestRaw(txObj.unsignedHash);
|
|
87
|
+
txObj.signature = sig;
|
|
88
|
+
return txObj.serialized;
|
|
89
|
+
}
|
|
90
|
+
/** Returns a 65-byte compact signature as a 0x-prefixed hex string. */
|
|
91
|
+
async signDigest(digest) {
|
|
92
|
+
const sig = await this.signDigestRaw(digest);
|
|
93
|
+
return sig.serialized;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Sends the digest to KMS, decodes the DER signature, normalises s,
|
|
97
|
+
* and recovers the y-parity by matching against our address.
|
|
98
|
+
*/
|
|
99
|
+
async signDigestRaw(digest) {
|
|
100
|
+
const digestBytes = getBytes(digest);
|
|
101
|
+
const res = await this.kms.send(new SignCommand({
|
|
102
|
+
KeyId: this.keyId,
|
|
103
|
+
Message: digestBytes,
|
|
104
|
+
MessageType: MessageType.DIGEST,
|
|
105
|
+
SigningAlgorithm: SigningAlgorithmSpec.ECDSA_SHA_256
|
|
106
|
+
}));
|
|
107
|
+
if (!res.Signature) {
|
|
108
|
+
throw new Error("EvmKmsSigner: KMS Sign returned no Signature");
|
|
109
|
+
}
|
|
110
|
+
const decoded = EcdsaSigAsn.decode(Buffer.from(res.Signature), "der");
|
|
111
|
+
const r = BigInt("0x" + decoded.r.toString(16).padStart(64, "0"));
|
|
112
|
+
let s = BigInt("0x" + decoded.s.toString(16).padStart(64, "0"));
|
|
113
|
+
// EIP-2: only accept low-s signatures. KMS happily returns either side.
|
|
114
|
+
const N = secp256k1.Point.Fn.ORDER;
|
|
115
|
+
if (s > N / 2n)
|
|
116
|
+
s = N - s;
|
|
117
|
+
const rHex = "0x" + r.toString(16).padStart(64, "0");
|
|
118
|
+
const sHex = "0x" + s.toString(16).padStart(64, "0");
|
|
119
|
+
// Recover the right y-parity by trying both candidates and matching
|
|
120
|
+
// against the address we know belongs to this KMS key.
|
|
121
|
+
const expectedAddr = (await this.getAddress()).toLowerCase();
|
|
122
|
+
for (const yParity of [0, 1]) {
|
|
123
|
+
const candidate = Signature.from({ r: rHex, s: sHex, yParity });
|
|
124
|
+
const recoveredPubKey = SigningKey.recoverPublicKey(digestBytes, candidate);
|
|
125
|
+
if (computeAddress(recoveredPubKey).toLowerCase() === expectedAddr) {
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error("EvmKmsSigner: recovered address does not match KMS key — signature integrity failure");
|
|
130
|
+
}
|
|
131
|
+
async getUncompressedPublicKey() {
|
|
132
|
+
if (this.uncompressedPubKeyCache)
|
|
133
|
+
return this.uncompressedPubKeyCache;
|
|
134
|
+
const res = await this.kms.send(new GetPublicKeyCommand({ KeyId: this.keyId }));
|
|
135
|
+
if (!res.PublicKey) {
|
|
136
|
+
throw new Error("EvmKmsSigner: KMS GetPublicKey returned no PublicKey");
|
|
137
|
+
}
|
|
138
|
+
const decoded = EcdsaPubKeyAsn.decode(Buffer.from(res.PublicKey), "der");
|
|
139
|
+
// pubKey.data is the SEC1 uncompressed point (0x04 || X || Y), 65 bytes.
|
|
140
|
+
this.uncompressedPubKeyCache = new Uint8Array(decoded.pubKey.data);
|
|
141
|
+
return this.uncompressedPubKeyCache;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./evm-kms-signer.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./evm-kms-signer.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ember-finance/sdk",
|
|
3
3
|
"description": "Ember Protocol SDK",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -79,14 +79,16 @@
|
|
|
79
79
|
"test": "tests"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
+
"@aws-sdk/client-kms": "^3.1044.0",
|
|
82
83
|
"@firefly-exchange/library-sui": "^3.0.0",
|
|
84
|
+
"@noble/curves": "^2.2.0",
|
|
85
|
+
"asn1.js": "^5.4.1",
|
|
83
86
|
"axios": "1.13.6",
|
|
84
87
|
"yarn": "^1.22.19"
|
|
85
88
|
},
|
|
86
89
|
"devDependencies": {
|
|
87
90
|
"@mysten/bcs": "^2.0.3",
|
|
88
91
|
"@mysten/sui": "^2.0.0",
|
|
89
|
-
"ethers": "6.13.4",
|
|
90
92
|
"@openapitools/openapi-generator-cli": "^2.24.0",
|
|
91
93
|
"@types/chai": "^4.3.3",
|
|
92
94
|
"@types/chai-as-promised": "^7.1.5",
|
|
@@ -101,6 +103,7 @@
|
|
|
101
103
|
"eslint-plugin-import": "^2.25.2",
|
|
102
104
|
"eslint-plugin-n": "^15.0.0",
|
|
103
105
|
"eslint-plugin-promise": "^6.0.0",
|
|
106
|
+
"ethers": "6.13.4",
|
|
104
107
|
"git-format-staged": "^3.0.0",
|
|
105
108
|
"husky": "^8.0.0",
|
|
106
109
|
"mocha": "^10.1.0",
|