/** * Licensed to jclouds, Inc. (jclouds) under one or more * contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. jclouds licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jclouds.ssh; import static com.google.common.base.Joiner.on; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Splitter.fixedLength; import static com.google.common.base.Throwables.propagate; import static com.google.common.collect.Iterables.get; import static com.google.common.collect.Iterables.size; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.io.BaseEncoding.base64; import static org.jclouds.crypto.Pems.pem; import static org.jclouds.crypto.Pems.privateKeySpec; import static org.jclouds.util.Strings2.toStringAndClose; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.security.spec.RSAPrivateCrtKeySpec; import java.security.spec.RSAPublicKeySpec; import java.util.Map; import org.jclouds.crypto.Crypto; import org.jclouds.crypto.Pems; import com.google.common.annotations.Beta; import com.google.common.base.Charsets; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.common.io.InputSupplier; /** * Utilities for ssh key pairs * * @author Adrian Cole * @see <a href= * "http://stackoverflow.com/questions/3706177/how-to-generate-ssh-compatible-id-rsa-pub-from-java" * /> */ @Beta public class SshKeys { /** * Executes {@link Pems#publicKeySpecFromOpenSSH(InputSupplier)} on the string which was OpenSSH * Base64 Encoded {@code id_rsa.pub} * * @param idRsaPub * formatted {@code ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...} * @see Pems#publicKeySpecFromOpenSSH(InputSupplier) */ public static RSAPublicKeySpec publicKeySpecFromOpenSSH(String idRsaPub) { try { return publicKeySpecFromOpenSSH(ByteStreams.newInputStreamSupplier( idRsaPub.getBytes(Charsets.UTF_8))); } catch (IOException e) { throw propagate(e); } } /** * Returns {@link RSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub} * * @param supplier * the input stream factory, formatted {@code ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...} * * @return the {@link RSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub} * @throws IOException * if an I/O error occurs */ public static RSAPublicKeySpec publicKeySpecFromOpenSSH(InputSupplier<? extends InputStream> supplier) throws IOException { InputStream stream = supplier.getInput(); Iterable<String> parts = Splitter.on(' ').split(toStringAndClose(stream).trim()); checkArgument(size(parts) >= 2 && "ssh-rsa".equals(get(parts, 0)), "bad format, should be: ssh-rsa AAAAB3..."); stream = new ByteArrayInputStream(base64().decode(get(parts, 1))); String marker = new String(readLengthFirst(stream)); checkArgument("ssh-rsa".equals(marker), "looking for marker ssh-rsa but got %s", marker); BigInteger publicExponent = new BigInteger(readLengthFirst(stream)); BigInteger modulus = new BigInteger(readLengthFirst(stream)); return new RSAPublicKeySpec(modulus, publicExponent); } // http://www.ietf.org/rfc/rfc4253.txt private static byte[] readLengthFirst(InputStream in) throws IOException { int byte1 = in.read(); int byte2 = in.read(); int byte3 = in.read(); int byte4 = in.read(); int length = (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0); byte[] val = new byte[length]; in.read(val, 0, length); return val; } /** * * @param generator * to generate RSA key pairs * @param rand * for initializing {@code generator} * @return new 2048 bit keyPair * @see Crypto#rsaKeyPairGenerator() */ public static KeyPair generateRsaKeyPair(KeyPairGenerator generator, SecureRandom rand) { generator.initialize(2048, rand); return generator.genKeyPair(); } /** * return a "public" -> rsa public key, "private" -> its corresponding private key */ public static Map<String, String> generate() { try { return generate(KeyPairGenerator.getInstance("RSA"), new SecureRandom()); } catch (NoSuchAlgorithmException e) { throw propagate(e); } } public static Map<String, String> generate(KeyPairGenerator generator, SecureRandom rand) { KeyPair pair = generateRsaKeyPair(generator, rand); Builder<String, String> builder = ImmutableMap.builder(); builder.put("public", encodeAsOpenSSH(RSAPublicKey.class.cast(pair.getPublic()))); builder.put("private", pem(RSAPrivateKey.class.cast(pair.getPrivate()))); return builder.build(); } public static String encodeAsOpenSSH(RSAPublicKey key) { byte[] keyBlob = keyBlob(key.getPublicExponent(), key.getModulus()); return "ssh-rsa " + base64().encode(keyBlob); } /** * @param privateKeyPEM * RSA private key in PEM format * @param publicKeyOpenSSH * RSA public key in OpenSSH format * @return true if the keypairs match */ public static boolean privateKeyMatchesPublicKey(String privateKeyPEM, String publicKeyOpenSSH) { KeySpec privateKeySpec = privateKeySpec(privateKeyPEM); checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec, "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec); return privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec.class.cast(privateKeySpec), publicKeySpecFromOpenSSH(publicKeyOpenSSH)); } /** * @return true if the keypairs match */ public static boolean privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec privateKey, RSAPublicKeySpec publicKey) { return privateKey.getPublicExponent().equals(publicKey.getPublicExponent()) && privateKey.getModulus().equals(publicKey.getModulus()); } /** * @return true if the keypair has the same fingerprint as supplied */ public static boolean privateKeyHasFingerprint(RSAPrivateCrtKeySpec privateKey, String fingerprint) { return fingerprint(privateKey.getPublicExponent(), privateKey.getModulus()).equals(fingerprint); } /** * @param privateKeyPEM * RSA private key in PEM format * @param fingerprint * ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} * @return true if the keypair has the same fingerprint as supplied */ public static boolean privateKeyHasFingerprint(String privateKeyPEM, String fingerprint) { KeySpec privateKeySpec = privateKeySpec(privateKeyPEM); checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec, "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec); return privateKeyHasFingerprint(RSAPrivateCrtKeySpec.class.cast(privateKeySpec), fingerprint); } /** * @param privateKeyPEM * RSA private key in PEM format * @return fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} */ public static String fingerprintPrivateKey(String privateKeyPEM) { KeySpec privateKeySpec = privateKeySpec(privateKeyPEM); checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec, "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec); RSAPrivateCrtKeySpec certKeySpec = RSAPrivateCrtKeySpec.class.cast(privateKeySpec); return fingerprint(certKeySpec.getPublicExponent(), certKeySpec.getModulus()); } /** * @param publicKeyOpenSSH * RSA public key in OpenSSH format * @return fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} */ public static String fingerprintPublicKey(String publicKeyOpenSSH) { RSAPublicKeySpec publicKeySpec = publicKeySpecFromOpenSSH(publicKeyOpenSSH); return fingerprint(publicKeySpec.getPublicExponent(), publicKeySpec.getModulus()); } /** * @return true if the keypair has the same SHA1 fingerprint as supplied */ public static boolean privateKeyHasSha1(RSAPrivateCrtKeySpec privateKey, String fingerprint) { return sha1(privateKey).equals(fingerprint); } /** * @param privateKeyPEM * RSA private key in PEM format * @param sha1HexColonDelimited * ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} * @return true if the keypair has the same fingerprint as supplied */ public static boolean privateKeyHasSha1(String privateKeyPEM, String sha1HexColonDelimited) { KeySpec privateKeySpec = privateKeySpec(privateKeyPEM); checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec, "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec); return privateKeyHasSha1(RSAPrivateCrtKeySpec.class.cast(privateKeySpec), sha1HexColonDelimited); } /** * @param privateKeyPEM * RSA private key in PEM format * @return sha1HexColonDelimited ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} */ public static String sha1PrivateKey(String privateKeyPEM) { KeySpec privateKeySpec = privateKeySpec(privateKeyPEM); checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec, "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec); RSAPrivateCrtKeySpec certKeySpec = RSAPrivateCrtKeySpec.class.cast(privateKeySpec); return sha1(certKeySpec); } /** * Create a SHA-1 digest of the DER encoded private key. * * @param publicExponent * @param modulus * * @return hex sha1HexColonDelimited ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} */ public static String sha1(RSAPrivateCrtKeySpec privateKey) { try { byte[] encodedKey = KeyFactory.getInstance("RSA").generatePrivate(privateKey).getEncoded(); return hexColonDelimited(Hashing.sha1().hashBytes(encodedKey)); } catch (InvalidKeySpecException e) { throw propagate(e); } catch (NoSuchAlgorithmException e) { throw propagate(e); } } /** * @return true if the keypair has the same fingerprint as supplied */ public static boolean publicKeyHasFingerprint(RSAPublicKeySpec publicKey, String fingerprint) { return fingerprint(publicKey.getPublicExponent(), publicKey.getModulus()).equals(fingerprint); } /** * @param publicKeyOpenSSH * RSA public key in OpenSSH format * @param fingerprint * ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} * @return true if the keypair has the same fingerprint as supplied */ public static boolean publicKeyHasFingerprint(String publicKeyOpenSSH, String fingerprint) { return publicKeyHasFingerprint(publicKeySpecFromOpenSSH(publicKeyOpenSSH), fingerprint); } /** * Create a fingerprint per the following <a * href="http://tools.ietf.org/html/draft-friedl-secsh-fingerprint-00" >spec</a> * * @param publicExponent * @param modulus * * @return hex fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9} */ public static String fingerprint(BigInteger publicExponent, BigInteger modulus) { byte[] keyBlob = keyBlob(publicExponent, modulus); return hexColonDelimited(Hashing.md5().hashBytes(keyBlob)); } private static String hexColonDelimited(HashCode hc) { return on(':').join(fixedLength(2).split(base16().lowerCase().encode(hc.asBytes()))); } private static byte[] keyBlob(BigInteger publicExponent, BigInteger modulus) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); writeLengthFirst("ssh-rsa".getBytes(), out); writeLengthFirst(publicExponent.toByteArray(), out); writeLengthFirst(modulus.toByteArray(), out); return out.toByteArray(); } catch (IOException e) { throw propagate(e); } } // http://www.ietf.org/rfc/rfc4253.txt private static void writeLengthFirst(byte[] array, ByteArrayOutputStream out) throws IOException { out.write((array.length >>> 24) & 0xFF); out.write((array.length >>> 16) & 0xFF); out.write((array.length >>> 8) & 0xFF); out.write((array.length >>> 0) & 0xFF); if (array.length == 1 && array[0] == (byte) 0x00) out.write(new byte[0]); else out.write(array); } }