Skip to content

Commit c19a442

Browse files
committed
feat(sdk-core): add externalSigner createOfflineRound1Share handler
Add the EdDSA MPCv2 offline round-1 handler for external signer flows. It stores encrypted carry-over state for the next signing round. - Generate the round-1 EdDSA MPCv2 signature share from a fresh DSG session. - Persist DSG session state and user message payload in the round-1 session. - Encrypt session and ephemeral GPG private key data with signing-context adata. - Cover SJCL encryption, v2 envelopes, payload shape, and transaction guards. Ticket: WCI-378
1 parent 9e96014 commit c19a442

2 files changed

Lines changed: 534 additions & 7 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2K
4646
export class EddsaMPCv2Utils extends BaseEddsaUtils {
4747
private static readonly MPS_DSG_SIGNING_USER_GPG_KEY = 'MPS_DSG_SIGNING_USER_GPG_KEY';
4848
private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE';
49+
private static readonly MPS_DSG_SIGNING_ROUND2_STATE = 'MPS_DSG_SIGNING_ROUND2_STATE';
4950

5051
/** @inheritdoc */
5152
async createKeychains(params: {
@@ -534,6 +535,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
534535

535536
// #region external signer
536537

538+
// #region Round1Share
537539
async createOfflineRound1Share(params: {
538540
txRequest: TxRequest;
539541
prv: string;
@@ -597,6 +599,113 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
597599

598600
return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey };
599601
}
602+
// #endregion
603+
604+
// #region Round2Share
605+
async createOfflineRound2Share(params: {
606+
txRequest: TxRequest;
607+
walletPassphrase: string;
608+
bitgoPublicGpgKey: string;
609+
encryptedUserGpgPrvKey: string;
610+
encryptedRound1Session: string;
611+
}): Promise<{
612+
signatureShareRound2: SignatureShareRecord;
613+
encryptedRound2Session: string;
614+
}> {
615+
const { walletPassphrase, encryptedUserGpgPrvKey, encryptedRound1Session, bitgoPublicGpgKey, txRequest } = params;
616+
617+
const { signableHex, derivationPath } = this.getSignableHexAndDerivationPath(
618+
txRequest,
619+
'Unable to find transactions in txRequest'
620+
);
621+
const adata = `${signableHex}:${derivationPath}`;
622+
623+
const useV2 = isV2Envelope(encryptedRound1Session);
624+
625+
const { bitgoGpgKey, userGpgPrvKey } = await this.getBitgoAndUserGpgKeys(
626+
bitgoPublicGpgKey,
627+
encryptedUserGpgPrvKey,
628+
walletPassphrase,
629+
adata,
630+
EddsaMPCv2Utils.MPS_DSG_SIGNING_USER_GPG_KEY
631+
);
632+
633+
const transactions = txRequest.transactions;
634+
assert(Array.isArray(transactions) && transactions.length === 1, 'txRequest must have exactly one transaction');
635+
const signatureShares = transactions[0].signatureShares;
636+
assert(signatureShares, 'Missing signature shares in round 1 txRequest');
637+
638+
const bitgoShareRoundOne = getBitgoSignatureShare(signatureShares, SignatureShareType.USER);
639+
const parsedBitGoToUserSigShareRoundOne = decodeWithCodec(
640+
EddsaMPCv2SignatureShareRound1Output,
641+
JSON.parse(bitgoShareRoundOne.share),
642+
'Unexpected signature share response. Unable to parse data.'
643+
);
644+
645+
if (parsedBitGoToUserSigShareRoundOne.type !== 'round1Output') {
646+
throw new Error('Unexpected signature share response. Unable to parse data.');
647+
}
648+
649+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgKey);
650+
651+
this.validateAdata(adata, encryptedRound1Session, EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND1_STATE);
652+
653+
let decryptedRound1Session: string;
654+
if (useV2) {
655+
decryptedRound1Session = await this.bitgo.decryptAsync({
656+
input: encryptedRound1Session,
657+
password: walletPassphrase,
658+
});
659+
} else {
660+
decryptedRound1Session = this.bitgo.decrypt({
661+
input: encryptedRound1Session,
662+
password: walletPassphrase,
663+
});
664+
}
665+
666+
const { dsgSession, userMsgPayload } = JSON.parse(decryptedRound1Session) as {
667+
dsgSession: string;
668+
userMsgPayload: string;
669+
};
670+
671+
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
672+
userDsg.restoreSession(dsgSession);
673+
const userMsg1: MPSTypes.DeserializedMessage = {
674+
from: MPCv2PartiesEnum.USER,
675+
payload: new Uint8Array(Buffer.from(userMsgPayload, 'base64')),
676+
};
677+
678+
const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]);
679+
assert(userMsg2, 'DSG handleIncomingMessages produced no round-2 output');
680+
681+
const signatureShareRound2 = await getSignatureShareRoundTwo(userMsg2, userGpgPrvKey);
682+
const sessionPayload = JSON.stringify({
683+
dsgSession: userDsg.getSession(),
684+
userMsgPayload: Buffer.from(userMsg2.payload).toString('base64'),
685+
});
686+
687+
if (useV2) {
688+
const session = await this.bitgo.createEncryptionSession(walletPassphrase);
689+
try {
690+
const encryptedRound2Session = await session.encrypt(
691+
sessionPayload,
692+
`${EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND2_STATE}:${adata}`
693+
);
694+
return { signatureShareRound2, encryptedRound2Session };
695+
} finally {
696+
session.destroy();
697+
}
698+
}
699+
700+
const encryptedRound2Session = this.bitgo.encrypt({
701+
input: sessionPayload,
702+
password: walletPassphrase,
703+
adata: `${EddsaMPCv2Utils.MPS_DSG_SIGNING_ROUND2_STATE}:${adata}`,
704+
});
705+
706+
return { signatureShareRound2, encryptedRound2Session };
707+
}
708+
// #endregion
600709

601710
/** @inheritdoc */
602711
async signEddsaMPCv2TssUsingExternalSigner(

0 commit comments

Comments
 (0)