mirror of https://github.com/apache/jclouds.git
Issue 719: ssh fingerprint and comparison support
This commit is contained in:
parent
0e9d3666ad
commit
a1d1f0131f
|
@ -98,15 +98,15 @@ public class CryptoStreams {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the MAC value for a supplied input stream. The mac object is reset when
|
* Computes and returns the MAC value for a supplied input stream. The mac
|
||||||
* this method returns successfully.
|
* object is reset when this method returns successfully.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
* @param mac
|
* @param mac
|
||||||
* the mac object
|
* the mac object
|
||||||
* @return the result of {@link Mac#doFinal()} after updating the mac object with all of the
|
* @return the result of {@link Mac#doFinal()} after updating the mac object
|
||||||
* bytes in the stream and encoding in Base64
|
* with all of the bytes in the stream and encoding in Base64
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* if an I/O error occurs
|
* if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
|
@ -115,20 +115,20 @@ public class CryptoStreams {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the Digest value for a supplied input stream. The digest object is reset
|
* Computes and returns the Digest value for a supplied input stream. The
|
||||||
* when this method returns successfully.
|
* digest object is reset when this method returns successfully.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
* @param md
|
* @param md
|
||||||
* the digest object
|
* the digest object
|
||||||
* @return the result of {@link MessageDigest#digest()} after updating the digest object with all
|
* @return the result of {@link MessageDigest#digest()} after updating the
|
||||||
* of the bytes in the stream
|
* digest object with all of the bytes in the stream
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* if an I/O error occurs
|
* if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public static byte[] digest(InputSupplier<? extends InputStream> supplier, final MessageDigest md)
|
public static byte[] digest(InputSupplier<? extends InputStream> supplier, final MessageDigest md)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return com.google.common.io.ByteStreams.readBytes(supplier, new ByteProcessor<byte[]>() {
|
return com.google.common.io.ByteStreams.readBytes(supplier, new ByteProcessor<byte[]>() {
|
||||||
public boolean processBytes(byte[] buf, int off, int len) {
|
public boolean processBytes(byte[] buf, int off, int len) {
|
||||||
md.update(buf, off, len);
|
md.update(buf, off, len);
|
||||||
|
@ -142,14 +142,15 @@ public class CryptoStreams {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the MD5 value for a supplied input stream. A digest object is created and
|
* Computes and returns the MD5 value for a supplied input stream. A digest
|
||||||
* disposed of at runtime, consider using {@link #digest} to be more efficient.
|
* object is created and disposed of at runtime, consider using
|
||||||
|
* {@link #digest} to be more efficient.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
*
|
*
|
||||||
* @return the result of {@link MessageDigest#digest()} after updating the md5 object with all of
|
* @return the result of {@link MessageDigest#digest()} after updating the
|
||||||
* the bytes in the stream
|
* md5 object with all of the bytes in the stream
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* if an I/O error occurs
|
* if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
|
@ -162,35 +163,40 @@ public class CryptoStreams {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] md5(byte[] in) throws IOException {
|
public static byte[] md5(byte[] in) {
|
||||||
return md5(ByteStreams.newInputStreamSupplier(in));
|
try {
|
||||||
|
return md5(ByteStreams.newInputStreamSupplier(in));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Throwables.propagate(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the MAC value for a supplied input stream. The mac object is reset when
|
* Computes and returns the MAC value for a supplied input stream. The mac
|
||||||
* this method returns successfully.
|
* object is reset when this method returns successfully.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
* @param mac
|
* @param mac
|
||||||
* the mac object
|
* the mac object
|
||||||
* @return the result of {@link Mac#doFinal()} after updating the mac object with all of the
|
* @return the result of {@link Mac#doFinal()} after updating the mac object
|
||||||
* bytes in the stream
|
* with all of the bytes in the stream
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* if an I/O error occurs
|
* if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public static byte[] mac(InputSupplier<? extends InputStream> supplier, final Mac mac) throws IOException {
|
public static byte[] mac(InputSupplier<? extends InputStream> supplier, final Mac mac) throws IOException {
|
||||||
return com.google.common.io.ByteStreams.readBytes(checkNotNull(supplier, "supplier"),
|
return com.google.common.io.ByteStreams.readBytes(checkNotNull(supplier, "supplier"),
|
||||||
new ByteProcessor<byte[]>() {
|
new ByteProcessor<byte[]>() {
|
||||||
public boolean processBytes(byte[] buf, int off, int len) {
|
public boolean processBytes(byte[] buf, int off, int len) {
|
||||||
mac.update(buf, off, len);
|
mac.update(buf, off, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getResult() {
|
public byte[] getResult() {
|
||||||
return mac.doFinal();
|
return mac.doFinal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -206,20 +212,21 @@ public class CryptoStreams {
|
||||||
public static String base64Encode(InputSupplier<? extends InputStream> supplier) throws IOException {
|
public static String base64Encode(InputSupplier<? extends InputStream> supplier) throws IOException {
|
||||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
return com.google.common.io.ByteStreams.readBytes(InputSuppliers.base64Encoder(supplier),
|
return com.google.common.io.ByteStreams.readBytes(InputSuppliers.base64Encoder(supplier),
|
||||||
new ByteProcessor<String>() {
|
new ByteProcessor<String>() {
|
||||||
public boolean processBytes(byte[] buf, int off, int len) {
|
public boolean processBytes(byte[] buf, int off, int len) {
|
||||||
out.write(buf, off, len);
|
out.write(buf, off, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getResult() {
|
public String getResult() {
|
||||||
return new String(out.toByteArray(), Charsets.UTF_8);
|
return new String(out.toByteArray(), Charsets.UTF_8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the unencoded value for an input stream which is encoded in Base64.
|
* Computes and returns the unencoded value for an input stream which is
|
||||||
|
* encoded in Base64.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
|
@ -231,21 +238,21 @@ public class CryptoStreams {
|
||||||
public static byte[] base64Decode(InputSupplier<? extends InputStream> supplier) throws IOException {
|
public static byte[] base64Decode(InputSupplier<? extends InputStream> supplier) throws IOException {
|
||||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
return com.google.common.io.ByteStreams.readBytes(InputSuppliers.base64Decoder(supplier),
|
return com.google.common.io.ByteStreams.readBytes(InputSuppliers.base64Decoder(supplier),
|
||||||
new ByteProcessor<byte[]>() {
|
new ByteProcessor<byte[]>() {
|
||||||
public boolean processBytes(byte[] buf, int off, int len) {
|
public boolean processBytes(byte[] buf, int off, int len) {
|
||||||
out.write(buf, off, len);
|
out.write(buf, off, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getResult() {
|
public byte[] getResult() {
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final static byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5',
|
final static byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5',
|
||||||
(byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
(byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||||
(byte) 'f' };
|
(byte) 'f' };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the hex value for a supplied input stream.
|
* Computes and returns the hex value for a supplied input stream.
|
||||||
|
@ -281,7 +288,8 @@ public class CryptoStreams {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes and returns the unencoded value for an input stream which is encoded in hex.
|
* Computes and returns the unencoded value for an input stream which is
|
||||||
|
* encoded in hex.
|
||||||
*
|
*
|
||||||
* @param supplier
|
* @param supplier
|
||||||
* the input stream factory
|
* the input stream factory
|
||||||
|
|
|
@ -166,8 +166,13 @@ public class Pems {
|
||||||
* private key in pem encoded format.
|
* private key in pem encoded format.
|
||||||
* @see Pems#privateKeySpec(InputSupplier)
|
* @see Pems#privateKeySpec(InputSupplier)
|
||||||
*/
|
*/
|
||||||
public static KeySpec privateKeySpec(String pem) throws IOException {
|
public static KeySpec privateKeySpec(String pem) {
|
||||||
return privateKeySpec(InputSuppliers.of(pem));
|
try {
|
||||||
|
return privateKeySpec(InputSuppliers.of(pem));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Throwables.propagate(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -302,6 +307,8 @@ public class Pems {
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws CertificateEncodingException
|
* @throws CertificateEncodingException
|
||||||
*/
|
*/
|
||||||
|
// TODO: understand why pem isn't passing SshKeysTest.testCanGenerate where
|
||||||
|
// keys are checked to match.
|
||||||
public static String pem(PrivateKey key) {
|
public static String pem(PrivateKey key) {
|
||||||
String marker = key instanceof RSAPrivateCrtKey ? PRIVATE_PKCS1_MARKER : PRIVATE_PKCS8_MARKER;
|
String marker = key instanceof RSAPrivateCrtKey ? PRIVATE_PKCS1_MARKER : PRIVATE_PKCS8_MARKER;
|
||||||
return pem(key instanceof RSAPrivateCrtKey ? getEncoded(RSAPrivateCrtKey.class.cast(key)) : key.getEncoded(),
|
return pem(key instanceof RSAPrivateCrtKey ? getEncoded(RSAPrivateCrtKey.class.cast(key)) : key.getEncoded(),
|
||||||
|
|
|
@ -18,23 +18,46 @@
|
||||||
*/
|
*/
|
||||||
package org.jclouds.crypto;
|
package org.jclouds.crypto;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Throwables.propagate;
|
||||||
|
import static org.jclouds.crypto.CryptoStreams.base64;
|
||||||
|
import static org.jclouds.crypto.CryptoStreams.hex;
|
||||||
|
import static org.jclouds.crypto.CryptoStreams.md5;
|
||||||
|
import static org.jclouds.crypto.Pems.privateKeySpec;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.StringWriter;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||||
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.bouncycastle.openssl.PEMWriter;
|
||||||
|
import org.jclouds.encryption.internal.Base64;
|
||||||
|
import org.jclouds.io.InputSuppliers;
|
||||||
|
import org.jclouds.util.Strings2;
|
||||||
|
|
||||||
import com.google.common.annotations.Beta;
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableMap.Builder;
|
import com.google.common.collect.ImmutableMap.Builder;
|
||||||
import com.google.common.io.ByteArrayDataOutput;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.InputSupplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates OpenSSH RSA keypairs
|
* Utilities for ssh key pairs
|
||||||
*
|
*
|
||||||
* @author Adrian Cole
|
* @author Adrian Cole
|
||||||
* @see <a href=
|
* @see <a href=
|
||||||
|
@ -43,9 +66,57 @@ import com.google.common.io.ByteStreams;
|
||||||
*/
|
*/
|
||||||
@Beta
|
@Beta
|
||||||
public class SshKeys {
|
public class SshKeys {
|
||||||
// All data type encoding is defined in the section #5 of RFC #4251. string
|
|
||||||
// and mpint (multiple precision integer) types are encoded this way :
|
/**
|
||||||
private static final byte[] sshrsa = new byte[] { 0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a' };
|
* 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) throws IOException {
|
||||||
|
return publicKeySpecFromOpenSSH(InputSuppliers.of(idRsaPub));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(Strings2.toStringAndClose(stream));
|
||||||
|
checkArgument(Iterables.size(parts) >= 2 && "ssh-rsa".equals(Iterables.get(parts, 0)),
|
||||||
|
"bad format, should be: ssh-rsa AAAAB3...");
|
||||||
|
stream = new ByteArrayInputStream(Base64.decode(Iterables.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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -55,7 +126,8 @@ public class SshKeys {
|
||||||
* @see Crypto#rsaKeyPairGenerator()
|
* @see Crypto#rsaKeyPairGenerator()
|
||||||
*/
|
*/
|
||||||
public static KeyPair generateRsaKeyPair(KeyPairGenerator generator) {
|
public static KeyPair generateRsaKeyPair(KeyPairGenerator generator) {
|
||||||
generator.initialize(2048);
|
SecureRandom rand = new SecureRandom();
|
||||||
|
generator.initialize(2048, rand);
|
||||||
return generator.genKeyPair();
|
return generator.genKeyPair();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +139,7 @@ public class SshKeys {
|
||||||
try {
|
try {
|
||||||
return generate(KeyPairGenerator.getInstance("RSA"));
|
return generate(KeyPairGenerator.getInstance("RSA"));
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
Throwables.propagate(e);
|
propagate(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,31 +153,91 @@ public class SshKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String encodeAsOpenSSH(RSAPublicKey key) {
|
public static String encodeAsOpenSSH(RSAPublicKey key) {
|
||||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
byte[] keyBlob = keyBlob(key.getPublicExponent(), key.getModulus());
|
||||||
/* encode the "ssh-rsa" string */
|
return "ssh-rsa " + base64(keyBlob);
|
||||||
out.write(sshrsa);
|
|
||||||
/* Encode the public exponent */
|
|
||||||
BigInteger e = key.getPublicExponent();
|
|
||||||
byte[] data = e.toByteArray();
|
|
||||||
encodeUint32(data.length, out);
|
|
||||||
out.write(data);
|
|
||||||
/* Encode the modulus */
|
|
||||||
BigInteger m = key.getModulus();
|
|
||||||
data = m.toByteArray();
|
|
||||||
encodeUint32(data.length, out);
|
|
||||||
out.write(data);
|
|
||||||
return "ssh-rsa " + CryptoStreams.base64(out.toByteArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String encodeAsPem(RSAPrivateKey key) {
|
public static String encodeAsPem(RSAPrivateKey key) {
|
||||||
return Pems.pem(key.getEncoded(), Pems.PRIVATE_PKCS1_MARKER, 64);
|
StringWriter stringWriter = new StringWriter();
|
||||||
|
PEMWriter pemFormatWriter = new PEMWriter(stringWriter);
|
||||||
|
try {
|
||||||
|
pemFormatWriter.writeObject(key);
|
||||||
|
pemFormatWriter.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Throwables.propagate(e);
|
||||||
|
}
|
||||||
|
return stringWriter.toString();
|
||||||
|
// TODO: understand why pem isn't passing testCanGenerate where keys are
|
||||||
|
// checked to match.
|
||||||
|
// return pem(key.getEncoded(), PRIVATE_PKCS1_MARKER, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void encodeUint32(int value, ByteArrayDataOutput out) {
|
/**
|
||||||
out.write((byte) ((value >>> 24) & 0xff));
|
* @param privateKeyPEM
|
||||||
out.write((byte) ((value >>> 16) & 0xff));
|
* RSA private key in PEM format
|
||||||
out.write((byte) ((value >>> 8) & 0xff));
|
* @param publicKeyOpenSSH
|
||||||
out.write((byte) (value & 0xff));
|
* RSA public key in OpenSSH format
|
||||||
|
* @return true if the keypair are matches
|
||||||
|
*/
|
||||||
|
public static boolean privateKeyMatchesPublicKey(String privateKeyPEM, String publicKeyOpenSSH) {
|
||||||
|
try {
|
||||||
|
KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
|
||||||
|
checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
|
||||||
|
"incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
|
||||||
|
return privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec.class.cast(privateKeySpec),
|
||||||
|
publicKeySpecFromOpenSSH(publicKeyOpenSSH));
|
||||||
|
} catch (IOException e) {
|
||||||
|
propagate(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the keypair are matches
|
||||||
|
*/
|
||||||
|
public static boolean privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec privateKey, RSAPublicKeySpec publicKey) {
|
||||||
|
return privateKey.getPublicExponent().equals(publicKey.getPublicExponent())
|
||||||
|
&& privateKey.getModulus().equals(publicKey.getModulus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Joiner.on(":").join(Splitter.fixedLength(2).split(hex(md5(keyBlob))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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) {
|
||||||
|
propagate(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://www.ietf.org/rfc/rfc4253.txt
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,48 +18,80 @@
|
||||||
*/
|
*/
|
||||||
package org.jclouds.crypto;
|
package org.jclouds.crypto;
|
||||||
|
|
||||||
|
import static org.jclouds.crypto.SshKeys.fingerprint;
|
||||||
|
import static org.jclouds.crypto.SshKeys.generate;
|
||||||
|
import static org.jclouds.crypto.SshKeys.privateKeyMatchesPublicKey;
|
||||||
|
import static org.jclouds.crypto.SshKeys.publicKeySpecFromOpenSSH;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.interfaces.RSAPrivateCrtKey;
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||||
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jclouds.io.Payloads;
|
import org.jclouds.io.Payloads;
|
||||||
|
import org.jclouds.util.Strings2;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Adrian Cole
|
* @author Adrian Cole
|
||||||
*/
|
*/
|
||||||
@Test(groups = "unit", singleThreaded = true)
|
@Test(groups = "unit", singleThreaded = true, testName = "SshKeysTest")
|
||||||
public class SshKeysTest {
|
public class SshKeysTest {
|
||||||
|
|
||||||
public static final String SSH_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJvZkkmoabQopH7yd9Ak2xJ34X21c0xXsJ85xbqOyqzwRmCJVHT2EPUhg6PhiozSok42WDKDjFFZ7B1IbtBM+PWUmlUCJ1r2xfLb6TPJqBkDUCbQ5lxupvmGh4gOBxf54Nrv2zS7QOiaNx870QqG8csHP7Mz7dCo9GQ9XydhNt+z4eNXPM5ToPUHRdHf4g9lmXYCdazZ3SqGdK0dwNS+dFVDQ/jzZjA31WBx45m2k/PQMIoQnlPHlJO5vyTT/O39UAwdBp8tK5Awt3azhku45mm03/+4CxObGKt6p3fvP7xlN0FsnwsSkn6IJe5J+blpSHuLDqjG9OhjYAvnf6MrZR";
|
String expectedFingerprint = "2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9";
|
||||||
public static final String SSH_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDJvZkkmoabQopH\\n7yd9Ak2xJ34X21c0xXsJ85xbqOyqzwRmCJVHT2EPUhg6PhiozSok42WDKDjFFZ7B\\n1IbtBM+PWUmlUCJ1r2xfLb6TPJqBkDUCbQ5lxupvmGh4gOBxf54Nrv2zS7QOiaNx\\n870QqG8csHP7Mz7dCo9GQ9XydhNt+z4eNXPM5ToPUHRdHf4g9lmXYCdazZ3SqGdK\\n0dwNS+dFVDQ/jzZjA31WBx45m2k/PQMIoQnlPHlJO5vyTT/O39UAwdBp8tK5Awt3\\nazhku45mm03/+4CxObGKt6p3fvP7xlN0FsnwsSkn6IJe5J+blpSHuLDqjG9OhjYA\\nvnf6MrZRAgMBAAECggEBAMDzwHeL/FafS9cFXEVqYJih5y42MbBdeRLJl7DrXoD4\\nQ4K7jtuHhpO6t0VtgvRgVoC1pa/OVo3Z4eANv4cO5N58Tb35aRwaTpKyE+aLPlPR\\nc4IAgJbDrBJUOQeYbBLiNm9sAWbtbyfAaT1iHGDEWJGeCzAlkWik4ugXlZeza13y\\nC4hg2yUymju27nSIWcEfAo7Zmw8CTy32edLGUNq8qu7KYSaocKvf522Kjl5p6cly\\nnd+OhcFA4pSTeKkgjb1dEHw2p4JVX/srHCpySo4ERtHAFyVyTIhsP5GICAq856U5\\nGMSIT1Hja+6q/FEDqT2R32ju62VfQBS2kSfeBlzot6kCgYEA/1p6Xljvn6wrYklQ\\n046E7xp7jlx2yKWnbIjRf5QFpK5Czb53I+mSkQcDF3imaZK+f/xz1OURmGv6phsA\\nFdU+UOdzGki7DYCNuTuz9LhdpIqxGqlcvnvOP9A6ouRGTz45VX2rqUx5A2ou0SPW\\n7ok++JfvRGj4VC1G2005ht90/98CgYEAykBeNHnChXmxRv5ESD28S5ijxFLI5qvG\\nooUIhdVQ2ws7H7bb4Arj4JClu55Y3uOi2uVgPq5Pqy9aLcAALuSJjRQj9+NU9EJS\\nLy1WhBQTRNpTlUshNpg4H5b9sFoMTgz3nrTU24NY73RtoDl19TjiK7O9rTasYlcr\\n36dLejyeT88CgYEAs4vp2OcN7ibABobolyhx3jGvyOTI/MJFm7IEJIFvCmEhRctz\\nuEOms+TLTridwkPVQObAh2Rd39+kySDZCYD8JSTosQWMyKyoeiM5oIv2BBkk+Es3\\nlBQ3bHU8lYaOzW9CHxOTHSJRQI5rxtA9c1H7fg5OxbpNSdrgJJkDJwt+F98CgYEA\\niCBWx58EK+5CQXQ15SGYMJFl+Gd3zLnlEdHUcK+ooiWm/6uFxf/ObIEu616iljJE\\nlGw6ITYVbTSLz6sg9G7hndDmfJvHvDc/NX2gc3lHltoT07IjgqllbO2lhiK1kXrs\\n1ycC9VQscc69UlAacph8scliaskXsYDWiMwC4x0VuMUCgYA/FSHHAXLyBqoFqEpQ\\nRmfgXzUcU/mKEaGRPhAvoAU87Z74mEgaO5Hbv7zmFwWpD4cdW0WybzVv9r5Hs/hS\\n4JGBzx4KPFAygnVeXiftVuM9scycg3RHK907hmlYNZicI7TbUlM5RQ4ngn/LTjXo\\nG+TDTPn3Luw1fvAMEEcdsr9cJw==\\n-----END RSA PRIVATE KEY-----";
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCanGenerate() {
|
public void testCanReadRsaAndCompareFingerprintOnPublicRSAKey() throws IOException {
|
||||||
Map<String, String> map = SshKeys.generate();
|
String pubKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test.pub"));
|
||||||
assert map.get("public").startsWith("ssh-rsa ") : map;
|
RSAPublicKeySpec key = SshKeys.publicKeySpecFromOpenSSH(pubKey);
|
||||||
assert map.get("private").startsWith("-----BEGIN RSA PRIVATE KEY-----") : map;
|
String fingerPrint = fingerprint(key.getPublicExponent(), key.getModulus());
|
||||||
|
assertEquals(fingerPrint, expectedFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncodeAsPem() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
|
public void testCanReadRsaAndCompareFingerprintOnPrivateRSAKey() throws IOException {
|
||||||
String encoded = SshKeys.encodeAsPem((RSAPrivateCrtKey) KeyFactory.getInstance("RSA").generatePrivate(
|
String privKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test"));
|
||||||
Pems.privateKeySpec(Payloads.newStringPayload(PemsTest.PRIVATE_KEY))));
|
RSAPrivateCrtKeySpec key = (RSAPrivateCrtKeySpec) Pems.privateKeySpec(privKey);
|
||||||
assertEquals(encoded.replace("\n", "\\n").trim(), SSH_PRIVATE_KEY);
|
String fingerPrint = fingerprint(key.getPublicExponent(), key.getModulus());
|
||||||
|
assertEquals(fingerPrint, expectedFingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrivateKeyMatchesPublicKeyTyped() throws IOException {
|
||||||
|
String privKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test"));
|
||||||
|
RSAPrivateCrtKeySpec privateKey = (RSAPrivateCrtKeySpec) Pems.privateKeySpec(privKey);
|
||||||
|
String pubKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test.pub"));
|
||||||
|
RSAPublicKeySpec publicKey = publicKeySpecFromOpenSSH(pubKey);
|
||||||
|
assert privateKeyMatchesPublicKey(privateKey, publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrivateKeyMatchesPublicKeyString() throws IOException {
|
||||||
|
String privKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test"));
|
||||||
|
String pubKey = Strings2.toStringAndClose(getClass().getResourceAsStream("/test.pub"));
|
||||||
|
assert privateKeyMatchesPublicKey(privKey, pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCanGenerate() {
|
||||||
|
Map<String, String> map = generate();
|
||||||
|
assert map.get("public").startsWith("ssh-rsa ") : map;
|
||||||
|
assert map.get("private").startsWith("-----BEGIN RSA PRIVATE KEY-----") : map;
|
||||||
|
assert privateKeyMatchesPublicKey(map.get("private"), map.get("public")) : map;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncodeAsOpenSSH() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
|
public void testEncodeAsOpenSSH() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
|
||||||
String encoded = SshKeys.encodeAsOpenSSH((RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(
|
String encoded = SshKeys.encodeAsOpenSSH((RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(
|
||||||
Pems.publicKeySpec(Payloads.newStringPayload(PemsTest.PUBLIC_KEY))));
|
SshKeys.publicKeySpecFromOpenSSH(Payloads.newPayload(getClass().getResourceAsStream("/test.pub")))));
|
||||||
assertEquals(encoded, SSH_PUBLIC_KEY);
|
assertEquals(encoded, Strings2.toStringAndClose(getClass().getResourceAsStream("/test.pub")).trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEAnJvA40x4OK+9nVYTS0N916VMjC6/qYe/IuDUdy6hdW1wz9IO
|
||||||
|
MTS3CPxlE0KuoNO1/M7O7yFso6IragTxUkNqJ2mUOqV0Bf0CEkUIKzeYRGfpx+QM
|
||||||
|
PorHbLQXjjFinWKwibZuv6lqtvqwcsrjW7bpWsz9x+0qqKM0o0UhjUMhTRqZoYxo
|
||||||
|
E2zUH7WA8JRatE/bQkjv/nWBfI+/WzSDhJn7AjIql0Nd4Q+bxohIJEZu8yDw1H6T
|
||||||
|
pd7mw83m6UYBk4eZH79r3d2euuQMUKIunyLbw7vNJJ8qYTJQuNYIiJuWKnzzjxuJ
|
||||||
|
UfumhdOqfjSobznhAjTLUbA/btZCiQ/TasV4cQIDAQABAoIBAEeOn1b8ZN455qDS
|
||||||
|
aKR2JTT4cX6ICckznnEYW9xNMTcPl4FN0HBJTuzLLn/bcyFHOxtVf5YiJpqqCb46
|
||||||
|
ne1hokp54mHdoaLu1Rh19GKS139CH77XA4U8Mh0IOM8e35lcM5/o/LeUeI89Aoyh
|
||||||
|
CbupWvzDN543TsuZLv7/InKCXt/0dXhAQpq3UiBT63EITQbyom5fSPnMzqM3F8jD
|
||||||
|
E9ZqkX4JsnTPC7FQDIpPCaKjG9YCZqoljz+1ssli3mN66V/JKefcCiVoubalmmT2
|
||||||
|
dpvmRtFaKvhAmkWYakYybYg8aDi3YygAHSU1bzxlY4TNiQgPdnTTDAPyeqqVrE1D
|
||||||
|
Chi+18UCgYEAzlk7c+tFwxZ3ryycOe0loFudUNE5rviHhPgbOHoSTooXh0Hq1Vrb
|
||||||
|
2ic+4FbRpoPHLpcLM9LX+arezUdeFBZ8qunjUG6MbUhAeAm/3cfMk+nZg3Skpg8+
|
||||||
|
C1D3hxGX4qdhURHvc2QUH7VIUWbucvPgtL8pt1z5Su/EE1Cb2XVsvu8CgYEAwkqZ
|
||||||
|
4vTZxI0XqJo6BfUnKGJEDC8xeWr10ELPdXLTCuNDpLSYNedQAxZi9XzvbnbWZ/MF
|
||||||
|
Z7IWkzzyAjsX0gpI56cxxtas/chxUboBlUo6ZW8QcPDcU2sKJi318wzElqqvRMNM
|
||||||
|
InfLf8nuPC9hyhe49/lFBBSZJeIo396DuqnTPp8CgYBO4NVVLm5wcLo3gDoH+psT
|
||||||
|
fXHZXuFJ/T7wmVbuc9tjom30CkKWZDD+Z1olr4pcuKr/KEXj/YkJq0OX/Nv9mcr2
|
||||||
|
GooGSPvtGl1qhW+Oe728HPxEv+XghJsXAFBelV8WCR2uO8jotyzqIgYO9+XWk1sm
|
||||||
|
PJzZtvSkrJqrN3kb20NCiQKBgDDVP0hj8jgMnl2qJdtJesYTrLbDRdQWpiHqKOqE
|
||||||
|
Kbca1+2V1ov1z453GfhJpoRFKi6GTl15zWLEdq9I2vvXyesvgrtPSbufnZvE/JDh
|
||||||
|
TzwfZip832O4C5z9AExOcTrNO7A0xfYD1goQXuiRoCqDO+JXrJkR9EwpQ8zAyKsp
|
||||||
|
9AZRAoGAGq3TYpmlI5oucEURHKsHOrIBirHFD+RaXMynxzgwkRnt6Z5Mg10I7Ddr
|
||||||
|
LiGK8/IrF8bg1F7weLVmj93zjvhQTh5yvb1jwVdFGXM2rbR7/P7F6n2f7xM4+lmv
|
||||||
|
Tq7E9Sv8UVuraAwJihlKCuBtpZM1t2JhcuNjXAZngj7R9j5HIZg=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1 @@
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcm8DjTHg4r72dVhNLQ33XpUyMLr+ph78i4NR3LqF1bXDP0g4xNLcI/GUTQq6g07X8zs7vIWyjoitqBPFSQ2onaZQ6pXQF/QISRQgrN5hEZ+nH5Aw+isdstBeOMWKdYrCJtm6/qWq2+rByyuNbtulazP3H7SqoozSjRSGNQyFNGpmhjGgTbNQftYDwlFq0T9tCSO/+dYF8j79bNIOEmfsCMiqXQ13hD5vGiEgkRm7zIPDUfpOl3ubDzebpRgGTh5kfv2vd3Z665AxQoi6fItvDu80knyphMlC41giIm5YqfPOPG4lR+6aF06p+NKhvOeECNMtRsD9u1kKJD9NqxXhx
|
Loading…
Reference in New Issue