ddn.net.ssh.key_files

SSH private key file loaders (unencrypted formats only).

Loads explicitly supported unencrypted private key containers and adapts them to the SshSigner interface used by SshSession.authenticatePublicKey. The supported container and key combinations are:

  • openssh-key-v1 with cipher/KDF none

    (PEM label "OPENSSH PRIVATE KEY"; also accepted without armor). Carries ssh-ed25519, ssh-rsa, and ecdsa-sha2-nistp256 keys; exactly one key per file is supported.

  • PKCS#1 RSAPrivateKey (PEM label "RSA PRIVATE KEY"

    or raw DER).

  • SEC 1 ECPrivateKey (PEM label "EC PRIVATE KEY" or

    raw DER), restricted to P-256.

  • Unencrypted PKCS#8 PrivateKeyInfo (PEM label

    "PRIVATE KEY" or raw DER) carrying RSA, Ed25519, or ECDSA P-256 keys.

Encrypted containers are governed by the passphrase contract: a caller may supply an SshPassphraseProvider callback that acquires the passphrase synchronously (key loading never runs on the pump path). With a provider, encrypted openssh-key-v1 files using the dominant aes256-ctr cipher with the bcrypt KDF and encrypted PKCS#8 files using PBES2 with PBKDF2 (HMAC-SHA-1/-SHA-256/-SHA-512) and AES-CBC are decrypted in-process; other encrypted-key-v1 ciphers (including the aes256-gcm@openssh.com default of OpenSSH 9.5 and later) and other PBES2 schemes fail with SshKeyEncryptionException. Wrong passphrases are retried a bounded number of times through SshKeyPassphraseException. Without a provider, every encrypted container — encrypted openssh-key-v1, encrypted PKCS#8 ("ENCRYPTED PRIVATE KEY" / PBES2, and legacy PEM encryption (Proc-Type/DEK-Info) — fails with SshKeyEncryptionException so callers can direct the user at ssh-add/agent-based workflows instead. Unknown labels and structures fail with SshKeyException.

Loaded keys are validated before use: public/private consistency is checked cryptographically (Ed25519 public keys are re-derived from the private material and must match the container; RSA requires n == p * q), and RSA moduli below SshKeyLimits.rsaMinBits are rejected. Private ECDSA containers are rejected before scalar decoding.

Secret hygiene: every secret buffer this module allocates — the KDF-derived key material, the cipher state, and the decrypted private section — is explicitly zeroized once parsing completes. The passphrase itself arrives as an immutable D string and the loaded key material is copied into long-lived key objects, so perfect scrubbing is impossible under D's GC; this residual-copy risk is accepted and documented rather than hidden.

Types 7

Raised when a private key file uses encryption.

Encrypted containers are explicitly out of scope: OpenSSH encryption requires bcrypt_pbkdf and legacy PEM/PKCS#8 encryption is a separate feature. The distinct type lets callers direct users at agent-based workflows without string matching.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Constructs an encryption-not-supported exception.

Raised when passphrase-protected key material cannot be unlocked.

This is the retry-loop signal: an encrypted container reports a rejected passphrase by throwing this type, and the loader asks the provider again until maxPassphraseAttempts is exhausted. It derives from SshKeyEncryptionException so callers written against the older single-exception surface keep working.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Constructs a passphrase-failure exception.

Context handed to an SshPassphraseProvider when an encrypted private key file needs its passphrase.

Fields
uint attemptOne-based number of this attempt; a value above 1 means the previous passphrase was rejected and the user should be told so.
string containerHuman-readable container description, for example "openssh-key-v1 (aes256-ctr") or "PKCS#8 PBES2 (AES-256-CBC").
aliasSshPassphraseProvider = string delegate( scope const SshPassphraseInfo info)

Synchronously acquires the passphrase for an encrypted key file.

Returns

The passphrase as UTF-8 text, or null to decline

(for example when the user cancels a prompt), which aborts loading with SshKeyPassphraseException.

Key file parser limits enforced before any allocation.

fileMax = 131072Maximum key file size in bytes
commentMax = 1024Maximum key comment length in bytes
kdfOptionsMax = 1024Maximum openssh-key-v1 kdfoptions length in bytes
privateBlobMax = 65536Maximum openssh-key-v1 private section length in bytes

An SshSigner backed by an in-memory private key loaded from a key file.

Signing is synchronous: sign completes in one call. RSA keys accept only the SHA-2 signature algorithms (rsa-sha2-256/rsa-sha2-512); the SHA-1 ssh-rsa signature algorithm is refused like everywhere else in the profile.

