Implement Windows named pipes on The Annoying OS
Marco Ricci

Marco Ricci commited on 2025-12-14 17:21:00
Zeige 2 geänderte Dateien mit 509 Einfügungen und 43 Löschungen.


Using the `ctypes` module, call into The Annoying OS's system libraries
and bind the functions necessary to create, read from/write to, and
close handles to existing Windows named pipes.  We also bind the
functions necessary to compute the pipe name for PuTTY/Pageant, and
provide convenience functions to connect to PuTTY/Pageant and to
OpenSSH, the de facto main SSH implementations on The Annoying OS.

One major design question remains: how to discover the (named pipe
address for) the SSH agent to use?  Tempting options are using
`SSH_AUTH_SOCK` again (whether literally or with special notation), and
listing the preferred agent addresses in the user configuration file.

For certain "well-known" addresses such as PuTTY/Pageant or OpenSSH, we
provide specific SSH agent socket provider registry entries that attempt
to connect to the respective agent. Other applications could easily
register a custom provider that respects their configuration if the
final socket (address) depends on external configuration.  It is thus
not clear to me if the system needs to be more flexible than it
currently is, e.g., if the SSH agent socket provider needs to accept
arguments that further configure the address or the connection options
to the socket or named pipe.

The formal name for this facility is "Windows named pipe", and this is
the name other developers will expect.  Thus, any mention of The
Annoying OS in the context of Windows named pipes will strictly be
called "Windows named pipe", not "The Annoying OS named pipe" or
similar.  (Some pre-existing names were adapted accordingly.)
Similarly, the other facility is called "UNIX domain socket", not
"AF_UNIX" or "AfUnix" or somesuch.

This commit contains no specific test code for this functionality; we
leave this to follow-up commits.  For ease of integration with the
existing test suite, *in this commit* we attempt to use `SSH_AUTH_SOCK`
directly as the named pipe's name (which will fail unless specifically
prepared).

