Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ConfigService } from 'src/config/config';
import { AuthService } from '../auth.service';

// Regression guard for the custodial-Lightning sign-in bypass: an empty stored signature must never
// authenticate. verifySignature is private, so we exercise it via bracket access with a bare instance
// (the Lightning branch only uses getSignMessages + the static CryptoService.getBlockchainsBasedOn).
describe('AuthService custodial Lightning signature check', () => {
let service: AuthService;

// LNNID + 66 alnum → recognised as a Lightning address by CryptoService
const lightningAddress = `LNNID${'A'.repeat(66)}`;
// 140 lowercase alnum → matches the custodial-Lightning signature shape
const validShapeSignature = 'a'.repeat(140);

const verify = (signature: string, dbSignature: string | undefined, isSignUp = false): Promise<boolean> =>
(
service as unknown as {
verifySignature: (
address: string,
signature: string,
isCustodial: boolean,
key: string | undefined,
dbSignature: string | undefined,
blockchain: undefined,
isSignUp: boolean,
) => Promise<boolean>;
}
).verifySignature(lightningAddress, signature, false, undefined, dbSignature, undefined, isSignUp);

beforeAll(() => {
new ConfigService();
});

beforeEach(() => {
service = Object.create(AuthService.prototype);
});

it('rejects sign-in when the stored signature is empty (account takeover guard)', async () => {
await expect(verify(validShapeSignature, '')).resolves.toBe(false);
await expect(verify(validShapeSignature, undefined)).resolves.toBe(false);
});

it('rejects sign-in when the signature does not match the stored one', async () => {
await expect(verify(validShapeSignature, 'b'.repeat(140))).resolves.toBe(false);
});

it('accepts sign-in when the signature matches a non-empty stored signature', async () => {
await expect(verify(validShapeSignature, validShapeSignature)).resolves.toBe(true);
});

it('accepts sign-up (establishes the first signature) even without a stored signature', async () => {
await expect(verify(validShapeSignature, undefined, true)).resolves.toBe(true);
});
});
10 changes: 7 additions & 3 deletions src/subdomains/generic/user/models/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class AuthService {
const custodyProvider = await this.custodyProviderService.getWithMasterKey(dto.signature).catch(() => undefined);
if (
!custodyProvider &&
!(await this.verifySignature(dto.address, dto.signature, isCustodial, dto.key, undefined, dto.blockchain))
!(await this.verifySignature(dto.address, dto.signature, isCustodial, dto.key, undefined, dto.blockchain, true))
) {
throw new BadRequestException('Invalid signature');
}
Expand Down Expand Up @@ -475,14 +475,18 @@ export class AuthService {
key?: string,
dbSignature?: string,
blockchain?: Blockchain,
isSignUp = false,
): Promise<boolean> {
const { defaultMessage, fallbackMessage } = this.getSignMessages(address);

const blockchains = CryptoService.getBlockchainsBasedOn(address);

if (blockchains.includes(Blockchain.LIGHTNING) && (isCustodial || /^[a-z0-9]{140,146}$/.test(signature))) {
// custodial Lightning wallet, only comparison check
return !dbSignature || signature === dbSignature;
// custodial Lightning wallet: no cryptographic check is possible, so the signature acts as a
// shared secret. On sign-up nothing is stored yet, so the first signature establishes it; on
// sign-in it must match a NON-EMPTY stored signature. An empty stored signature must never
// authenticate — otherwise any signature passes for an account whose credential was never set.
return isSignUp || (!!dbSignature && signature === dbSignature);
}

if (blockchains.includes(Blockchain.DEFICHAIN)) {
Expand Down