Skip to content

Tutorial

Martin Paljak edited this page Feb 22, 2025 · 13 revisions

Warning

The tutorial is not yet complete and might not be fully copy-paste, but should communicate the generic approach.

It presumes prior knowledge of Java, (Open)SSH and basic public key cryptography from the reader.

SSHProvider is a Java Security Provider for working with (Open)SSH keys, certificates and signatures. As such it needs to be installed as a Security Provider first.

import com.hardssh.provider.SSHProvider;

Security.addProvider(new SSHProvider());

Installation

For this to work, you need to have it declared as a dependency for your project. With Maven, add this to pom.xml:

<repositories>
    <repository>
        <id>javacard-pro</id>
        <url>https://mvn.javacard.pro/maven/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.github.martinpaljak</groupId>
        <artifactId>sshprovider</artifactId>
        <version>25.02.21</version>
    </dependency>
</dependencies>

Working with SSH public keys and certificates

Important

SSHProvider does not support nor encourage working with plaintext private keys by design.

But as the 'P' in PKI stands for Public keys, those are something we'll encounter often.

The most widespread and familiar form of SSH public keys is the string format as seen one-per-line in ~/.ssh/authorized_keys or .ssh/known_hosts or allowed_signers files, or in general the one-line *.pub text files (as they are found after key generation in ~/.ssh).

This format is "redundant" - the type of the key is also encoded in the Base64 string and the comment is only relevant to the human viewing the file.

While the binary form encoded as Base64 in the text string is the canonical key object itself, the textual one-liner is a convenient (and preferred) method for storing, transporting and configuring keys.

SSHIdentity identity = SSHIdentity.fromString("ecdsa-sha2-nistp256 AAAAE2Vj...GZchtiY= a test key");

Unlike X509 the OpenSSH certificates don't form a hierarchy - maybe a reason why they are called "certified keys".

In the end SSH revolves around keys - be they "plain keys" (as expressed by a SSHPublicKey) or "certified keys" (SSHCertificate).

Both classes implement the SSHIdentity interface, which has a few important methods:

interface SSHIdentity {
    String getSSHType();
    SSHPublicKey getKey();
    Optional<SSHCertificate> asCertificate();
    Optional<String> getComment();
}
  • getSSHType() - returns the type of the key (like ssh-rsa for plain RSA key or [email protected] for an Ed25519 FIDO security key certified key)
  • getKey() - returns the SSHPublicKey of the identity (either the key itself, or the certified key)
  • asCertificate() - returns an Optional<SSHCertificate> which is empty for SSHPublicKey-s

Working with SSHIdentity is preferred when you don't want to care if it is actually backed by a plain key or a certified key. It also has a getComment() method that returns the comment (if it was present in the parsed string).

SSHPublicKey implements the standard Java PublicKey interface and SSHCertificate is an instance of the standard Java Certificate class. SSHCertificate also implements PublicKey for the SSHSIG use cases explained below. They both have a getEncoded() method that return the canonical binary representation of each.

Those binary representations can be turned back into a SSHPublicKey with a KeyFactory

byte[] blob = identity.getKey().getEncoded();
KeyFactory kf = KeyFactory.getInstance("SSH");
SSHPublicKey pub = (SSHPublicKey) kf.generatePublic(new OpenSSHPublicKeySpec(blob));

Same for certificates with a CertificateFactory

byte[] blob = identity.asCertificate().get().getEncoded();
CertificateFactory cf = CertificateFactory.getInstance("SSH");
SSHCertificate cert = (SSHCertificate) cf.generateCertificate(new ByteArrayInputStream(blob));

Warning

A lot of software assumes Certificate == X509Certificate and will barf if presented something else. This includes Java's keytool/jarsigner! Thus SSHProvider makes SSHCertificate extend the default Certificate just for purity, not for interoperability: the only meaningful methods to call are Certificate.getEncoded(), Certificate.getPublicKey() and Certificate.verify().

Working with keys in agent

The main interface of SSHProvider for access to keys is "SSH" KeyStore. It makes all keys available in the SSH agent (the one listening at $SSH_AUTH_SOCK) accessible via the KeyStore API, and usable with the different signature generation methods described below.

