---
title: "Sign a file with a key in ssh-agent in C#"
url: https://recursiveai.org/onecrypto/cookbook/sign-with-ssh-agent/
description: "Sign a file in C# with an SSH key held by ssh-agent, without the private key file: list the agent’s keys, sign through the agent, write an SSH signature (SSHSIG) and check it with ssh-keygen -Y verify. Runnable code with OneCrypto.Keys 0.4.0-preview.1."
version: "0.4.0-preview.1"
---

# Sign a file with a key in ssh-agent in C#

`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.

```shell
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.

```shell
export SSH_AUTH_SOCK="$(mktemp -d)/agent.sock"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")"
ssh-keygen -q -t ed25519 -N '' -C 'alice@example.org' -f demo_key
ssh-add demo_key
rm demo_key
```

```text
Agent pid 3085783
Identity added: demo_key (alice@example.org)
```

Put this in `Program.cs`:

```csharp
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 == "alice@example.org");
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];
}
```

```text
Agent key: Ed25519 alice@example.org
-----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:

```shell
echo "alice@example.org $(cat demo_key.pub)" > allowed_signers
ssh-keygen -Y verify -f allowed_signers -I alice@example.org -n file \
  -s release.txt.sig < release.txt
echo 'release 2026.11, build 4128' | ssh-keygen -Y verify -f allowed_signers \
  -I alice@example.org -n file -s release.txt.sig
```

```text
Good "file" signature for alice@example.org 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:

```shell
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](https://recursiveai.org/onecrypto/docs/key-backend-review/)).
- **The agent hashes the data itself**, so `SignDigest` is not available, and neither are RSA-PSS or security-key (`sk-`) keys. A refusal from the agent (a locked agent, a key added with `ssh-add -c` whose confirmation was denied) is an `AgentRefusedException`, 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](https://recursiveai.org/onecrypto/docs/keys/#ssh-agent).
