ssh-agent holds your unlocked SSH keys so ssh never reads the key files again. SshAgentClient talks to the same agent over SSH_AUTH_SOCK: it lists the keys and asks the agent to sign, and the private key never reaches your program. The program below signs a file the way ssh-keygen -Y sign does, so ssh-keygen -Y verify and anything that reads Git’s SSH signatures can check it.
dotnet new console -o SshAgentSign && cd SshAgentSign dotnet add package RecursiveAI.OneCrypto.Keys --version 0.4.0-preview.1
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. To try it without touching your own agent and keys, start a throwaway agent on a temporary socket, give it a new Ed25519 key, and delete the key file, so the key only exists in the agent. Skip this to use your own agent: the program reads SSH_AUTH_SOCK like ssh does.
export SSH_AUTH_SOCK="$(mktemp -d)/agent.sock" eval "$(ssh-agent -a "$SSH_AUTH_SOCK")" ssh-keygen -q -t ed25519 -N '' -C '[email protected]' -f demo_key ssh-add demo_key rm demo_key
Agent pid 3085783 Identity added: demo_key ([email protected])
Put this in Program.cs:
using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; using RecursiveAI.OneCrypto.Keys; using RecursiveAI.OneCrypto.Keys.Agents; using RecursiveAI.OneCrypto.Wire.Ssh; // The agent in SSH_AUTH_SOCK, the one ssh and ssh-add use var agent = new SshAgentClient(); IReadOnlyList<SshAgentIdentity> identities = agent.ListIdentities(); foreach (SshAgentIdentity id in identities) Console.WriteLine($"Agent key: {id.PublicKey?.Type} {id.Comment}"); SshAgentIdentity alice = identities.First(i => i.Comment == "[email protected]"); using SshAgentKey key = agent.GetKey(alice.PublicKey!); File.WriteAllText("release.txt", "release 2026.11, build 4127\n"); byte[] data = File.ReadAllBytes("release.txt"); File.WriteAllText("release.txt.sig", SignSshsig(key, data, "file")); Console.Write(File.ReadAllText("release.txt.sig")); // An SSH signature file (OpenSSH PROTOCOL.sshsig), as `ssh-keygen -Y sign -n file` writes. // The agent signs; the program only frames what it signs and what it returns. static string SignSshsig(SshAgentKey key, byte[] data, string ns) { byte[] digest = SHA512.HashData(data); byte[] toSign = [.. "SSHSIG"u8, .. Str(ns), .. Str(""), .. Str("sha512"), .. Str(digest)]; SignatureSpec spec = key.SshKey.Type == SshKeyType.Rsa ? SignatureSpec.RsaPkcs1(HashAlgorithmName.SHA512) // rsa-sha2-512 : key.DefaultSpec; byte[] signature = key.SignToSshSignature(toSign, spec); byte[] file = [.. "SSHSIG"u8, 0, 0, 0, 1, .. Str(key.SshKey.Blob.ToArray()), .. Str(ns), .. Str(""), .. Str("sha512"), .. Str(signature)]; string b64 = Convert.ToBase64String(file); var pem = new StringBuilder("-----BEGIN SSH SIGNATURE-----\n"); for (int i = 0; i < b64.Length; i += 70) pem.Append(b64, i, Math.Min(70, b64.Length - i)).Append('\n'); return pem.Append("-----END SSH SIGNATURE-----\n").ToString(); } // An SSH "string": a 4-byte big-endian length, then the bytes static byte[] Str(object value) { byte[] bytes = value is string s ? Encoding.UTF8.GetBytes(s) : (byte[])value; byte[] length = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(length, bytes.Length); return [.. length, .. bytes]; }
Agent key: Ed25519 [email protected] -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgwDd/whwuFxWgXZ/T8XC9PUyLdK 9HySgVm0vodrnoqPEAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx OQAAAEBa4i1o1bMPB9XQfG3P2cYgE83B8dqxQopBuIVt6azuHY1bdHVkN4DMfEn98Ql3zc OcgRl5lEAw1l/OK3O4lnIO -----END SSH SIGNATURE-----
Every signature the agent returns is checked against the key’s public key before SignToSshSignature hands it back (unless SshAgentOptions.VerifySignatures is turned off), and the algorithm on it must be the one asked for. RSA keys sign with rsa-sha2-256 or rsa-sha2-512; the SHA-1 ssh-rsa form is never requested. ECDSA keys work the same way as Ed25519. GetKey returns an ISigningKey, so the same agent key can also sign a CMS message or a certificate through OneCrypto’s other APIs.
Check it with ssh-keygen
ssh-keygen -Y verify takes an allowed-signers file that maps an identity to a public key:
echo "[email protected] $(cat demo_key.pub)" > allowed_signers ssh-keygen -Y verify -f allowed_signers -I [email protected] -n file \ -s release.txt.sig < release.txt echo 'release 2026.11, build 4128' | ssh-keygen -Y verify -f allowed_signers \ -I [email protected] -n file -s release.txt.sig
Good "file" signature for [email protected] with ED25519 key SHA256:To7Hg/mJ8VjR8Tu/oSzHajqBpd0h99zb5TurOlfVmZ4 Signature verification failed: incorrect signature Could not verify signature.
The second check changes the file, and ssh-keygen rejects the signature. When you’re done, stop the throwaway agent:
eval "$(ssh-agent -k)"Limits
- Not audited. OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.4.0-preview.1, and the API can change.
- The key-backend glue (PKCS #11, gpg-agent and ssh-agent) “had an internal review … it has not been independently audited” (Key-backend review).
- The agent hashes the data itself, so
SignDigestis not available, and neither are RSA-PSS or security-key (sk-) keys. A refusal from the agent (a locked agent, a key added withssh-add -cwhose confirmation was denied) is anAgentRefusedException, and the agent never says which. - No SSHSIG API yet. The SSH signature file is built by the code above; signing and verifying SSH signature files is a planned package.
- The project tests against OpenSSH’s ssh-agent with RSA, ECDSA on three curves and Ed25519 keys. This page ran on OpenSSH 10.2.
Protocol, limits and errors: Keys, two-phase signing and key backends.