1. 引言
前序博客有:
开源代码见:
- https://github.com/BitGo/BitGoJS/blob/master/modules/account-lib/test/unit/mpc/tss/eddsa/eddsa.ts(TSS Unit Tests)
- https://github.com/BitGo/BitGoJS/blob/master/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts#L128(TSS 2-of-3 Key Creation Test)
- https://github.com/BitGo/BitGoJS/blob/master/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts#L279(Signing Test with 2-of-3 TSS)
回顾Multi-Sig和TSS:
- Multi-Sig (Multi-Signature Scheme) 多签方案
- TSS (Threshold Signature Scheme) 门限签名方案
这2个方案都可保护密钥安全,但机制有所不同。
Multi-Sig和TSS都是通过在多个参与者分发共享秘密私钥信息来避免单点故障,这意味着malicious actor需同时攻击多个密钥持有者才能恢复出相应的私钥。
Multi-Sig与TSS的主要不同之处在于:
- Multi-Sig采用多个distributed密钥,需要一定阈值数量的密钥来签署交易。
- TSS对应一个密钥,切分为多片分发,也需要一定阈值数量的key shares来签署交易。
2. 何为TSS?
利用分布式密钥生成协议,TSS使多方能够参与密钥生成过程,是一种 在整个流程中不会透露私钥本身 的数字签名方案。
在交易过程中,每个参与者都会生成一个指定给其他参与者的值,然后通过多个回合共享这些值。结合这些secret shares,每个参与者可以生成自己的private key share。在签名过程中,只要阈值数量的参与者聚集在一起就可派生出最终签名。
整个过程中,私钥永远不会构建——即意味着私钥永远不会暴露。malicious actor需攻击达到阈值多个参与者,否则无法恢复出该私钥。
TSS的一个广泛使用的类比是,在不透露个人工资的情况下,找到一群人的平均工资。
假设有4方:
- Arnold:工资$45000
- Bonnie:工资$41000
- Chen:工资$53000
- Daniel:工资$27000
每个人将其工资切分为4个随机值,并将其中3个分享给另外3个人,每个人将其剩的那个随机值与收到的其它3个人分享的随机值求和,然后4个人分享出各自的随机数和值——总和为$166000,除以4即为平均工资$41500:
在不泄露个人工资的情况下,可计算出相应的平均工资。
TSS的工作原理与此类似,最终的签名——类比为本例中的平均工资——可通过组合secret shares来实现,这些secret shares为各方生成的random values,不会泄露各方的private key share(对应本例中的个人工资)。
TSS与Shamir’s Secret Sharing(SSS)不同:
- SSS中:需将私钥切片,然后分发给各参与者。签署交易时,各分片持有者需聚集重构出相应的私钥。
- TSS中:在整个流程中永远不会重构私钥——无论是密钥生成还是交易签署过程中,从而不存在密钥暴露的风险。
3. TSS vs Multi-Sig
TSS和Multi-Sig分别适合不同的场景:
- 1)问责性:如果最终用户想知道哪些方参与了交易签名,Multi-Sig钱包更适合。区块链准确地记录了使用的密钥,然后可以追溯。与此同时,TSS只记录了是否使用了key shares的阈值,而不是具体的哪一个。因此,没有直接的链上方式来确定谁参与了交易签署。
- 2)兼容性:并非所有协议都支持Multi-Sig。例如,BTC原生支持Multi-Sig方案,这要归功于其支付到脚本哈希(P2SH)标准,在该标准中,最终用户可以指定从一个地址移动交易所需的签名数量,但对于某些其他区块链来说,情况并非如此。
如果协议本身不支持Multi-Sig,则需要创建一个智能合约来提供相同的功能。如果智能合约没有经过良好的审计,它可能会受到漏洞的影响,例如Parity Wallets漏洞,其中约3000万美元被盗。安全地编写智能合约并进行审核需要时间,最好是由第三方公司进行审核,这往往会减缓添加新币支持的过程。 - 3)成本:多重签名交易包含更多数据,因为它们每个交易包含多个签名,需要在链上存储和验证。同时,TSS交易只包含一个签名,不需要额外的智能合约,因此链上的数据更少。这使得TSS在gas 费用方面比Multi-Sig便宜一些。
*4)速度:Multi-Sig和TSS提供了相当的交易处理速度。有人可能会怀疑Multi-Sig可能会更慢,因为它会产生多个需要在链上验证的签名。然而,TSS有其自身的障碍,首先需要参与者之间进行更多的往返以生成交易。无论如何,这都有点无意义:区块链的速度通常受到其自身协议的约束,而不是特定钱包使用的签名方案。 - 5)灵活性:TSS比Multi-Sig更具协议不可知性,因此对于提供商来说实现起来更为灵活;它可以用于任何区块链,而不需要智能合约。智能合约需要时间开发,需要彻底审计。有了TSS,任何coins都可以快速进入web3应用程序。
4. BigGo TSS方案 vs 市场上其它MPC方案
虽然TSS源于MPC(多方计算)——一种替代安全方案,但BitGo的TSS解决方案解决了困扰当前市场上一些MPC/TSS实现的以下问题:
- 1)问责性:责任本质上,TSS不会显示谁在区块链上签署了交易。然而,BitGo维护安全、详细的日志,记录在给定交易中使用了哪些key shares。
BitGo的TSS解决方案遵循我们的Multi-Sig模型,其中分别有User、BitGo和BackUp key shares,其中两个key share需要一起构建签名。我们在我们的平台中存储日志,记录在交易签名期间使用了这3个key shares中的哪个,然后保护这些日志以确保它们是不可变的。 - 2)专用技术。BitGo不依赖第三方解决方案来保护BitGo key share。相反,BitGo有一个专门的研发团队来开发为加密货币量身定制的硬件安全模块(HSM)。
通过设计HSM,我们保护自己免受供应链攻击。我们也不会盲目签署任何交易,这是为pre-cryptocurrency世界构建的传统HSM的风险。相反,BitGo专门构建的HSM在完全签名并将其发布到区块链之前,会对收到的每笔交易进行额外检查。 - 3)混合方法。BitGo不仅仅依靠某种技术来保护key share;我们通过一种多样化的混合方法确保业务连续性,其中不同类型的技术存储和保护key shares。
BitGo key share(也称为平台key share)由BitGo保管,并受我们专门构建的硬件安全模块(HSM)保护。同时,用户key share仍由客户保管。
保护用户key share的客户端技术可以是TPM、HSM或部署在场所或云上的任何安全飞地。出于备份和灾难恢复目的,备份key share受离线保护。
通过混合方法,BitGo可以防止单点故障:如果在保护key share的某项技术中发现漏洞,则整个key share都面临风险,因此由该技术管理的资金也面临风险。
如果企业在漏洞被利用之前发现了漏洞,他们需要尽快找到修补程序,并在修补程序交付之前冻结其服务。如果补丁不可用,他们需要找到一种新技术来迁移key share,这可能需要几个月的时间,并导致服务停机。BitGo通过不依赖保护TSS key share的单一技术来解决这个问题。
BitGo的混合方法还支持业务连续性,因为我们部署的保护key share的技术在地理上是分开的。在自然灾害的情况下,依靠单一的地理位置可能最终会损失客户的所有资金。 - 4)同行评审。许多TSS解决方案没有经过开源和实战测试,这意味着它们可能存在未发现的漏洞。今天,许多公司依靠专有密码技术来掩盖他们为了更快地进入市场而匆忙完成这项工作的事实。这意味着密码技术中很可能存在漏洞,将来也会被利用,这一切都要归功于测试和透明度不足。
安全专家Bruce Schneier认为,你不能破坏你创建的代码并不意味着没有人可以。同样,OWASP(开放式Web应用程序安全项目)表示:“专有加密算法不值得信任,因为它们通常依赖于‘晦涩难懂的安全性’,而不是可靠的数学。如果可能,应该避免使用这些算法。”
我们已经开源并启动了一个bug赏金计划来解决这个问题,因为我们相信这是确保客户安全的唯一途径。我们的技术经过了验证和战斗测试-您不必相信我们的承诺。 - 5)备份密钥。某些TSS实现对每个key share都是相同的,并且不会为备份目的分配任何密钥。然而,客户端需要能够在不需要服务提供商的情况下签署交易。如果服务提供商因自然灾害、技术故障或服务停机而无法提供服务,客户机仍应能够处理其交易。
因此,BitGo在客户的监护下分配一个用于备份的key share,以便用户使用user key share和备份key share来处理交易。
这也可以在客户丢失key share的情况下保护客户,例如,如果用户key share的持有者离开公司,或者密码丢失。在这种情况下,用户仍然可以使用他们的备份key share与BitGo key share来签署交易。
5. BitGo Shamir’s secret share实现
BitGo的SSS秘钥分发协议中,支持的曲线有2种:
- Ed25519Curve
- Secp256k1Curve
代码见:
- https://github.com/BitGo/BitGoJS/blob/master/modules/account-lib/test/unit/mpc/shamir.ts(SSS密钥分发测试用例)
- https://github.com/BitGo/BitGoJS/blob/master/modules/sdk-core/src/account-lib/mpc/shamir.ts(SSS密钥分发方案实现)
SSS密钥分发方案关键函数有:
- split:将某secret
s
s
s进行切分,共numShares份,其中threshold份即可恢复出该secret。本质为构建degree为
threshold-1
的多项式,该多项式的常数项为secret s s s(其它项系数为随机值),插值点必须大于0,可指定插值点(默认插值点为1~numShares+1),将插值结果放入shares
中,v
为其它项系数与曲线basePoint乘积。const { shares, v } = shamir.split(secret, 2, 3, undefined, salt);
- verify:验证某secret share确实是相应index的有效切片。
shamir.verify(shares[1], v, 1).should.equal(true);
- combine:根据threshold个share重构出原始的secret。
const combineSecret12 = shamir.combine({ 1: shares[1], 2: shares[2], }); combineSecret12.toString().should.equal(secretString);
/**
* Perform Shamir sharing on the secret `secret` to the degree `threshold - 1` split `numShares`
* ways. The split secret requires `threshold` shares to be reconstructed.
*
* @param secret secret to split
* @param threshold share threshold required to reconstruct secret
* @param numShares total number of shares to split to split secret into
* @param indices optional indices which can be used while generating the shares
* @param salt optional salt which could be used while generating the shares
* @returns Dictionary containing `shares`, a dictionary where each key is an int
* in the range 1<=x<=numShares representing that share's free term, and `v`, an
* array of proofs to be shared with all participants.
*/
split(secret: bigint, threshold: number, numShares: number, indices?: Array<number>, salt = BigInt(0)): SplitSecret {
let bigIndices: Array<bigint>;
if (indices) {
bigIndices = indices.map((i) => {
if (i < 1) {
throw new Error('Invalid value supplied for indices');
}
return BigInt(i);
});
} else {
// make range(1, n + 1)
bigIndices = Array(numShares)
.fill(null)
.map((_, i) => BigInt(i + 1));
}
if (threshold < 2) {
throw new Error('Threshold cannot be less than two');
}
if (threshold > numShares) {
throw new Error('Threshold cannot be greater than the total number of shares');
}
const coefs: bigint[] = [];
const v: Array<bigint> = [];
for (let ind = 0; ind < threshold - 1; ind++) {
const coeff = clamp(
bigIntFromBufferLE(crypto.createHmac('sha256', ind.toString(10)).update(bigIntToBufferLE(secret, 32)).digest())
);
coefs.push(coeff);
v.unshift(this.curve.basePointMult(coeff));
}
coefs.push(secret);
const shares: Record<number, bigint> = {};
for (let ind = 0; ind < bigIndices.length; ind++) {
const x = bigIndices[ind];
let partial = coefs[0];
for (let other = 1; other < coefs.length; other++) {
partial = this.curve.scalarAdd(coefs[other], this.curve.scalarMult(partial, x));
}
shares[parseInt(x.toString(), 10)] = partial;
}
return { shares, v };
}
/**
* Verify a VSS share.
*
* @param u Secret share received from other party.
* @param v Verification values received from other party.
* @param index Verifier's index.
* @returns True on success; otherwise throws Error.
*/
verify(u: bigint, v: Array<bigint>, index: number): boolean {
if (v.length < 2) {
throw new Error('Threshold cannot be less than two');
}
if (index < 1) {
throw new Error('Invalid value supplied for index');
}
const i = BigInt(index);
let x = v[0];
let t = BigInt(1);
for (const vsj of v.slice(1)) {
t = this.curve.scalarMult(t, i);
const vjt = this.curve.pointMultiply(vsj, t);
x = this.curve.pointAdd(x, vjt);
}
const sigmaG = this.curve.basePointMult(u);
if (x !== sigmaG) {
throw new Error('Could not verify share');
}
return true;
}
/**
* Reconstitute a secret from a dictionary of shares. The number of shares must
* be equal to `t` to reconstitute the original secret.
*
* @param shares dictionary of shares. each key is the free term of the share
* @returns secret
*/
combine(shares: Record<number, bigint>): bigint {
try {
let s = BigInt(0);
for (const i in shares) {
const yi = shares[i];
const xi = BigInt(i);
let num = BigInt(1);
let denum = BigInt(1);
for (const j in shares) {
const xj = BigInt(j);
if (xi !== xj) {
num = this.curve.scalarMult(num, xj);
}
}
for (const j in shares) {
const xj = BigInt(j);
if (xi !== xj) {
denum = this.curve.scalarMult(denum, this.curve.scalarSub(xj, xi));
}
}
const inverted = this.curve.scalarInvert(denum);
const innerMultiplied = this.curve.scalarMult(num, inverted);
const multiplied = this.curve.scalarMult(innerMultiplied, yi);
s = this.curve.scalarAdd(multiplied, s);
}
return s;
} catch (error) {
throw new Error('Failed to combine Shamir shares , ' + error);
}
}
6. BitGo基于MPC密钥分发协议构建的TSS门限签名方案
前序博客有:
BitGo基于MPC密钥分发协议构建的TSS门限签名方案:
- 1)Sepc256k1曲线:对应ecdsa模块
- 2)Ed25519曲线:对应eddsa模块
以eddsa模块为例:
- https://github.com/BitGo/BitGoJS/blob/master/modules/account-lib/test/unit/mpc/tss/eddsa/eddsa.ts(TSS Unit Tests)
- https://github.com/BitGo/BitGoJS/blob/master/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts#L128(TSS 2-of-3 Key Creation Test)
- https://github.com/BitGo/BitGoJS/blob/master/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts#L279(Signing Test with 2-of-3 TSS)
- https://github.com/BitGo/BitGoJS/blob/master/modules/sdk-core/src/account-lib/mpc/tss/eddsa/eddsa.ts(MPC密钥分发 以及 Eddsa门限签名算法)
Eddsa门限签名核心思想为:
/**
* Module provides functions for MPC using threshold signature scheme (TSS). It contains
* functions for key generation and message signing with EdDSA.
*
*
* ======================
* EdDSA Key Generation
* ======================
* 1. Each signer generates their own key share, which involves a private u-share and a public y-share.
* 2. Signers distribute their y-share to other signers.
* 3. After exchanging y-shares the next phase is to combine key shares. Each signer combines their u-share
* with the y-shares received from other signers in order to generate a p-share for themselves. We
* also save j-shares for other signers.
* 4. At this point the players do not distribute any shares and the first phase of the
* signing protocol is complete.
*
* ======================
* EdDSA Signing
* ======================
* 1. The parties from key generation decide they want to sign something. They begin the signing protocol
* by generating shares of an ephemeral key.
*
* a) Each signer uses his p-share and the j-shares stored for other players to generate his signing share.
* b) This results in each signer having a private x-share and public r-shares.
*
* 2. Signers distribute their r-shares to other signers.
* 3. After exchanging r-shares, each signer signs their share of the ephemeral key using their private
* x-share with the r-shares from other signers.
* 4. This results in each signer having a public g-share which they send to the other signers.
* 5. After the signers broadcast their g-shares, the final signature can be re-constructed independently.
*/
keyShare(index: number, threshold: number, numShares: number, seed?: Buffer): KeyShare {
if (!(index > 0 && index <= numShares)) {
throw new Error('Invalid KeyShare config');
}
if (seed && seed.length !== 64) {
throw new Error('Seed must have length 64');
}
const seedchain = seed ?? randomBytes(64);
const actualSeed = seedchain.slice(0, 32);
const chaincode = seedchain.slice(32);
const h = createHash('sha512').update(actualSeed).digest();
const u = clamp(bigIntFromBufferLE(h.slice(0, 32)));
const y = Eddsa.curve.basePointMult(u);
const { shares: split_u, v } = Eddsa.shamir.split(u, threshold, numShares);
const P_i: UShare = {
i: index,
t: threshold,
n: numShares,
y: bigIntToBufferLE(y, 32).toString('hex'),
seed: actualSeed.toString('hex'),
chaincode: chaincode.toString('hex'),
};
const shares: KeyShare = {
uShare: P_i,
yShares: {},
};
for (const ind in split_u) {
const i = parseInt(ind, 10);
if (i === index) {
continue;
}
shares.yShares[i] = {
i,
j: P_i.i,
y: bigIntToBufferLE(y, 32).toString('hex'),
v: bigIntToBufferLE(v[0], 32).toString('hex'),
u: bigIntToBufferLE(split_u[ind], 32).toString('hex'),
chaincode: chaincode.toString('hex'),
};
}
return shares;
}
keyCombine(uShare: UShare, yShares: YShare[]): KeyCombine {
const h = createHash('sha512').update(Buffer.from(uShare.seed, 'hex')).digest();
const u = clamp(bigIntFromBufferLE(h.slice(0, 32)));
const yValues = [uShare, ...yShares].map((share) => bigIntFromBufferLE(Buffer.from(share.y, 'hex')));
const y = yValues.reduce((partial, share) => Eddsa.curve.pointAdd(partial, share));
const chaincodes = [uShare, ...yShares].map(({ chaincode }) => bigIntFromBufferBE(Buffer.from(chaincode, 'hex')));
const chaincode = chaincodes.reduce((acc, chaincode) => (acc + chaincode) % base);
// Verify shares.
for (const share of yShares) {
if ('v' in share) {
try {
Eddsa.shamir.verify(
bigIntFromBufferLE(Buffer.from(share.u, 'hex')),
[bigIntFromBufferLE(Buffer.from(share.y, 'hex')), bigIntFromBufferLE(Buffer.from(share.v!, 'hex'))],
uShare.i
);
} catch (err) {
// TODO(BG-61036): Fix Verification
// throw new Error(`Could not verify share from participant ${share.j}. Verification error: ${err}`);
}
}
}
const P_i: PShare = {
i: uShare.i,
t: uShare.t,
n: uShare.n,
y: bigIntToBufferLE(y, 32).toString('hex'),
u: bigIntToBufferLE(u, 32).toString('hex'),
prefix: h.slice(32).toString('hex'),
chaincode: bigIntToBufferBE(chaincode, 32).toString('hex'),
};
const players: KeyCombine = {
pShare: P_i,
jShares: {},
};
for (let ind = 0; ind < yShares.length; ind++) {
const P_j = yShares[ind];
players.jShares[P_j.j] = {
i: P_j.j,
j: P_i.i,
};
}
return players;
}
signShare(message: Buffer, pShare: PShare, jShares: JShare[], seed?: Buffer): SignShare {
if (seed && seed.length !== 64) {
throw new Error('Seed must have length 64');
}
const indices = [pShare, ...jShares].map(({ i }) => i);
const { shares: split_u, v } = Eddsa.shamir.split(
bigIntFromBufferLE(Buffer.from(pShare.u, 'hex')),
pShare.t,
pShare.n
);
// Generate nonce contribution.
const prefix = Buffer.from(pShare.prefix, 'hex');
const randomBuffer = seed ?? randomBytes(64);
const digest = createHash('sha512')
.update(Buffer.concat([prefix, message, randomBuffer]))
.digest();
const r = Eddsa.curve.scalarReduce(bigIntFromBufferLE(digest));
const R = Eddsa.curve.basePointMult(r);
const { shares: split_r } = Eddsa.shamir.split(r, indices.length, indices.length, indices);
const P_i: XShare = {
i: pShare.i,
y: pShare.y,
u: bigIntToBufferLE(split_u[pShare.i], 32).toString('hex'),
r: bigIntToBufferLE(split_r[pShare.i], 32).toString('hex'),
R: bigIntToBufferLE(R, 32).toString('hex'),
};
const resultShares: SignShare = {
xShare: P_i,
rShares: {},
};
for (let ind = 0; ind < jShares.length; ind++) {
const S_j = jShares[ind];
resultShares.rShares[S_j.i] = {
i: S_j.i,
j: pShare.i,
u: bigIntToBufferLE(split_u[S_j.i], 32).toString('hex'),
v: bigIntToBufferLE(v[0], 32).toString('hex'),
r: bigIntToBufferLE(split_r[S_j.i], 32).toString('hex'),
R: bigIntToBufferLE(R, 32).toString('hex'),
};
}
return resultShares;
}
sign(message: Buffer, playerShare: XShare, rShares: RShare[], yShares: YShare[] = []): GShare {
const S_i = playerShare;
const uValues = [playerShare, ...rShares, ...yShares].map(({ u }) => bigIntFromBufferLE(Buffer.from(u, 'hex')));
const x = uValues.reduce((acc, u) => Eddsa.curve.scalarAdd(acc, u));
const RValues = [playerShare, ...rShares].map(({ R }) => bigIntFromBufferLE(Buffer.from(R, 'hex')));
const R = RValues.reduce((partial, share) => Eddsa.curve.pointAdd(partial, share));
const rValues = [playerShare, ...rShares].map(({ r }) => bigIntFromBufferLE(Buffer.from(r, 'hex')));
const r = rValues.reduce((partial, share) => Eddsa.curve.scalarAdd(partial, share));
const combinedBuffer = Buffer.concat([bigIntToBufferLE(R, 32), Buffer.from(S_i.y, 'hex'), message]);
const digest = createHash('sha512').update(combinedBuffer).digest();
const k = Eddsa.curve.scalarReduce(bigIntFromBufferLE(digest));
const gamma = Eddsa.curve.scalarAdd(r, Eddsa.curve.scalarMult(k, x));
const result = {
i: playerShare.i,
y: playerShare.y,
gamma: bigIntToBufferLE(gamma, 32).toString('hex'),
R: bigIntToBufferLE(R, 32).toString('hex'),
};
return result;
}
signCombine(shares: GShare[]): Signature {
const y = shares[0].y;
const R = shares[0].R;
const resultShares = {};
for (const ind in shares) {
const S_i = shares[ind];
resultShares[S_i.i] = bigIntFromBufferLE(Buffer.from(S_i.gamma, 'hex'));
}
const sigma: bigint = Eddsa.shamir.combine(resultShares);
const result = {
y,
R,
sigma: bigIntToBufferLE(sigma, 32).toString('hex'),
};
return result;
}
verify(message: Buffer, signature: Signature): boolean {
const publicKey = bigIntFromBufferLE(Buffer.from(signature.y, 'hex'));
const signedMessage = Buffer.concat([Buffer.from(signature.R, 'hex'), Buffer.from(signature.sigma, 'hex')]);
return Eddsa.curve.verify(message, signedMessage, publicKey);
}
参考资料
[1] BitGo TSS: A technical deep-dive
[2] BitGo’s TSS Bug Bounty Program