Since many of the symbols are undefined on other operating systems, and
because the `ctypes` library relies on dynamically generated attributes
and is thus mostly invisible to static analysis tools, much of the code
needs type checking exemptions and extraneous explicit (C) casts at the
Python level.  Additionally, because our stub functions use the same
CamelCase naming as The Annoying OS's official documentation does, much
of the code also needs linting exceptions for the naming policy.
... ...
@@ -741,12 +741,14 @@ def handle_notimplementederror(
741 741
     """  # noqa: DOC201
742 742
 
743 743
     def handle_notimplementederror(excgroup: BaseExceptionGroup) -> NoReturn:
744
-        if excgroup.subgroup(socketprovider.AfUnixNotAvailableError):
744
+        if excgroup.subgroup(
745
+            socketprovider.UnixDomainSocketsNotAvailableError
746
+        ):
745 747
             warning_callback(
746 748
                 _msg.TranslatedString(_msg.WarnMsgTemplate.NO_AF_UNIX)
747 749
             )
748 750
         if excgroup.subgroup(
749
-            socketprovider.TheAnnoyingOsNamedPipesNotAvailableError
751
+            socketprovider.WindowsNamedPipesNotAvailableError
750 752
         ):
751 753
             warning_callback(
752 754
                 _msg.TranslatedString(
... ...
@@ -8,15 +8,50 @@ from __future__ import annotations
8 8
 
9 9
 import collections
10 10
 import ctypes
11
+import enum
12
+import errno
13
+import hashlib
11 14
 import os
12 15
 import socket
13
-from typing import TYPE_CHECKING, ClassVar, cast
16
+from ctypes.wintypes import (  # type: ignore[attr-defined]
17
+    BOOL,
18
+    DWORD,
19
+    HANDLE,
20
+    LPCVOID,
21
+    LPCWSTR,
22
+    LPDWORD,
23
+    LPVOID,
24
+)
25
+from typing import TYPE_CHECKING, cast
14 26
 
15 27
 if TYPE_CHECKING:
16 28
     from collections.abc import Callable
29
+    from typing import ClassVar
30
+
31
+    from typing_extensions import Any, Buffer, Literal, Self
17 32
 
18 33
     from derivepassphrase import _types
19 34
 
35
+    kernel32: ctypes.WinDLL  # type: ignore[name-defined]
36
+    crypt32: ctypes.WinDLL  # type: ignore[name-defined]
37
+
38
+    CryptProtectMemory: Callable[[LPVOID, DWORD, DWORD], BOOL]
39
+    CreateFile: Callable[
40
+        [
41
+            LPCWSTR,
42
+            DWORD,
43
+            DWORD,
44
+            None,
45
+            DWORD,
46
+            DWORD,
47
+            HANDLE | None,
48
+        ],
49
+        HANDLE,
50
+    ]
51
+    ReadFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, None], BOOL]
52
+    WriteFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, None], BOOL]
53
+    CloseHandle: Callable[[HANDLE], BOOL]
54
+
20 55
 __all__ = ("SocketProvider",)
21 56
 
22 57
 
... ...
@@ -24,12 +59,384 @@ class NoSuchProviderError(KeyError):
24 59
     """No such SSH agent socket provider is known."""
25 60
 
26 61
 
27
-class AfUnixNotAvailableError(NotImplementedError):
28
-    """This Python installation does not support socket.AF_UNIX."""
62
+class UnixDomainSocketsNotAvailableError(NotImplementedError):
63
+    """This Python installation does not support UNIX domain sockets.
64
+
65
+    On this installation, the standard library symbol
66
+    [`socket.AF_UNIX`][] is not available, necessary to create UNIX
67
+    domain sockets.
68
+
69
+    """
70
+
71
+
72
+class WindowsNamedPipesNotAvailableError(NotImplementedError):
73
+    """This Python installation does not support Windows named pipes.
74
+
75
+    On this installation, the standard library symbol
76
+    [`ctypes.WinDLL`][] is not available, necessary to interface with
77
+    the Annoying Operating System's standard library to create Windows
78
+    named pipes.
79
+
80
+    """
81
+
82
+
83
+GENERIC_READ = 0x80000000
84
+GENERIC_WRITE = 0x40000000
85
+
86
+FILE_SHARE_READ = 0x00000001
87
+FILE_SHARE_WRITE = 0x00000002
88
+
89
+OPEN_EXISTING = 0x3
90
+
91
+INVALID_HANDLE_VALUE = -1
92
+
93
+CRYPTPROTECTMEMORY_BLOCK_SIZE = 0x10
94
+CRYPTPROTECTMEMORY_CROSS_PROCESS = 0x1
95
+
96
+PIPE_PREFIX = "//./pipe/".replace("/", "\\")
97
+
98
+
99
+# The type checker and the standard library use different
100
+# (finer-grained?) types and type definitions on The Annoying Operating
101
+# System to annotate things derived from ctypes.WinDLL.  Furthermore, on
102
+# other OSes, ctypes.WinDLL is missing entirely.  So the type checker
103
+# attempts to validate our code against *very* different type
104
+# hierarchies, depending on what OS it is running on, and so we require
105
+# a lot of type checking exclusions to keep the types reasonably narrow.
106
+#
107
+# Additionally, the official documentation for kernel32.dll and
108
+# crypt32.dll (and The Annoying OS in general) uses CamelCase for both
109
+# function names and function parameters.  This clashes with the
110
+# standard Python naming conventions, so we need linting exclusions for
111
+# each such name.
112
+#
113
+# Finally, since this code is mostly platform-specific, we use coverage
114
+# pragmas for the respective branches to avoid surprises about missing
115
+# coverage.
116
+#
117
+# All these factors necessitate *a lot* of "type: ignore", "noqa:" and
118
+# "pragma: ... no cover" comments below.
119
+
120
+try:  # pragma: unless the-annoying-os no cover
121
+    import ctypes
122
+
123
+    crypt32 = ctypes.WinDLL("crypt32.dll")  # type: ignore[attr-defined]
124
+    CryptProtectMemory = crypt32.CryptProtectMemory  # type: ignore[attr-defined]
125
+except (
126
+    AttributeError,
127
+    FileNotFoundError,
128
+    ImportError,
129
+):  # pragma unless posix no cover
130
+
131
+    def CryptProtectMemory(  # noqa: N802
132
+        pDataIn: LPVOID,  # noqa: N803
133
+        cbDataIn: DWORD,  # noqa: N803
134
+        dwFlags: DWORD,  # noqa: N803
135
+    ) -> BOOL:  # pragma: no cover [external]
136
+        raise NotImplementedError
137
+
138
+else:  # pragma: unless the-annoying-os no cover
139
+    CryptProtectMemory.argtypes = [LPVOID, DWORD, DWORD]  # type: ignore[attr-defined]
140
+    CryptProtectMemory.restype = BOOL  # type: ignore[attr-defined]
141
+
142
+
143
+def _errcheck(
144
+    result: int,
145
+    func: Callable,
146
+    arguments: Any,  # noqa: ANN401
147
+) -> int:  # pragma: no cover [external]
148
+    del func
149
+    del arguments
150
+    # Even if they have the same value, ctypes pointers don't
151
+    # compare equal to each other.  We need to compare the values
152
+    # explicitly, keeping the ranges in mind as well.
153
+    result_value = HANDLE(result).value
154
+    invalid_value = HANDLE(INVALID_HANDLE_VALUE).value
155
+    if result_value == invalid_value:
156
+        raise ctypes.WinError()  # type: ignore[attr-defined]
157
+    assert result_value is not None  # for the type checker
158
+    return result_value
159
+
160
+
161
+try:  # pragma: unless the-annoying-os no cover
162
+    kernel32 = ctypes.WinDLL("Kernel32.dll")  # type: ignore[attr-defined]
163
+    CreateFile = kernel32.CreateFileW  # type: ignore[attr-defined]
164
+    ReadFile = kernel32.ReadFile  # type: ignore[attr-defined]
165
+    WriteFile = kernel32.WriteFile  # type: ignore[attr-defined]
166
+    CloseHandle = kernel32.CloseHandle  # type: ignore[attr-defined]
167
+except (
168
+    AttributeError,
169
+    FileNotFoundError,
170
+):  # pragma: unless posix no cover
171
+
172
+    def CreateFile(  # noqa: N802, PLR0913, PLR0917
173
+        lpFilename: LPCWSTR,  # noqa: N803
174
+        dwDesiredAccess: DWORD,  # noqa: N803
175
+        dwShareMode: DWORD,  # noqa: N803
176
+        lpSecurityAttributes: None,  # noqa: N803
177
+        dwCreationDisposition: DWORD,  # noqa: N803
178
+        dwFlagsAndAttributes: DWORD,  # noqa: N803
179
+        hTemplateFile: HANDLE | None,  # noqa: N803
180
+    ) -> HANDLE:  # pragma: no cover [external]
181
+        del (
182
+            lpFilename,
183
+            dwDesiredAccess,
184
+            dwShareMode,
185
+            lpSecurityAttributes,
186
+            dwCreationDisposition,
187
+            dwFlagsAndAttributes,
188
+            hTemplateFile,
189
+        )
190
+        msg = "This Python version does not support Windows named pipes."
191
+        raise WindowsNamedPipesNotAvailableError(msg)
192
+
193
+    def ReadFile(  # noqa: N802
194
+        hFile: HANDLE,  # noqa: N803
195
+        lpBuffer: LPVOID,  # noqa: N803
196
+        nNumberOfBytesToRead: DWORD,  # noqa: N803
197
+        lpNumberOfBytesRead: LPDWORD,  # noqa: N803
198
+        lpOverlapped: None,  # noqa: N803
199
+    ) -> BOOL:  # pragma: no cover [external]
200
+        del (
201
+            hFile,
202
+            lpBuffer,
203
+            nNumberOfBytesToRead,
204
+            lpNumberOfBytesRead,
205
+            lpOverlapped,
206
+        )
207
+        raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))
208
+
209
+    def WriteFile(  # noqa: N802
210
+        hFile: HANDLE,  # noqa: N803
211
+        lpBuffer: LPVOID,  # noqa: N803
212
+        nNumberOfBytesToWrite: DWORD,  # noqa: N803
213
+        lpNumberOfBytesWritten: LPDWORD,  # noqa: N803
214
+        lpOverlapped: None,  # noqa: N803
215
+    ) -> BOOL:  # pragma: no cover [external]
216
+        del (
217
+            hFile,
218
+            lpBuffer,
219
+            nNumberOfBytesToWrite,
220
+            lpNumberOfBytesWritten,
221
+            lpOverlapped,
222
+        )
223
+        raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))
224
+
225
+else:  # pragma: unless the-annoying-os no cover
226
+    CreateFile.argtypes = [  # type: ignore[attr-defined]
227
+        LPCWSTR,
228
+        DWORD,
229
+        DWORD,
230
+        # Actually, LPSECURITY_ATTRIBUTES, but we always pass
231
+        # None/NULL.
232
+        ctypes.c_void_p,
233
+        DWORD,
234
+        DWORD,
235
+        # We always pass None/NULL.
236
+        HANDLE,
237
+    ]
238
+    CreateFile.restype = HANDLE  # type: ignore[attr-defined]
239
+    CreateFile.errcheck = _errcheck  # type: ignore[assignment, attr-defined]
240
+    ReadFile.argtypes = [  # type: ignore[attr-defined]
241
+        HANDLE,
242
+        LPVOID,
243
+        DWORD,
244
+        LPDWORD,
245
+        # Actually, LPOVERLAPPED, but we always pass None/NULL.
246
+        ctypes.c_void_p,
247
+    ]
248
+    WriteFile.argtypes = [  # type: ignore[attr-defined]
249
+        HANDLE,
250
+        LPVOID,
251
+        DWORD,
252
+        LPDWORD,
253
+        # Actually, LPOVERLAPPED, but we always pass None/NULL.
254
+        ctypes.c_void_p,
255
+    ]
256
+    CloseHandle.argtypes = [HANDLE]  # type: ignore[attr-defined]
257
+    ReadFile.restype = BOOL  # type: ignore[attr-defined]
258
+    WriteFile.restype = BOOL  # type: ignore[attr-defined]
259
+    CloseHandle.restype = BOOL  # type: ignore[attr-defined]
260
+
261
+
262
+class WindowsNamedPipeHandle:
263
+    """A Windows named pipe handle.
264
+
265
+    This handle implements the [`SSHAgentSocket`][_types.SSHAgentSocket]
266
+    interface.  It is only constructable if the Python installation can
267
+    successfully call into the Annoying OS `kernel32.dll` library via
268
+    [`ctypes`][].
269
+
270
+    """
271
+
272
+    _IO_ON_CLOSED_FILE = "I/O operation on closed file."
273
+
274
+    def __init__(self, name: str) -> None:
275
+        """Create a named pipe handle.
276
+
277
+        Args:
278
+            name:
279
+                The named pipe's name.
280
+
281
+        Raises:
282
+            OSError:
283
+                The system failed to open the named pipe.
284
+            ValueError:
285
+                The named pipe's name does not designate a named pipe.
286
+            WindowsNamedPipesNotAvailableError:
287
+                This Python version does not support Windows named
288
+                pipes.
289
+
290
+        """
291
+        if not name.replace("/", "\\").startswith(PIPE_PREFIX):
292
+            raise ValueError(errno.EINVAL, os.strerror(errno.EINVAL))
293
+        self.handle: HANDLE | None = CreateFile(
294
+            ctypes.c_wchar_p(name),
295
+            ctypes.c_ulong(GENERIC_READ | GENERIC_WRITE),
296
+            ctypes.c_ulong(FILE_SHARE_READ | FILE_SHARE_WRITE),
297
+            None,
298
+            ctypes.c_ulong(OPEN_EXISTING),
299
+            ctypes.c_ulong(0),
300
+            None,
301
+        )
302
+
303
+    def __enter__(self) -> Self:
304
+        return self
305
+
306
+    def __exit__(self, *args: object) -> Literal[False]:
307
+        try:
308
+            if self.handle is not None:
309
+                CloseHandle(self.handle)
310
+            return False
311
+        finally:
312
+            self.handle = None
313
+
314
+    def recv(self, data: int, flags: int = 0, /) -> bytes:
315
+        if self.handle is None:
316
+            raise ValueError(self._IO_ON_CLOSED_FILE)
317
+        del flags
318
+        result = bytearray()
319
+        read_count = DWORD(0)
320
+        buffer = (ctypes.c_char * 65536)()
321
+        while data > 0:
322
+            block_size = min(max(0, data), 65536)
323
+            success = ReadFile(
324
+                self.handle,
325
+                ctypes.cast(ctypes.byref(buffer), ctypes.c_void_p),
326
+                DWORD(block_size),
327
+                ctypes.cast(ctypes.byref(read_count), LPDWORD),
328
+                None,
329
+            )
330
+            if (
331
+                not success or read_count.value == 0
332
+            ):  # pragma: no cover [external]
333
+                raise ctypes.WinError()  # type: ignore[attr-defined]
334
+            result.extend(buffer.raw[:block_size])
335
+            data -= read_count.value
336
+            read_count.value = 0
337
+        return bytes(result)
338
+
339
+    def sendall(self, data: Buffer, flags: int = 0, /) -> None:
340
+        if self.handle is None:
341
+            raise ValueError(self._IO_ON_CLOSED_FILE)
342
+        del flags
343
+        data = bytes(data)
344
+        databuf = (ctypes.c_char * len(data))()
345
+        for i, x in enumerate(data):
346
+            databuf[i] = ctypes.c_char(x)
347
+        write_count = DWORD(0)
348
+        WriteFile(
349
+            self.handle,
350
+            ctypes.cast(ctypes.byref(databuf), ctypes.c_void_p),
351
+            DWORD(len(data)),
352
+            ctypes.cast(ctypes.byref(write_count), LPDWORD),
353
+            None,
354
+        )
355
+
356
+    @classmethod
357
+    def for_openssh(cls) -> Self:
358
+        """Construct a named pipe for use with OpenSSH on The Annoying OS.
359
+
360
+        Returns:
361
+            A new named pipe handle, using OpenSSH's pipe name.
362
+
363
+        """
364
+        return cls(f"{PIPE_PREFIX}openssh-ssh-agent")
365
+
366
+    @classmethod
367
+    def for_pageant(cls) -> Self:
368
+        """Construct a named pipe for use with Pageant.
369
+
370
+        Returns:
371
+            A new named pipe handle, using Pageant's pipe name.
372
+
373
+        """
374
+        return cls(cls.pageant_named_pipe_name())
375
+
376
+    @staticmethod
377
+    def pageant_named_pipe_name(
378
+        *,
379
+        require_cryptprotectmemory: bool = False,
380
+    ) -> str:
381
+        """Return the pipe name that Pageant on The Annoying OS would use.
382
+
383
+        Args:
384
+            require_cryptprotectmemory:
385
+                Pageant normally attempts to use the
386
+                `CryptProtectMemory` system function from the
387
+                `crypt32.dll` library on The Annoying OS, ignoring any
388
+                errors.  This in turn influences the resulting named
389
+                pipe's filename.
29 390
 
391
+                If true, and if we fail to call `CryptProtectMemory`, we
392
+                abort; otherwise we ignore any errors from calling or
393
+                failing to call `CryptProtectMemory`.
394
+
395
+        Raises:
396
+            NotImplementedError:
397
+                `require_cryptprotectmemory` was `True`, but this Python
398
+                version cannot call `CryptProtectMemory` due to lack of
399
+                system support.
400
+
401
+        """
402
+        realname = b"Pageant\x00"
403
+        num_blocks = (
404
+            len(realname) + CRYPTPROTECTMEMORY_BLOCK_SIZE - 1
405
+        ) // CRYPTPROTECTMEMORY_BLOCK_SIZE
406
+        assert num_blocks == 1
407
+        buf_decl = ctypes.c_char * (CRYPTPROTECTMEMORY_BLOCK_SIZE * num_blocks)
408
+        buf = buf_decl()
409
+        for i, x in enumerate(realname):
410
+            buf[i] = ctypes.c_char(x)
411
+        try:
412
+            CryptProtectMemory(
413
+                ctypes.cast(ctypes.byref(buf), ctypes.c_void_p),
414
+                DWORD(len(buf.raw)),
415
+                DWORD(CRYPTPROTECTMEMORY_CROSS_PROCESS),
416
+            )
417
+        except NotImplementedError:  # pragma: no cover [external]
418
+            if require_cryptprotectmemory:
419
+                raise
420
+        buf_as_ssh_string = bytearray()
421
+        buf_as_ssh_string.extend(int.to_bytes(len(buf.raw), 4, "big"))
422
+        buf_as_ssh_string.extend(buf.raw)
423
+        digest = hashlib.sha256(buf_as_ssh_string).hexdigest()
424
+        return f"{PIPE_PREFIX}pageant.{os.getlogin()}.{digest}"
30 425
 
31
-class TheAnnoyingOsNamedPipesNotAvailableError(NotImplementedError):
32
-    """This Python installation does not support Windows named pipes."""
426
+
427
+class _WindowsNamedPipeSocketAddress(enum.Enum):
428
+    """Internal enum for the socket address for Windows named pipes.
429
+
430
+    Attributes:
431
+        PAGEANT: The dynamic address for PuTTY/Pageant.
432
+        OPENSSH: The static address for OpenSSH-on-Windows.
433
+
434
+    """
435
+
436
+    PAGEANT = "PuTTY/Pageant on The Annoying OS"
437
+    """"""
438
+    OPENSSH = "OpenSSH on The Annoying OS"
439
+    """"""
33 440
 
34 441
 
35 442
 class SocketProvider:
... ...
@@ -52,13 +459,11 @@ class SocketProvider:
52 459
         Raises:
53 460
             KeyError:
54 461
                 The `SSH_AUTH_SOCK` environment variable was not found.
55
-            AfUnixNotAvailableError:
56
-                [This Python version does not support UNIX domain
57
-                sockets][AF_UNIX], necessary to automatically connect to
462
+            UnixDomainSocketsNotAvailableError:
463
+                This Python version does not support UNIX domain
464
+                sockets, necessary to automatically connect to
58 465
                 a running SSH agent via the `SSH_AUTH_SOCK` environment
59 466
                 variable.
60
-
61
-                [AF_UNIX]: https://docs.python.org/3/library/socket.html#socket.AF_UNIX
62 467
             OSError:
63 468
                 There was an error setting up a socket connection to the
64 469
                 agent.
... ...
@@ -66,7 +471,7 @@ class SocketProvider:
66 471
         """
67 472
         if not hasattr(socket, "AF_UNIX"):
68 473
             msg = "This Python version does not support UNIX domain sockets."
69
-            raise AfUnixNotAvailableError(msg)
474
+            raise UnixDomainSocketsNotAvailableError(msg)
70 475
         else:  # noqa: RET506  # pragma: unless posix no cover
71 476
             sock = socket.socket(family=socket.AF_UNIX)
72 477
             if "SSH_AUTH_SOCK" not in os.environ:
... ...
@@ -78,35 +483,92 @@ class SocketProvider:
78 483
             return sock
79 484
 
80 485
     @staticmethod
81
-    def the_annoying_os_named_pipes() -> _types.SSHAgentSocket:
82
-        """Return a socket connected to Pageant/OpenSSH on The Annoying OS.
83
-
84
-        This may be a write-through socket if the underlying connection
85
-        to Pageant or OpenSSH does not use an actual network socket to
86
-        communicate.
486
+    def _the_annoying_os_named_pipe(
487
+        pipe_name: _WindowsNamedPipeSocketAddress | str,
488
+    ) -> _types.SSHAgentSocket:
489
+        """Return a socket wrapper around a Windows named pipe.
87 490
 
88 491
         Raises:
89
-            TheAnnoyingOsNamedPipesNotAvailableError:
90
-                This functionality is not implemented yet.
91
-
92
-        Warning: Not implemented yet
93
-            This functionality is not implemented yet.  Specifically,
94
-            [we do not yet support any of the communication mechanisms
95
-            used by the leading SSH agent
96
-            implementations.][windows-ssh-agent-support]
97
-
98
-            [windows-ssh-agent-support]: https://the13thletter.info/derivepassphrase/0.x/wishlist/windows-ssh-agent-support/
492
+            OSError:
493
+                There was an error setting up a connection to the agent.
494
+            WindowsNamedPipesNotAvailableError:
495
+                This Python version does not support Windows named
496
+                pipes.
99 497
 
100 498
         """
101 499
         if not hasattr(ctypes, "WinDLL"):
102 500
             msg = "This Python version does not support Windows named pipes."
103
-            raise TheAnnoyingOsNamedPipesNotAvailableError(msg)
501
+            raise WindowsNamedPipesNotAvailableError(msg)
104 502
         else:  # noqa: RET506  # pragma: unless the-annoying-os no cover
105
-            msg = (
106
-                "Communicating with Pageant or OpenSSH on Windows "
107
-                "is not implemented yet."
503
+            # TODO(the-13th-letter): Rewrite using structural pattern
504
+            # matching.
505
+            # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
506
+            if pipe_name == _WindowsNamedPipeSocketAddress.PAGEANT:
507
+                return WindowsNamedPipeHandle.for_pageant()
508
+            if pipe_name == _WindowsNamedPipeSocketAddress.OPENSSH:
509
+                return WindowsNamedPipeHandle.for_openssh()
510
+            return WindowsNamedPipeHandle(pipe_name)
511
+
512
+    @classmethod
513
+    def the_annoying_os_named_pipe_for_pageant(cls) -> _types.SSHAgentSocket:
514
+        """Return a socket wrapper connected to Pageant on The Annoying OS.
515
+
516
+        Raises:
517
+            OSError:
518
+                There was an error setting up a connection to Pageant.
519
+            WindowsNamedPipesNotAvailableError:
520
+                This Python version does not support Windows named
521
+                pipes.
522
+
523
+        """
524
+        return cls._the_annoying_os_named_pipe(
525
+            _WindowsNamedPipeSocketAddress.PAGEANT
108 526
         )
109
-            raise NotImplementedError(msg)
527
+
528
+    @classmethod
529
+    def the_annoying_os_named_pipe_for_openssh(cls) -> _types.SSHAgentSocket:
530
+        """Return a socket wrapper connected to the OpenSSH agent on The Annoying OS.
531
+
532
+        Raises:
533
+            OSError:
534
+                There was an error setting up a connection to the OpenSSH
535
+                agent.
536
+            WindowsNamedPipesNotAvailableError:
537
+                This Python version does not support Windows named
538
+                pipes.
539
+
540
+        """  # noqa: E501
541
+        return cls._the_annoying_os_named_pipe(
542
+            _WindowsNamedPipeSocketAddress.OPENSSH
543
+        )
544
+
545
+    @classmethod
546
+    def the_annoying_os_named_pipe_ssh_auth_sock(cls) -> _types.SSHAgentSocket:
547
+        r"""Return a socket wrapper connected to the agent in `SSH_AUTH_SOCK`.
548
+
549
+        The `SSH_AUTH_SOCK` environment variable is assumed to contain
550
+        a valid named pipe name, i.e., a path starting with `\\.\pipe\`
551
+        or `//./pipe/`.
552
+
553
+        Raises:
554
+            KeyError:
555
+                The `SSH_AUTH_SOCK` environment variable was not found.
556
+            ValueError:
557
+                The path in `SSH_AUTH_SOCK` clearly does not name a valid
558
+                named pipe name.
559
+            OSError:
560
+                There was an error setting up a connection to the OpenSSH
561
+                agent.
562
+            WindowsNamedPipesNotAvailableError:
563
+                This Python version does not support Windows named
564
+                pipes.
565
+
566
+        """
567
+        address = os.environ["SSH_AUTH_SOCK"]
568
+        if not address.replace("/", "\\").startswith(PIPE_PREFIX):
569
+            msg = f"Invalid named pipe name: {address!r}"
570
+            raise ValueError(msg)
571
+        return cls._the_annoying_os_named_pipe(address)
110 572
 
111 573
     registry: ClassVar[
112 574
         dict[str, _types.SSHAgentSocketProvider | str | None]
... ...
@@ -337,21 +799,23 @@ class SocketProvider:
337 799
 
338 800
 
339 801
 SocketProvider.registry.update({
340
-    "posix": SocketProvider.unix_domain_ssh_auth_sock,
341
-    "the_annoying_os": SocketProvider.the_annoying_os_named_pipes,
802
+    "ssh_auth_sock_on_posix": SocketProvider.unix_domain_ssh_auth_sock,
803
+    "pageant_on_the_annoying_os": SocketProvider.the_annoying_os_named_pipe_for_pageant,  # noqa: E501
804
+    "openssh_on_the_annoying_os": SocketProvider.the_annoying_os_named_pipe_for_openssh,  # noqa: E501
805
+    "ssh_auth_sock_on_the_annoying_os": SocketProvider.the_annoying_os_named_pipe_ssh_auth_sock,  # noqa: E501
342 806
     # known instances
343 807
     "stub_agent": None,
344 808
     "stub_with_address": None,
345 809
     "stub_with_address_and_deterministic_dsa": None,
346 810
     # aliases
347
-    "native": "the_annoying_os" if os.name == "nt" else "posix",
348
-    "unix_domain": "posix",
349
-    "ssh_auth_sock": "posix",
811
+    "posix": "ssh_auth_sock_on_posix",
812
+    "ssh_auth_sock": "ssh_auth_sock_on_posix",
813
+    "unix_domain": "ssh_auth_sock_on_posix",
814
+    "the_annoying_os": "pageant_on_the_annoying_os",
350 815
     "the_annoying_os_named_pipe": "the_annoying_os",
351
-    "pageant_on_the_annoying_os": "the_annoying_os",
352
-    "openssh_on_the_annoying_os": "the_annoying_os",
353 816
     "windows": "the_annoying_os",
354 817
     "windows_named_pipe": "the_annoying_os",
355
-    "pageant_on_windows": "the_annoying_os",
356
-    "openssh_on_windows": "the_annoying_os",
818
+    "pageant_on_windows": "pageant_on_the_annoying_os",
819
+    "openssh_on_windows": "openssh_on_the_annoying_os",
820
+    "native": "the_annoying_os" if os.name == "nt" else "posix",
357 821
 })
358 822