Skip to content

Commit 97ada38

Browse files
fix(sdk-lib-mpc): replace date:null with tolerance window in OpenPGP calls
2 parents 566f93b + 884d91e commit 97ada38

File tree

2 files changed

+101
-5
lines changed

2 files changed

+101
-5
lines changed

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
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+
* OpenPGP's `date` parameter shifts the reference time for ALL temporal
20+
* checks simultaneously (key expiry, self-signature validity, signature
21+
* freshness). This means a single shifted date cannot independently relax
22+
* key-expiry checks without breaking self-signature validation on fresh keys.
23+
*
24+
* Therefore:
25+
* - encrypt/decrypt omit `date` (use default = current time) for normal key
26+
* expiry checking and self-signature validation.
27+
* - verify uses `now + tolerance` so that signatures from OVC devices whose
28+
* clocks are up to 24 hours ahead are not rejected as "from the future".
29+
*/
30+
export const SIGNATURE_DATE_TOLERANCE_MS = 24 * 60 * 60 * 1000;
31+
432
/**
533
* Detach signs a binary and encodes it in base64
634
* @param data binary to encode in base64 and sign
@@ -49,7 +77,6 @@ export async function encryptAndDetachSignData(
4977
showVersion: false,
5078
showComment: false,
5179
},
52-
date: null as unknown as undefined,
5380
});
5481
const signature = await pgp.sign({
5582
message,
@@ -90,13 +117,12 @@ export async function decryptAndVerifySignedData(
90117
showComment: false,
91118
},
92119
format: 'binary',
93-
date: null as unknown as undefined,
94120
});
95121
const verificationResult = await pgp.verify({
96122
message: await pgp.createMessage({ binary: decryptedMessage.data }),
97123
signature: await pgp.readSignature({ armoredSignature: encryptedAndSignedMessage.signature }),
98124
verificationKeys: publicKey,
99-
date: null as unknown as undefined,
125+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
100126
});
101127
await verificationResult.signatures[0].verified;
102128
return Buffer.from(decryptedMessage.data).toString('base64');
@@ -113,7 +139,7 @@ export async function verifySignedData(signedMessage: AuthMessage, publicArmor:
113139
message: await pgp.createMessage({ binary: Buffer.from(signedMessage.message, 'base64') }),
114140
signature: await pgp.readSignature({ armoredSignature: signedMessage.signature }),
115141
verificationKeys: publicKey,
116-
date: null as unknown as undefined,
142+
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
117143
});
118144
try {
119145
await verificationResult.signatures[0].verified;

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { decryptAndVerifySignedData, encryptAndDetachSignData } from '../../../../src/tss/ecdsa-dkls/commsLayer';
1+
import {
2+
decryptAndVerifySignedData,
3+
encryptAndDetachSignData,
4+
verifySignedData,
5+
SIGNATURE_DATE_TOLERANCE_MS,
6+
} from '../../../../src/tss/ecdsa-dkls/commsLayer';
27
import * as openpgp from 'openpgp';
38

49
describe('DKLS Communication Layer', function () {
@@ -94,4 +99,69 @@ describe('DKLS Communication Layer', function () {
9499
.toHex()}`
95100
);
96101
});
102+
103+
describe('signature date tolerance', function () {
104+
it('should confirm tolerance constant is 24 hours', function () {
105+
SIGNATURE_DATE_TOLERANCE_MS.should.equal(24 * 60 * 60 * 1000);
106+
});
107+
108+
it('should reject encryption to an expired key', async function () {
109+
// Key created 48h ago with a 23h lifetime → expired 25h ago.
110+
const expiredKey = await openpgp.generateKey({
111+
userIDs: [{ name: 'expired', email: 'expired@username.com' }],
112+
curve: 'secp256k1',
113+
date: new Date(Date.now() - 48 * 60 * 60 * 1000),
114+
keyExpirationTime: 23 * 3600,
115+
});
116+
117+
await encryptAndDetachSignData(
118+
Buffer.from('ffffffff', 'base64'),
119+
expiredKey.publicKey,
120+
senderKey.privateKey
121+
).should.be.rejectedWith('Error encrypting message: Primary key is expired');
122+
});
123+
124+
it('should accept verification of a signature created by a device whose clock is ahead', async function () {
125+
// Simulate a signature created by a device whose clock is 12 hours
126+
// ahead. The verify tolerance (now + 24h) should accept it.
127+
const futureDate = new Date(Date.now() + 12 * 60 * 60 * 1000);
128+
const message = await openpgp.createMessage({ binary: Buffer.from('ffffffff', 'base64') });
129+
const privateKey = await openpgp.readPrivateKey({ armoredKey: senderKey.privateKey });
130+
const signature = await openpgp.sign({
131+
message,
132+
signingKeys: privateKey,
133+
format: 'armored',
134+
detached: true,
135+
date: futureDate,
136+
config: { rejectCurves: new Set(), showVersion: false, showComment: false },
137+
});
138+
139+
const result = await verifySignedData(
140+
{ message: Buffer.from('ffffffff', 'base64').toString('base64'), signature },
141+
senderKey.publicKey
142+
);
143+
result.should.equal(true);
144+
});
145+
146+
it('should reject verification of a signature created more than 24h in the future', async function () {
147+
// Simulate a signature from a device whose clock is 25 hours ahead.
148+
const farFutureDate = new Date(Date.now() + 25 * 60 * 60 * 1000);
149+
const message = await openpgp.createMessage({ binary: Buffer.from('ffffffff', 'base64') });
150+
const privateKey = await openpgp.readPrivateKey({ armoredKey: senderKey.privateKey });
151+
const signature = await openpgp.sign({
152+
message,
153+
signingKeys: privateKey,
154+
format: 'armored',
155+
detached: true,
156+
date: farFutureDate,
157+
config: { rejectCurves: new Set(), showVersion: false, showComment: false },
158+
});
159+
160+
const result = await verifySignedData(
161+
{ message: Buffer.from('ffffffff', 'base64').toString('base64'), signature },
162+
senderKey.publicKey
163+
);
164+
result.should.equal(false);
165+
});
166+
});
97167
});

0 commit comments

Comments
 (0)