Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions modules/sdk-lib-mpc/src/tss/ecdsa-dkls/commsLayer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { SerializedMessages, AuthEncMessage, AuthEncMessages, PartyGpgKey, AuthMessage } from './types';
import * as pgp from 'openpgp';

/**
* Tolerance window for OpenPGP date-based key validity checks (24 hours).
*
* Background: OpenPGP.js uses the `date` parameter to check key expiry at a
* given point in time. We previously passed `date: null` to disable this check
* entirely (see HSM-706) because OVC cold-signing flows for trust and SMC
* clients can involve significant clock skew between the signing device and the
* server — the device may be air-gapped and its clock can drift by hours.
*
* Note: this GPG expiry check is not strictly required for replay protection.
* The DKLS protocol has its own mechanism for preventing replay attacks
* (session-bound commitments and round-specific message validation), so the
* OpenPGP date check is a defense-in-depth measure rather than the primary
* replay mitigation.
*
* OpenPGP's `date` parameter shifts the reference time for ALL temporal
* checks simultaneously (key expiry, self-signature validity, signature
* freshness). This means a single shifted date cannot independently relax
* key-expiry checks without breaking self-signature validation on fresh keys.
*
* Therefore:
* - encrypt/decrypt omit `date` (use default = current time) for normal key
* expiry checking and self-signature validation.
* - verify uses `now + tolerance` so that signatures from OVC devices whose
* clocks are up to 24 hours ahead are not rejected as "from the future".
*/
export const SIGNATURE_DATE_TOLERANCE_MS = 24 * 60 * 60 * 1000;

/**
* Detach signs a binary and encodes it in base64
* @param data binary to encode in base64 and sign
Expand Down Expand Up @@ -49,7 +77,6 @@ export async function encryptAndDetachSignData(
showVersion: false,
showComment: false,
},
date: null as unknown as undefined,
});
const signature = await pgp.sign({
message,
Expand Down Expand Up @@ -90,13 +117,12 @@ export async function decryptAndVerifySignedData(
showComment: false,
},
format: 'binary',
date: null as unknown as undefined,
});
const verificationResult = await pgp.verify({
message: await pgp.createMessage({ binary: decryptedMessage.data }),
signature: await pgp.readSignature({ armoredSignature: encryptedAndSignedMessage.signature }),
verificationKeys: publicKey,
date: null as unknown as undefined,
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
});
await verificationResult.signatures[0].verified;
return Buffer.from(decryptedMessage.data).toString('base64');
Expand All @@ -113,7 +139,7 @@ export async function verifySignedData(signedMessage: AuthMessage, publicArmor:
message: await pgp.createMessage({ binary: Buffer.from(signedMessage.message, 'base64') }),
signature: await pgp.readSignature({ armoredSignature: signedMessage.signature }),
verificationKeys: publicKey,
date: null as unknown as undefined,
date: new Date(Date.now() + SIGNATURE_DATE_TOLERANCE_MS),
});
try {
await verificationResult.signatures[0].verified;
Expand Down
72 changes: 71 additions & 1 deletion modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsComms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { decryptAndVerifySignedData, encryptAndDetachSignData } from '../../../../src/tss/ecdsa-dkls/commsLayer';
import {
decryptAndVerifySignedData,
encryptAndDetachSignData,
verifySignedData,
SIGNATURE_DATE_TOLERANCE_MS,
} from '../../../../src/tss/ecdsa-dkls/commsLayer';
import * as openpgp from 'openpgp';

describe('DKLS Communication Layer', function () {
Expand Down Expand Up @@ -94,4 +99,69 @@ describe('DKLS Communication Layer', function () {
.toHex()}`
);
});

describe('signature date tolerance', function () {
it('should confirm tolerance constant is 24 hours', function () {
SIGNATURE_DATE_TOLERANCE_MS.should.equal(24 * 60 * 60 * 1000);
});

it('should reject encryption to an expired key', async function () {
// Key created 48h ago with a 23h lifetime → expired 25h ago.
const expiredKey = await openpgp.generateKey({
userIDs: [{ name: 'expired', email: 'expired@username.com' }],
curve: 'secp256k1',
date: new Date(Date.now() - 48 * 60 * 60 * 1000),
keyExpirationTime: 23 * 3600,
});

await encryptAndDetachSignData(
Buffer.from('ffffffff', 'base64'),
expiredKey.publicKey,
senderKey.privateKey
).should.be.rejectedWith('Error encrypting message: Primary key is expired');
});

it('should accept verification of a signature created by a device whose clock is ahead', async function () {
// Simulate a signature created by a device whose clock is 12 hours
// ahead. The verify tolerance (now + 24h) should accept it.
const futureDate = new Date(Date.now() + 12 * 60 * 60 * 1000);
const message = await openpgp.createMessage({ binary: Buffer.from('ffffffff', 'base64') });
const privateKey = await openpgp.readPrivateKey({ armoredKey: senderKey.privateKey });
const signature = await openpgp.sign({
message,
signingKeys: privateKey,
format: 'armored',
detached: true,
date: futureDate,
config: { rejectCurves: new Set(), showVersion: false, showComment: false },
});

const result = await verifySignedData(
{ message: Buffer.from('ffffffff', 'base64').toString('base64'), signature },
senderKey.publicKey
);
result.should.equal(true);
});

it('should reject verification of a signature created more than 24h in the future', async function () {
// Simulate a signature from a device whose clock is 25 hours ahead.
const farFutureDate = new Date(Date.now() + 25 * 60 * 60 * 1000);
const message = await openpgp.createMessage({ binary: Buffer.from('ffffffff', 'base64') });
const privateKey = await openpgp.readPrivateKey({ armoredKey: senderKey.privateKey });
const signature = await openpgp.sign({
message,
signingKeys: privateKey,
format: 'armored',
detached: true,
date: farFutureDate,
config: { rejectCurves: new Set(), showVersion: false, showComment: false },
});

const result = await verifySignedData(
{ message: Buffer.from('ffffffff', 'base64').toString('base64'), signature },
senderKey.publicKey
);
result.should.equal(false);
});
});
});
Loading