using System.Runtime.Versioning; using DodoSSH.Client.App.Platform; using DodoSSH.Client.Session; namespace DodoSSH.Client.App.Tests; /// /// The real TPM-backed store, as far as it can be exercised without a person. Which is not far. /// /// /// /// Two tests, and the reason there are only two is a measured finding. A key created under /// CngUIProtectionLevels.ProtectKey prompts at creation, not only at use: the policy means /// "protect this key with a PIN", so Windows asks the user to set that up when the key is made. Sealing /// therefore prompts as well as opening, even though sealing needs only the public half. /// /// /// So anything that calls SaveAsync, TryLoadAsync with a blob present, or ForgetAsync /// after a save will block a suite forever waiting for somebody to enter a PIN. That was found by writing /// those tests and watching the run hang for ten minutes. They are gone; what is left is the two paths that /// provably reach no dialog. /// /// /// The rest of this store is verified by using the application, and that is not a gap this file can close — /// a consent dialog needs hardware and a person by design. Disabling the UI policy to make it testable /// would be testing a different class, and the one property worth having would be the property removed. /// /// [SupportedOSPlatform("windows")] public sealed class WindowsDeviceKeyStoreTests { private static CancellationToken Token => TestContext.Current.CancellationToken; /// /// The PIN prompt this platform never reaches, because Windows raises its own. /// /// /// ForThisMachine takes one so that the Linux store has somewhere to ask from — see /// LinuxDeviceKeyStore. On Windows nothing calls it, and answering null rather than throwing is /// the honest stand-in: if the selection ever did reach for it here, the result would be a store that /// declines rather than a test that fails somewhere unrelated. /// private static ValueTask NoPin(DevicePinPurpose purpose, CancellationToken cancellationToken) => ValueTask.FromResult(null); [Fact] public async Task OnAMachineWithATpm_TheStoreOffersItself() { // IsSupported probes with a throwaway key carrying no UI policy, which is why this one is safe to // run: no policy, no dialog. It is also the only honest availability test, because the platform // provider reports itself present on machines where creating a key then fails. SkipUnlessSupported(); var store = DesktopDeviceKeyStores.ForThisMachine(new ClientPaths(Path.GetTempPath()), NoPin); (await store.ShouldBeOfType().Chosen).ShouldBeOfType(); (await store.IsAvailableAsync(Token)).ShouldBeTrue(); } [Fact] public async Task WithNothingSaved_LoadingReturnsNullWithoutPrompting() { // Reaches no dialog because it returns on the missing file, before touching the TPM at all. That is // also what keeps a fresh machine's unlock screen quiet: it must not prompt for a key it has never // been given. If this test ever hangs, that ordering has been lost. SkipUnlessSupported(); var directory = Path.Combine(Path.GetTempPath(), $"dodossh-devicekey-{Guid.CreateVersion7():N}"); var store = new WindowsDeviceKeyStore(new ClientPaths(directory)); (await store.TryLoadAsync(Token)).ShouldBeNull(); // Nothing was created, so there is nothing to clean up — asserted, because a store that wrote a // directory just to answer "no" would be leaving litter on every launch of an unregistered machine. Directory.Exists(directory).ShouldBeFalse(); } private static void SkipUnlessSupported() { var supported = OperatingSystem.IsWindows() && WindowsDeviceKeyStore.IsSupported(); if (!supported) { Assert.Skip("This machine has no TPM the platform crypto provider will hold a key in."); } } }