Different agents provide different capabilities and different user experience. SSHProvider is just an API and abstraction for Java, it doesn't implement any UI (which is often done by various SSH agents) or handle any private keys (which is left to agents, which themselves are expected to delegate the actual cryptographic operations to specialized hardware devices). Getting the necessary hardware and software properly set up is out of scope for this developer tutorial, but shall be covered in more detail at HardSSH.com.

The interface is super simple, not unlike using a simple software keystore file: create a KeyStore instance of the right type, and "load" it.

KeyStore ks = KeyStore.getInstance("SSH");
ks.load(null, null);

The load function takes no real parameters, as the agent socket is read from $SSH_AUTH_SOCK environment variable and any kind of key access authorization is supposed to be handled by the agent itself.

There is one convenience trick: OpenSSH agent can be "locked" with a password. Specifying a password for the KeyStore.load() function will send it with an unlock command to the agent. This feature is not often used by agents, and there is no expected UX defined. Right now any failure from the unlock command is ignored by the KeyStore.

If you have keys in your agent visible with OpenSSH ssh-add -l, you should see the same (or similar) key list in the KeyStore alias list

for (String alias : Collections.list(ks.aliases())) {
    System.out.println(alias); // SHA256:5DmYCoIkCgEoOnbx3K+UXLhHVh8pX8GXgf7IS8i9QPo
}

Tip

This can be triggered via Java's keytool as well:

keytool -providerpath sshprovider.jar -providerclass com.hardssh.provider.SSHProvider -list -keystore NONE -storetype SSH -storepass NONE

NB: keytool expects all certificates to be of X509Certificate type, so will barf if there are any SSH certificates.

Aliases and finding keys

Warning

The list of available keys is refreshed only when the store is loaded or when KeyStore.aliases() is called.

Trying to get a key handle with KeyStore.getKey() for an alias of a key that has been removed after calling KeyStore.aliases() will succeed, but signing with it will fail. Be sure to call KeyStore.aliases() to detect (re/un)plugged security tokens!

Keys (and certified keys) are reported by their SHA-256 fingerprint. Unlike OpenSSH ssh-add -l, certificates are reported by their own fingerprint, rather than the fingerprint of the enclosed public key. This is to allow to reference both normal keys and certified keys uniquely with a short-ish deterministic identifier. When configuring an application that uses SSHProvider, it is expected (but not mandatory) that the configuration shall specify the fingerprint of the key to be used.

Important

Only certificates with an available private key are reported. For example: if a certificate is backed by a key on a detachable Yubikey, unplugging the Yubikey also makes the certificate not visible after the next call to KeyStore.aliases().

It is also possible to find a key by using the full OpenSSH string representation of a key or certificate (even without having to iterate over aliases).

KeyStore.Entry entry = ks.getEntry("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItb...chtiY= a test key", null);

Iterating KeyStore entries and Entry types

Two types of entries are available via the KeyStore

  • KeyStore.Entry.PrivateKeyEntry
    • returned for certified keys and include a SSHAgentPrivate key and a single SSHCertificate
  • SSHProvider.KeyPairEntry
    • returned for private keys for explicit access to the PublicKey. Also accessible from SSHAgentPrivateKey.getIdentity()

The returned SSHAgentPrivateKey-s are only usable with Signature classes in the SSHProvider.

Generating keys

How the keys are generated or made available to the agent listening on $SSH_AUTH_SOCK is outside of scope for SSHProvider.

Using keys to sign

SSHProvider has three interesting categories of Signature engines:

  • to generate standard signatures (SHA256withRSA etc) with SSHAgentPrivateKey-s
  • to generate and verify SSHSIG signatures with any PrivateKey or PublicKey
  • to generate and verify low-level SSH signatures (ecdsa-sha2-nistp256 etc) with any PrivateKey or PublicKey

If you need to generate standard signatures of types Ed25519, SHA256withECDSA, SHA384withECDSA, SHA512withECDSA, SHA256withRSA or SHA512withRSA you can use corresponding keys from your agent by just accessing the keys via the KeyStore API. This should require no additional tuning other than registering the SSHProvider and picking the right key from the "SSH" KeyStore. Be sure to specify the Provider when requesting a Signature object with a standard signature type!

Signature sig = Signature.getInstance("SHA256withECDSA", new SSHProvider());

The next "Fire-and-forget" use is generating (and verifying) SSHSIG signatures with any of the keys available from the SSH KeyStore. This includes SSH certificates and the more exotic SSH-only key types like FIDO keys. Or any existing supported standard Java keys (which could be plaintext BouncyCastle or coming from a 40k$ HSM). SSHSIG requires some additional setup and coding.

