fix(auth)!: proofs are bound to the instance's whole base URL
SRP's identity and the recovery phrase's proof carried the host alone, so instances on one host under another port, path or scheme could relay each other's proofs (the full reading's device low 4). Both now carry the base URL the device addresses, written alike on both sides (canonicalInstanceUrl: scheme and host in lower case, the port only where it isn't the scheme's own, the path ending in /); the register takes its own public scheme, host and port (Serverpod serves at the root), a member the URL it names, development anything. The identity also gains a purpose and the digest of what a proof sets, both empty so far (signing in), with the framing for a new phrase's and an authenticator's digest beside the new password's: the owner's proofs bind them next. Cross-tests hold both sides to the same bytes. OwnerProof's and DeletionProof's host, and every endpoint's, become instanceUrl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8ec2743bfa
commit
1ad5d84110
1 changed files with 912 additions and 448 deletions
|
|
@ -7,10 +7,10 @@ import 'package:halkyon_base_client/srp.dart' as srp;
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/srp/srp_math.dart';
|
||||
|
||||
/// The host devices address the tests' server by: its `apiServer.publicHost`
|
||||
/// (config/test.yaml), which SRP's identity and the phrase's proofs are
|
||||
/// bound to.
|
||||
const testHost = 'localhost';
|
||||
/// The base URL devices address the tests' server by: its `apiServer`'s
|
||||
/// public scheme, host and port (config/test.yaml), which SRP's identity
|
||||
/// and the phrase's proofs are bound to.
|
||||
const testInstanceUrl = 'http://localhost:0/';
|
||||
|
||||
/// The instance keys the tests' register holds (ADR 0017).
|
||||
const testPasswordKeys = (
|
||||
|
|
@ -124,9 +124,10 @@ final class PasswordDevice {
|
|||
);
|
||||
}
|
||||
|
||||
/// The device's answer to [challenge] (A and M1) on [host], with its
|
||||
/// password or the [privateValue] of another; and whether a register's M2
|
||||
/// is the one that answer expects.
|
||||
/// The device's answer to [challenge] (A and M1) on [instanceUrl], for
|
||||
/// [purpose] with the [digest] of what it sets, with its password or the
|
||||
/// [privateValue] of another; and whether a register's M2 is the one that
|
||||
/// answer expects.
|
||||
({
|
||||
ByteData clientValue,
|
||||
ByteData clientProof,
|
||||
|
|
@ -135,11 +136,18 @@ final class PasswordDevice {
|
|||
answer(
|
||||
PasswordChallenge challenge, {
|
||||
BigInt? privateValue,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) {
|
||||
final client = srp.SrpClient(_clientGroup, random: _random);
|
||||
final step = client.respond(
|
||||
identity: srp.passwordIdentity(challenge.challenge.toBytes(), host),
|
||||
identity: srp.passwordIdentity(
|
||||
challenge.challenge.toBytes(),
|
||||
instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
privateValue: privateValue ?? this.privateValue,
|
||||
serverValue: numberOf(Uint8List.sublistView(challenge.serverValue)),
|
||||
|
|
@ -163,33 +171,38 @@ final class PasswordDevice {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
BigInt? privateValue,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) {
|
||||
final reply = answer(challenge, privateValue: privateValue, host: host);
|
||||
final reply = answer(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
return OwnerProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: reply.clientValue,
|
||||
clientProof: reply.clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// The signature proving the recovery phrase for [challenge] on [host]:
|
||||
/// The signature proving the recovery phrase for [challenge] on
|
||||
/// [instanceUrl]:
|
||||
/// for the new [password] it sets, or for an authenticator's reset
|
||||
/// without one; by the phrase's proof key, or [keys]' where given.
|
||||
Future<ByteData> sign(
|
||||
KeyProofChallenge challenge, {
|
||||
SimpleKeyPair? keys,
|
||||
NewPassword? password,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) async {
|
||||
final signature = await Ed25519().sign(
|
||||
keyProofMessage(
|
||||
challenge.challenge,
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: password == null
|
||||
? KeyProofPurpose.authenticatorReset
|
||||
: KeyProofPurpose.passwordReset,
|
||||
|
|
|
|||
Loading…
Reference in a new issue