Fields
private SshPublicKeyInfo publicKey_
private Ed25519PrivateKey ed25519_
private RSAPrivateKey rsa_
Methods
SshPublicKeyInfo publicKey() @property constReturns: the public key offer built from the private key, never null.
SshSignResult sign(scope const(ubyte)[] requestData, string signatureAlgorithm)Signs the request data with the loaded key.
private SshSignResult signChecked(scope const(ubyte)[] requestData, string signatureAlgorithm)
private SshSignResult unsupported(string signatureAlgorithm) static @safe
Constructors
private structPbes2Params

Parsed PBES2 parameters of an EncryptedPrivateKeyInfo.

Fields
const(ubyte)[] saltPBKDF2 salt.
uint iterationsPBKDF2 iteration count.
uint keyLengthCipher key length in bytes implied by the encryption scheme.
string cipherNameHuman-readable encryption scheme name, e.g. AES-256-CBC.
const(ubyte)[] ivCipher IV.
uint prfShaHMAC-SHA variant for the PBKDF2 PRF: 1, 256, or 512.
const(ubyte)[] encryptedDataEncrypted body.

Functions 24

fnSshSigner loadSshPrivateKeyFile(const(ubyte)[] fileBytes)Loads an unencrypted SSH private key file.
fnSshSigner loadSshPrivateKeyFile(const(ubyte)[] fileBytes, SshPassphraseProvider provider)Loads a private key file, decrypting encrypted containers with a passphrase acquired from provider.
fnSshSigner loadSshPrivateKeyFile(string text)Ditto
private fnT withPassphraseRetries(T)(SshPassphraseProvider provider, string container, scope T delegate(const(ubyte)[] passphrase) attempt)Runs one decryption attempt per passphrase obtained from provider, retrying a rejected passphrase until maxPassphraseAttempts is exhausted.
private fnbool startsWithBytes(const(ubyte)[] data, string prefix) @safe pure nothrow
private fnSshPrivateKeySigner adopt(string algorithm, ubyte[] blob, string comment)
private fnSshPrivateKeySigner adoptEd25519(Ed25519PrivateKey key, string comment = "")
private fnSshPrivateKeySigner adoptRsa(RSAPrivateKey key, string comment = "")
private fnSshSigner loadPemKeyFile(string text, SshPassphraseProvider provider)
private fnint indexOfAscii(string haystack, string needle) @safe pure nothrow
private fnSshSigner loadDerKey(const(ubyte)[] der, SshPassphraseProvider provider)
private fnSshSigner loadPkcs8(const(ubyte)[] der, SshPassphraseProvider provider)
private fnSshSigner parsePkcs8Plain(const(ubyte)[] der)Parses an unencrypted PKCS#8 PrivateKeyInfo.
private fnSshSigner parsePkcs8Body(const(ubyte)[] der)
private fnSshSigner loadEncryptedPkcs8(const(ubyte)[] der, SshPassphraseProvider provider)Loads an EncryptedPrivateKeyInfo, decrypting it through the passphrase retry loop when provider is given.
private fnPbes2Params parsePbes2Params(const(ubyte)[] der)Parses the PBES2 parameters of an EncryptedPrivateKeyInfo (RFC 8018 appendix A.4), restricting the profile to PBKDF2 with HMAC-SHA-1/-SHA-256/-SHA-512 and AES-128/192/256-CBC.
private fnSshSigner decryptPbes2Attempt(const Pbes2Params params, const(ubyte)[] passphrase)Performs one PBES2 decrypt-and-adopt attempt.
private fnauto cryptoChecked(alias decoder)(const(ubyte)[] der, string what)
private fnSshSigner parseOpenSshKeyV1(const(ubyte)[] blob, SshPassphraseProvider provider)
private fnSshSigner decryptOpenSshPrivateSection(const(ubyte)[] privateBlob, const(ubyte)[] salt, uint rounds, const SshParsedPublicKey parsedPublic, const(ubyte)[] publicBlob, const(ubyte)[] passphrase)Decrypts one encrypted openssh-key-v1 private section attempt.
private fnSshSigner parseOpenSshPrivateSection(const(ubyte)[] section, const SshParsedPublicKey parsedPublic, const(ubyte)[] publicBlob, bool encrypted)Parses a (decrypted or plaintext) openssh-key-v1 private section.
private fnSshSigner parseOpenSshPrivateKey(ref SshWireReader reader, const SshParsedPublicKey parsedPublic, const(ubyte)[] publicBlob)
private fnvoid requireBlobMatch(const(ubyte)[] built, const(ubyte)[] stored, string keyType)

Variables 2

enumvarmaxPassphraseAttempts = 3

Bounded retry policy: the maximum number of passphrase attempts made against one encrypted container before giving up.

private enumvaropenSshMagic = "openssh-key-v1\0"