[client] Give a newly added profile its identity (review item 1)

AddProfile writes the config it builds straight to disk, but built it with
createNewConfig, which stopped generating the peer's keys when identity
generation moved out of apply() into EnsureIdentity. The profile file landed
with an empty PrivateKey and SSHKey.

Nothing lost the keys permanently — the daemon's own getConfig provisions and
persists them on first use — but every reader that does not write got a
config that cannot connect in the meantime, which is exactly the set this
branch grew: the update-settings gate deciding whether to refuse a request,
and the mobile SDKs loading a stored profile.

createProvisionedConfig exists for callers that persist or connect, and this
is one; before the split, createNewConfig produced the keys here too.
This commit is contained in:
riccardom
2026-09-11 14:51:45 +02:00
parent 050c2ba7d4
commit 222ad91c4d
2 changed files with 29 additions and 1 deletions
+5 -1
View File
@@ -313,7 +313,11 @@ func (s *ServiceManager) AddProfile(displayName, username string) (*Profile, err
}
profPath := filepath.Join(configDir, id.String()+".json")
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath})
// Provisioned, not bare: this config goes straight to disk, and a profile
// file with no identity is one whose first reader has to mint the keys and
// remember to write them back. Before identity generation moved out of
// apply() into EnsureIdentity, createNewConfig produced them here too.
cfg, err := createProvisionedConfig(ConfigInput{ConfigPath: profPath})
if err != nil {
return nil, fmt.Errorf("failed to create new config: %w", err)
}
@@ -228,3 +228,27 @@ func TestRemoveProfile_DeletesStateFile(t *testing.T) {
assert.True(t, errors.Is(err, os.ErrNotExist), "state file should be removed")
})
}
// A profile file is written here and read back by whoever connects with it, so
// it has to carry the peer's identity. While AddProfile used the bare
// constructor, it wrote a config with no keys: the first reader had to mint
// them, and the paths that read without writing — a gate deciding whether to
// refuse a request, the mobile SDKs loading a stored profile — got a config
// that cannot connect.
func TestAddProfileWritesAnIdentity(t *testing.T) {
withTestSM(t, func(sm *ServiceManager, username string) {
created, err := sm.AddProfile("work", username)
require.NoError(t, err)
stored, err := GetExistingConfig(created.Path)
require.NoError(t, err)
require.NotEmpty(t, stored.PrivateKey, "the profile was written without a WireGuard key")
require.NotEmpty(t, stored.SSHKey, "the profile was written without an SSH key")
// And the identity is the one on disk, not one minted per read.
reread, err := GetExistingConfig(created.Path)
require.NoError(t, err)
require.Equal(t, stored.PrivateKey, reread.PrivateKey)
})
}