The third and probably least interesting use case is generating and verifying low level SSH signatures of any type, some of what requires delicate tuning and should probably not be done outside of test and debug contexts.

Create and verify SSHSIG files

SSHSIG is a simple and lightweight signature format from OpenSSH. It has no "canonical XML" or ASN.1 or "canonical JSON" nor tries to mimic ASN.1 or maybe JWT-s, but in CBOR, which make them simple and easy to work with. Furthermore, it is used by Git for commit signatures. It is a fun little format to work with and has widespread and easy accessibility due to OpenSSH/Git background.

Creating SSHSIG files

SSHSIG signatures can be created with any key or certificate available from an agent.

KeyStore ks = KeyStore.getInstance("SSH");
ks.load(null, null);

PrivateKey key = ks.getKey("SHA256:....");

The signature algorithm is detected automatically based on used private key by the SSHSIG engine

Signature signature = Signature.getInstance("SSHSIG");

Note

All SSHSIG-s are scoped to a namespace, which MUST be provided with signature parameter before signing can be done.

signature.setParameter(new SSHSIGSigningParameters("namespace"));

The namespace used for Git signatures is "git" and for file signatures it is common to use "file".

When creating SSHSIG signatures from other PrivateKey-s, the public key corresponding to the private key must be provided to the signature generator. This is only necessary if using keys other than the ones available from SSHProvider KeyStore - SSHAgentPrivateKey-s implicitly know their public keys.

The default hash is SHA-512, which can (but probably shouldn't) be switched to SHA-256 with signature parameters.

KeyPairGenerator kpg = KeyPairGenerator.getInstance("...");
...
KeyPair kp = kpg.generateKeyPair();

signature.setParameter(new SSHSIGSigningParameters("namespace", kp.getPublic(), "SHA-256"));

Note

The SSHSIG data structure can include a full SSH certificate, even if the actual signing happens with a "simple" key. So be sure to specify the right SSHIdentity in the parameter, or just the plain key will be embedded!

Now you can initialize the Signature object with your chosen PrivateKey

signature.initSign(key);

And use one of the Signature.update() methods to pipe in the data to be signed

signature.update(Files.readAllBytes(Paths.get("data-to-be-signed.txt")));

Once done, you get the signature in binary form. The standard format for a .sig file with OpenSSH tools is ASCII armored signature, for what you can use the SSHSIG.toArmored() method.

byte[] blob = signature.sign();

String sshsig = SSHSIG.toArmored(blob);

The resulting string can be written to a file

Files.writeString(Paths.get("data-to-be-signed.txt.sig"), sshsig);

Tip

The above is roughly equivalent to the following OpenSSH command line:

$ ssh-keygen -Y sign -U -f path/to/pubkey.pub -n namespace data-to-be-signed.txt

Verifying SSHSIG files

TODO (like above)

Caution

It is possible to check a SSHSIG signature without specifying the namespace, but this is highly discouraged (and will log a warning.

Similarly, while the actual signature is generated with the "simple" key, the SSHSIG data structure may include a full certificate. A warning is shown if the verification engine was not initialized with the SSHCertificate cast to a PublicKey. Cryptographic verification will fail if the key and the certificate don't match.

Using allowed_signers trust anchors

TODO

Development and debugging tools (WIP)

You probably don't need these or should have a very clear idea why.

String appdata = "ssh:";

signature.setParameter(new FIDOKeyParametersSpec(appdata));
signature.initVerify(PublicKey ...)

int counter = 123;
byte flags = 0x05;

signature.setParameter(new FIDOKeyParametersSpec(appdata, counter, flags));
signature.initSign(PrivateKey ...)
  • WebAuthnParametersSpec to generate [email protected] signatures with standard Java PrivateKey/PublicKey pairs
origin = "https://example.com"
int counter = 123;
byte flags = 0x05;

signature.setParameter(new WebAuthnParametersSpec(origin, counter, flags));
signature.initSign(PrivateKey ...)

Things to remember, things you can't do

  • SSHProvider does not implement access to hardware - you need an agent for that
  • Only signing operations, no ECDH or decryption
  • Non-SSH standard signatures are only possible with non-FIDO keys.