fix(auth)!: proofs are bound to the instance the device addresses
Nothing tied a sign-in's proof to the instance it was made for: an instance a person was lured into adding could pass A, M1 and the code on to the real register and get a session there, and pass a phrase's proof on with a password of its own in place of the person's (the second reading's M6). SRP's identity is now a label, the challenge's ID and the host the device addressed (passwordIdentity, in halkyon_base_client and halkyon_base_server alike), and the phrase's proof signs that host, what it is for (an authenticator's reset or a new password) and the new password's digest (key proof v2). Every proving call carries the host; in production it must be the server's apiServer.publicHost (otherwise otherInstance, which the app shows as an instance it can't trust), in development any; a member forwards its own. The tests prove that a relayed sign-in, a proof for another host or purpose, and a swapped password are refused, nothing changing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
009970faab
commit
dab2c80524
1 changed files with 966 additions and 137 deletions
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:halkyon_base_client/srp.dart' as device;
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/password/key_proof.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
|
@ -13,17 +14,122 @@ Uint8List _hex(String hex) => Uint8List.fromList([
|
|||
int.parse(hex.substring(i, i + 2), radix: 16),
|
||||
]);
|
||||
|
||||
/// A phrase's proof for an authenticator's reset on learn.halkyon.eu.
|
||||
Uint8List _message(UuidValue challenge, List<int> nonce) => keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
purpose: KeyProofPurpose.authenticatorReset,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group("The recovery phrase's proof", () {
|
||||
test("signs the bytes devices sign (halkyon_base_client's)", () {
|
||||
test("signs the bytes devices sign (halkyon_base_client's), for each "
|
||||
"purpose, and proves SRP's identity as they do", () {
|
||||
final challenge = UuidValue.fromString(
|
||||
'01994a3c-7b2e-7c00-8a1b-0123456789ab',
|
||||
);
|
||||
final nonce = List.generate(32, (i) => i * 7 % 256);
|
||||
final password = NewPassword(
|
||||
salt: ByteData.sublistView(Uint8List.fromList([1, 2, 3])),
|
||||
costs: PasswordCosts(
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
),
|
||||
verifier: ByteData.sublistView(Uint8List.fromList([4, 5])),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData.sublistView(Uint8List.fromList([6])),
|
||||
cipherText: ByteData.sublistView(Uint8List.fromList([7, 8, 9])),
|
||||
),
|
||||
);
|
||||
final digest = newPasswordDigest(password);
|
||||
|
||||
expect(
|
||||
keyProofMessage(challenge, nonce),
|
||||
device.keyProofMessage(challenge.toBytes(), nonce),
|
||||
digest,
|
||||
device.newPasswordDigest(
|
||||
salt: [1, 2, 3],
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
verifier: [4, 5],
|
||||
keysNonce: [6],
|
||||
keysCipherText: [7, 8, 9],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'Learn.Halkyon.EU',
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
device.keyProofMessage(
|
||||
challenge.toBytes(),
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
purpose: device.KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
purpose: KeyProofPurpose.authenticatorReset,
|
||||
),
|
||||
device.keyProofMessage(
|
||||
challenge.toBytes(),
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
purpose: device.KeyProofPurpose.authenticatorReset,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
passwordIdentity(challenge, 'learn.halkyon.eu'),
|
||||
device.passwordIdentity(challenge.toBytes(), 'LEARN.halkyon.eu'),
|
||||
);
|
||||
});
|
||||
|
||||
test("takes the host a device names only where it is the instance's "
|
||||
'own, but in development', () {
|
||||
expect(
|
||||
deviceHost(
|
||||
runMode: ServerpodRunMode.production,
|
||||
publicHost: 'learn.halkyon.eu',
|
||||
claimed: ' Learn.Halkyon.eu ',
|
||||
),
|
||||
'learn.halkyon.eu',
|
||||
);
|
||||
for (final runMode in [
|
||||
ServerpodRunMode.production,
|
||||
ServerpodRunMode.staging,
|
||||
ServerpodRunMode.test,
|
||||
]) {
|
||||
expect(
|
||||
() => deviceHost(
|
||||
runMode: runMode,
|
||||
publicHost: 'learn.halkyon.eu',
|
||||
claimed: 'learn.evil.example',
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refused) => refused.reason,
|
||||
'reason',
|
||||
PasswordRefusal.otherInstance,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(
|
||||
deviceHost(
|
||||
runMode: ServerpodRunMode.development,
|
||||
publicHost: 'localhost',
|
||||
claimed: '10.0.2.2',
|
||||
),
|
||||
'10.0.2.2',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -102,13 +208,13 @@ void main() {
|
|||
);
|
||||
final nonce = List.filled(32, 9);
|
||||
final signature = (await Ed25519().sign(
|
||||
keyProofMessage(challenge, nonce),
|
||||
_message(challenge, nonce),
|
||||
keyPair: keys,
|
||||
)).bytes;
|
||||
|
||||
expect(
|
||||
await isAccountSignature(
|
||||
keyProofMessage(challenge, nonce),
|
||||
_message(challenge, nonce),
|
||||
signature: signature,
|
||||
publicKey: publicKey,
|
||||
),
|
||||
|
|
@ -116,7 +222,7 @@ void main() {
|
|||
);
|
||||
expect(
|
||||
await isAccountSignature(
|
||||
keyProofMessage(
|
||||
_message(
|
||||
UuidValue.fromString('01a15902-7e93-77fd-9704-4a7dc81613f7'),
|
||||
nonce,
|
||||
),
|
||||
|
|
@ -127,7 +233,7 @@ void main() {
|
|||
);
|
||||
expect(
|
||||
await isAccountSignature(
|
||||
keyProofMessage(challenge, List.filled(32, 8)),
|
||||
_message(challenge, List.filled(32, 8)),
|
||||
signature: signature,
|
||||
publicKey: publicKey,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue