How to Calculate Fingerprint From SSH RSA Public Key in Java?
Asked Answered
A

3

6

As title, How to Calculate Fingerprint From SSH RSA Public Key in Java? I got an rsaPublicKey object from sample.pub and I calculated the fingerprint by using library Apache Commons Codec DigestUtils.sha256Hex(rsaPublicKey.getEncoded()); But I got a different fingerprint when using ssh-keygen command ssh-keygen -E sha256 -lf sample.pub sample.pub as below ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAsuVPKUpLYSCNVIHD+e6u81IUznkDoiOvn/t56DRcutRc4OrNsZZ+Lmq49T4JCxUSmaT8PeLGS/IC946CNQzFwMh++sVoc19UUkZtRaDgiYn+HkYk8VW4IFI1dKfXomKSbX/lB+ohzLzXLVP2/UJgfBmdaE10k+6b+/Yd8YGXIeS8/Z9zToHPo0ORNSGIolgq3xMXUtfAOK/0KC6IFc/FuvuOSAG1UWup91bcm5GSXv4BWWjgFtOxCLIknYjsDah4qfrP8Olp5eUDhn/65xRcZsmRXoYe1ylhlSjJoPDFWXVs9npwqQmi3JaZtgg7xJxMu1ZcdpYxoj280zM9/6w1Lw==

Antirachitic answered 27/6, 2018 at 9:57 Comment(2)
Is there a way of doing the way around ? I have a fingerprint and I need the rsa key. Is that possible ?Wayside
It's impossible. fingerprint is calculated by hash function. If you know what's hash, you wouldn't ask such question.Antirachitic
B
7

Your main problem is that the XDR-style encoding used by SSH for publickey, which OpenSSH uses to compute the fingerprint, is not the same as the encoding used by Java crypto, which is an ASN.1 DER format defined by X.509 formally called SubjectPublicKeyInfo. In fact I'm very surprised you were able to read an OpenSSH .pub file in Java; there is no direct way to do so. See numerous existing Qs on this at ssh-keygen and openssl gives two different public keys (disclosure: mine) but on a quick check I don't think any of them are Java so you'll need to do something like:

byte[] n = rsapubkey.getModulus().toByteArray(); // Java is 2sC bigendian
byte[] e = rsapubkey.getPublicExponent().toByteArray(); // and so is SSH
byte[] tag = "ssh-rsa".getBytes(); // charset very rarely matters here
ByteArrayOutputStream os = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(tag.length); dos.write(tag);
dos.writeInt(e.length); dos.write(e);
dos.writeInt(n.length); dos.write(n);
byte[] encoded = os.toByteArray();
// now hash that (you don't really need Apache) 
// assuming SHA256-base64 (see below)
MessageDigest digest = MessageDigest.getInstance("SHA256");
byte[] result = digest.digest(encoded);
String output = Base64.getEncoder().encodeToString(result);

(Aside: Thanks linc01n for catching the bug -- I try to always compile before posting and I'm not sure how I missed this one.)

The second problem is that OpenSSH has never displayed SHA256 fingerprints in hex. It originally used MD5 fingerprints in hex with colons; in 6.8 it switched by default to SHA256 in base64 (using the traditional alphabet not the 'URLsafe' one preferred by JSON) although you can still get the older form (in ssh use -oFingerprintHash=md5 or the equivalent config setting; in ssh-keygen -l use -E md5). Determine which one(s?) you want and code accordingly.

Or, if you have the .pub file, just read the second space-separated field of the one line, convert from base64 to byte[], hash that, and display.

Boycie answered 27/6, 2018 at 11:17 Comment(10)
what if the default platform charset is UTF-16? Then "ssh-rsa" gets encoded wrongly.Seacock
"Fixed" :-) :-)Boycie
Haha, ok, you win.Seacock
I suggest updating the variable name "do" to something that isn't a keyword :)Slowpoke
This almost got me there but I discovered that PuTTYgen and ssh-keygen prefix their modulus with a 0 byte. To match their output you'd need to have do.writeInt(n.length + 1); do.write(0); do.write(n);. (Sorry, I'm not 100% on how to write a 0 byte.)Glaikit
@NJones: SSH mpint requires leading 00 byte on numbers whose bitlength is a multiple of 8, and RSA moduli are usually (not always) chosen as such multiples -- but Java BigInteger.toByteArray() already includes that byte when applicable and you should never need to add it; notice my comment about "2sC" i.e. two's-complement.Boycie
@dave_thompson_085: Thanks for the clarification. I was trying to translate to c# and didn't understand the context. I couldn't find anything on mpint requiring a leading 00 byte relative to bitlengths. I did find reference to needing a leading 00 when the MSB of the first byte is set in a positive int. If the leading zero isn't added then it could be interpreted as negative. I actually found a c# analogue, and I get the correct result by importing as unsigned BE and exporting as signed BE: new BigInteger(keyParams.Modulus!.AsSpan<byte>(), true, true).ToByteArray(false, true)Glaikit
@NJones: that means the same. The bitlength of a number is the position of its highest one=set bit, and since (modern) bytes are 8 bits, a number whose bitlength is a multiple of 8 is a number whose high byte (first in bigendian) has its high bit set, and needs the extra 00 byte to be recognized as positive. RSA moduli are usually chosen to have bitlengths like 1024 2048 3072 which are multiples of 8 and thus have the high bit set in the high=first byte.Boycie
@dave_thompson_085: Ah, I see what you're saying. You're talking about bitlength as minimum number of bits to express a particular binary value. That's not exactly the same as the number of bits it may take up (e.g. 32-bit int with a value of 3). I'd be surprised if the high bit always had to be set in the modulus because that would knock off 1 bit of potential entropy wouldn't it?Glaikit
@NJones: the security of an RSA key depends on its approximate magnitude (and the generation process, but that is usually hardcoded, so the magnitude is the only thing chosen). This is normally simplified by choosing a size in bits as I said (rather than the magnitude as such): mostly 1024 through the oughties, and still some leftovers, usually 2048 or 3072 now; some people use 4096 or more; and smartcards often use sizes slightly under 2048 like 2016 due to interface limitations. It would be impossible to conceal the modulus size, so no one tries.Boycie
E
4

Use this to calculate fingerprint from your public key:

    /**
     * Calculate fingerprint
     *
     * @param publicKey public key
     * @return fingerprint
     */
    public static String calculateFingerprint(String publicKey) {
        String derFormat = publicKey.split(" ")[1].trim();
        MessageDigest messageDigest = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("Could not get fingerprint", e);
        }
        byte[] digest = messageDigest.digest(Base64.getDecoder().decode(derFormat));
        final StringBuilder toRet = new StringBuilder();
        for (int i = 0; i < digest.length; i++) {
            if (i != 0) toRet.append(":");
            int b = digest[i] & 0xff;
            String hex = Integer.toHexString(b);
            if (hex.length() == 1) toRet.append("0");
            toRet.append(hex);
        }
        return toRet.toString();
    }

This will give you the same result as:

ssh-keygen -E md5 -l -f id_rsa.pub
Eucaine answered 27/12, 2019 at 13:58 Comment(0)
T
0

Solution for all those that needs the sha256 finger print from a ssh-rsa public key and are using apache sshd library.

   InputStream keyData = new ByteArrayInputStream(Base64.getDecoder().decode("AAAAB3NzaC1yc2E...."));
    try {
      String type = KeyEntryResolver.decodeString(keyData, KeyPairResourceLoader.MAX_KEY_TYPE_NAME_LENGTH);
      String canonicalName = KeyUtils.getCanonicalKeyType(type);
      if (!KeyPairProvider.SSH_RSA.equals(canonicalName)) {
        throw new IllegalArgumentException("Unexpected key type: " + type);
      }
      BigInteger e = KeyEntryResolver.decodeBigInt(keyData);
      BigInteger n = KeyEntryResolver.decodeBigInt(keyData);

      PublicKey key = SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM).generatePublic(new RSAPublicKeySpec(n, e));
System.out.println(KeyUtils.getFingerPrint(key));
Toreutics answered 9/10, 2024 at 13:10 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.