-
-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial
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());
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>
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 (likessh-rsa
for plain RSA key or[email protected]
for an Ed25519 FIDO security key certified key) -
getKey()
- returns theSSHPublicKey
of the identity (either the key itself, or the certified key) -
asCertificate()
- returns anOptional<SSHCertificate>
which is empty forSSHPublicKey
-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()
.
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.
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);
Two types of entries are available via the KeyStore
-
KeyStore.Entry.PrivateKeyEntry
- returned for certified keys and include a
SSHAgentPrivate
key and a singleSSHCertificate
- returned for certified keys and include a
-
SSHProvider.KeyPairEntry
- returned for private keys for explicit access to the
PublicKey
. Also accessible fromSSHAgentPrivateKey.getIdentity()
- returned for private keys for explicit access to the
The returned SSHAgentPrivateKey
-s are only usable with Signature
classes in the SSHProvider
.
How the keys are generated or made available to the agent listening on $SSH_AUTH_SOCK
is outside of scope for SSHProvider
.
SSHProvider
has three interesting categories of Signature
engines:
- to generate standard signatures (
SHA256withRSA
etc) withSSHAgentPrivateKey
-s - to generate and verify
SSHSIG
signatures with anyPrivateKey
orPublicKey
- to generate and verify low-level SSH signatures (
ecdsa-sha2-nistp256
etc) with anyPrivateKey
orPublicKey
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.
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.
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
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.
TODO
You probably don't need these or should have a very clear idea why.
-
FIDOKeyParametersSpec
to generate or verify[email protected]
and[email protected]
signatures with standard Java PrivateKey/PublicKey pairs
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 ...)
-
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.
Part of HardSSH
- hardware base layer and certificate management overlay for OpenSSH