Skip to content

Commit 31e3b76

Browse files
fix(sdk-lib-mpc): replace date:null with 24h tolerance window in OpenPGP calls
Replace `date: null as unknown as undefined` with a 24-hour tolerance window in all OpenPGP encrypt/decrypt/verify calls. The null date was intentionally added (HSM-706) for OVC cold-signing flows where air-gapped devices can have significant clock drift, but fully disabling date checks is unnecessary — the DKLS protocol has its own replay protection via session-bound commitments and round-specific validation. A 24-hour window preserves OVC compatibility while re-enabling key expiry checks as a defense-in-depth measure. Ticket: WAL-379 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 811c442 commit 31e3b76

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

modules/sdk-lib-mpc/src/tss/ecdsa-dkls/commsLayer.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import { SerializedMessages, AuthEncMessage, AuthEncMessages, PartyGpgKey, AuthMessage } from './types';
22
import * as pgp from 'openpgp';
33

4+
/**
5+
* Tolerance window for OpenPGP date-based key validity checks (24 hours).
6+
*
7+
* Background: OpenPGP.js uses the `date` parameter to check key expiry at a
8+
* given point in time. We previously passed `date: null` to disable this check
9+
* entirely (see HSM-706) because OVC cold-signing flows for trust and SMC
10+
* clients can involve significant clock skew between the signing device and the
11+
* server — the device may be air-gapped and its clock can drift by hours.
12+
*
13+
* Note: this GPG expiry check is not strictly required for replay protection.
14+
* The DKLS protocol has its own mechanism for preventing replay attacks
15+
* (session-bound commitments and round-specific message validation), so the
16+
* OpenPGP date check is a defense-in-depth measure rather than the primary
17+
* replay mitigation.
18+
*
19+
* A 24-hour window preserves compatibility with OVC flows while still
20+
* rejecting operations against keys that have been expired for more than a day.
21+
*/
22+
export const SIGNATURE_DATE_TOLERANCE_MS = 24 * 60 * 60 * 1000;
23+
424
/**
525
* Detach signs a binary and encodes it in base64
626
* @param data binary to encode in base64 and sign
@@ -49,7 +69,7 @@ export async function encryptAndDetachSignData(
4969
showVersion: false,
5070
showComment: false,
5171
},
52-
date: null as unknown as undefined,
72+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
5373
});
5474
const signature = await pgp.sign({
5575
message,
@@ -90,13 +110,13 @@ export async function decryptAndVerifySignedData(
90110
showComment: false,
91111
},
92112
format: 'binary',
93-
date: null as unknown as undefined,
113+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
94114
});
95115
const verificationResult = await pgp.verify({
96116
message: await pgp.createMessage({ binary: decryptedMessage.data }),
97117
signature: await pgp.readSignature({ armoredSignature: encryptedAndSignedMessage.signature }),
98118
verificationKeys: publicKey,
99-
date: null as unknown as undefined,
119+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
100120
});
101121
await verificationResult.signatures[0].verified;
102122
return Buffer.from(decryptedMessage.data).toString('base64');
@@ -113,7 +133,7 @@ export async function verifySignedData(signedMessage: AuthMessage, publicArmor:
113133
message: await pgp.createMessage({ binary: Buffer.from(signedMessage.message, 'base64') }),
114134
signature: await pgp.readSignature({ armoredSignature: signedMessage.signature }),
115135
verificationKeys: publicKey,
116-
date: null as unknown as undefined,
136+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
117137
});
118138
try {
119139
await verificationResult.signatures[0].verified;

modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsComms.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { decryptAndVerifySignedData, encryptAndDetachSignData } from '../../../../src/tss/ecdsa-dkls/commsLayer';
1+
import {
2+
decryptAndVerifySignedData,
3+
encryptAndDetachSignData,
4+
SIGNATURE_DATE_TOLERANCE_MS,
5+
} from '../../../../src/tss/ecdsa-dkls/commsLayer';
26
import * as openpgp from 'openpgp';
37

48
describe('DKLS Communication Layer', function () {
@@ -94,4 +98,34 @@ describe('DKLS Communication Layer', function () {
9498
.toHex()}`
9599
);
96100
});
101+
102+
describe('signature date tolerance', function () {
103+
// Key that expires in 1 second — well within the 24-hour tolerance window,
104+
// so encrypt at Date.now() + 24h will see an expired key.
105+
let shortLivedRecipientKey: { publicKey: string; privateKey: string };
106+
107+
before(async function () {
108+
shortLivedRecipientKey = await openpgp.generateKey({
109+
userIDs: [{ name: 'shortlived', email: 'shortlived@username.com' }],
110+
curve: 'secp256k1',
111+
keyExpirationTime: 1,
112+
});
113+
});
114+
115+
it('should reject encryption to an expired key', async function () {
116+
const text = 'ffffffff';
117+
118+
// Encryption checks key validity at Date.now() + 24h, which is past
119+
// the 1-second key expiry, so this should be rejected.
120+
await encryptAndDetachSignData(
121+
Buffer.from(text, 'base64'),
122+
shortLivedRecipientKey.publicKey,
123+
senderKey.privateKey
124+
).should.be.rejectedWith('Error encrypting message: Primary key is expired');
125+
});
126+
127+
it('should confirm tolerance constant is 24 hours', function () {
128+
SIGNATURE_DATE_TOLERANCE_MS.should.equal(24 * 60 * 60 * 1000);
129+
});
130+
});
97131
});

0 commit comments

Comments
 (0)