View Javadoc

1   /**
2    * Licensed to jclouds, Inc. (jclouds) under one or more
3    * contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  jclouds licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.jclouds.crypto;
20  
21  import static com.google.common.base.Preconditions.checkArgument;
22  import static com.google.common.base.Throwables.propagate;
23  import static org.jclouds.crypto.CryptoStreams.base64;
24  import static org.jclouds.crypto.CryptoStreams.hex;
25  import static org.jclouds.crypto.CryptoStreams.md5;
26  import static org.jclouds.crypto.Pems.privateKeySpec;
27  
28  import java.io.ByteArrayInputStream;
29  import java.io.ByteArrayOutputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.StringWriter;
33  import java.math.BigInteger;
34  import java.security.KeyFactory;
35  import java.security.KeyPair;
36  import java.security.KeyPairGenerator;
37  import java.security.NoSuchAlgorithmException;
38  import java.security.SecureRandom;
39  import java.security.interfaces.RSAPrivateKey;
40  import java.security.interfaces.RSAPublicKey;
41  import java.security.spec.InvalidKeySpecException;
42  import java.security.spec.KeySpec;
43  import java.security.spec.RSAPrivateCrtKeySpec;
44  import java.security.spec.RSAPublicKeySpec;
45  import java.util.Map;
46  
47  import org.bouncycastle.openssl.PEMWriter;
48  import org.jclouds.encryption.internal.Base64;
49  import org.jclouds.io.InputSuppliers;
50  import org.jclouds.util.Strings2;
51  
52  import com.google.common.annotations.Beta;
53  import com.google.common.base.Joiner;
54  import com.google.common.base.Splitter;
55  import com.google.common.base.Throwables;
56  import com.google.common.collect.ImmutableMap;
57  import com.google.common.collect.Iterables;
58  import com.google.common.collect.ImmutableMap.Builder;
59  import com.google.common.io.InputSupplier;
60  
61  /**
62   * Utilities for ssh key pairs
63   * 
64   * @author Adrian Cole
65   * @see <a href=
66   *      "http://stackoverflow.com/questions/3706177/how-to-generate-ssh-compatible-id-rsa-pub-from-java"
67   *      />
68   */
69  @Beta
70  public class SshKeys {
71  
72     /**
73      * Executes {@link Pems#publicKeySpecFromOpenSSH(InputSupplier)} on the string which was OpenSSH
74      * Base64 Encoded {@code id_rsa.pub}
75      * 
76      * @param idRsaPub
77      *           formatted {@code ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...}
78      * @see Pems#publicKeySpecFromOpenSSH(InputSupplier)
79      */
80     public static RSAPublicKeySpec publicKeySpecFromOpenSSH(String idRsaPub) {
81        try {
82           return publicKeySpecFromOpenSSH(InputSuppliers.of(idRsaPub));
83        } catch (IOException e) {
84           propagate(e);
85           return null;
86        }
87     }
88  
89     /**
90      * Returns {@link RSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub}
91      * 
92      * @param supplier
93      *           the input stream factory, formatted {@code ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...}
94      * 
95      * @return the {@link RSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub}
96      * @throws IOException
97      *            if an I/O error occurs
98      */
99     public static RSAPublicKeySpec publicKeySpecFromOpenSSH(InputSupplier<? extends InputStream> supplier)
100             throws IOException {
101       InputStream stream = supplier.getInput();
102       Iterable<String> parts = Splitter.on(' ').split(Strings2.toStringAndClose(stream));
103       checkArgument(Iterables.size(parts) >= 2 && "ssh-rsa".equals(Iterables.get(parts, 0)),
104                "bad format, should be: ssh-rsa AAAAB3...");
105       stream = new ByteArrayInputStream(Base64.decode(Iterables.get(parts, 1)));
106       String marker = new String(readLengthFirst(stream));
107       checkArgument("ssh-rsa".equals(marker), "looking for marker ssh-rsa but got %s", marker);
108       BigInteger publicExponent = new BigInteger(readLengthFirst(stream));
109       BigInteger modulus = new BigInteger(readLengthFirst(stream));
110       return new RSAPublicKeySpec(modulus, publicExponent);
111    }
112 
113    // http://www.ietf.org/rfc/rfc4253.txt
114    static byte[] readLengthFirst(InputStream in) throws IOException {
115       int byte1 = in.read();
116       int byte2 = in.read();
117       int byte3 = in.read();
118       int byte4 = in.read();
119       int length = ((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0));
120       byte[] val = new byte[length];
121       in.read(val, 0, length);
122       return val;
123    }
124 
125    /**
126     * 
127     * @param used
128     *           to generate RSA key pairs
129     * @return new 2048 bit keyPair
130     * @see Crypto#rsaKeyPairGenerator()
131     */
132    public static KeyPair generateRsaKeyPair(KeyPairGenerator generator) {
133       SecureRandom rand = new SecureRandom();
134       generator.initialize(2048, rand);
135       return generator.genKeyPair();
136    }
137 
138    /**
139     * return a "public" -> rsa public key, "private" -> its corresponding private key
140     */
141    public static Map<String, String> generate() {
142       try {
143          return generate(KeyPairGenerator.getInstance("RSA"));
144       } catch (NoSuchAlgorithmException e) {
145          propagate(e);
146          return null;
147       }
148    }
149 
150    public static Map<String, String> generate(KeyPairGenerator generator) {
151       KeyPair pair = generateRsaKeyPair(generator);
152       Builder<String, String> builder = ImmutableMap.<String, String> builder();
153       builder.put("public", encodeAsOpenSSH(RSAPublicKey.class.cast(pair.getPublic())));
154       builder.put("private", encodeAsPem(RSAPrivateKey.class.cast(pair.getPrivate())));
155       return builder.build();
156    }
157 
158    public static String encodeAsOpenSSH(RSAPublicKey key) {
159       byte[] keyBlob = keyBlob(key.getPublicExponent(), key.getModulus());
160       return "ssh-rsa " + base64(keyBlob);
161    }
162 
163    public static String encodeAsPem(RSAPrivateKey key) {
164       StringWriter stringWriter = new StringWriter();
165       PEMWriter pemFormatWriter = new PEMWriter(stringWriter);
166       try {
167          pemFormatWriter.writeObject(key);
168          pemFormatWriter.close();
169       } catch (IOException e) {
170          Throwables.propagate(e);
171       }
172       return stringWriter.toString();
173       // TODO: understand why pem isn't passing testCanGenerate where keys are
174       // checked to match.
175       // return pem(key.getEncoded(), PRIVATE_PKCS1_MARKER, 64);
176    }
177 
178    /**
179     * @param privateKeyPEM
180     *           RSA private key in PEM format
181     * @param publicKeyOpenSSH
182     *           RSA public key in OpenSSH format
183     * @return true if the keypairs match
184     */
185    public static boolean privateKeyMatchesPublicKey(String privateKeyPEM, String publicKeyOpenSSH) {
186       KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
187       checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
188                "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
189       return privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec.class.cast(privateKeySpec),
190                publicKeySpecFromOpenSSH(publicKeyOpenSSH));
191    }
192 
193    /**
194     * @return true if the keypairs match
195     */
196    public static boolean privateKeyMatchesPublicKey(RSAPrivateCrtKeySpec privateKey, RSAPublicKeySpec publicKey) {
197       return privateKey.getPublicExponent().equals(publicKey.getPublicExponent())
198                && privateKey.getModulus().equals(publicKey.getModulus());
199    }
200 
201    /**
202     * @return true if the keypair has the same fingerprint as supplied
203     */
204    public static boolean privateKeyHasFingerprint(RSAPrivateCrtKeySpec privateKey, String fingerprint) {
205       return fingerprint(privateKey.getPublicExponent(), privateKey.getModulus()).equals(fingerprint);
206    }
207 
208    /**
209     * @param privateKeyPEM
210     *           RSA private key in PEM format
211     * @param fingerprint
212     *           ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
213     * @return true if the keypair has the same fingerprint as supplied
214     */
215    public static boolean privateKeyHasFingerprint(String privateKeyPEM, String fingerprint) {
216       KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
217       checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
218                "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
219       return privateKeyHasFingerprint(RSAPrivateCrtKeySpec.class.cast(privateKeySpec), fingerprint);
220    }
221 
222    /**
223     * @param privateKeyPEM
224     *           RSA private key in PEM format
225     * @return fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
226     */
227    public static String fingerprintPrivateKey(String privateKeyPEM) {
228       KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
229       checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
230                "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
231       RSAPrivateCrtKeySpec certKeySpec = RSAPrivateCrtKeySpec.class.cast(privateKeySpec);
232       return fingerprint(certKeySpec.getPublicExponent(), certKeySpec.getModulus());
233    }
234 
235    /**
236     * @param publicKeyOpenSSH
237     *           RSA public key in OpenSSH format
238     * @return fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
239     */
240    public static String fingerprintPublicKey(String publicKeyOpenSSH) {
241       RSAPublicKeySpec publicKeySpec = publicKeySpecFromOpenSSH(publicKeyOpenSSH);
242       return fingerprint(publicKeySpec.getPublicExponent(), publicKeySpec.getModulus());
243    }
244 
245    /**
246     * @return true if the keypair has the same SHA1 fingerprint as supplied
247     */
248    public static boolean privateKeyHasSha1(RSAPrivateCrtKeySpec privateKey, String fingerprint) {
249       return sha1(privateKey).equals(fingerprint);
250    }
251 
252    /**
253     * @param privateKeyPEM
254     *           RSA private key in PEM format
255     * @param sha1HexColonDelimited
256     *           ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
257     * @return true if the keypair has the same fingerprint as supplied
258     */
259    public static boolean privateKeyHasSha1(String privateKeyPEM, String sha1HexColonDelimited) {
260       KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
261       checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
262                "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
263       return privateKeyHasSha1(RSAPrivateCrtKeySpec.class.cast(privateKeySpec), sha1HexColonDelimited);
264    }
265 
266    /**
267     * @param privateKeyPEM
268     *           RSA private key in PEM format
269     * @return sha1HexColonDelimited ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
270     */
271    public static String sha1PrivateKey(String privateKeyPEM) {
272       KeySpec privateKeySpec = privateKeySpec(privateKeyPEM);
273       checkArgument(privateKeySpec instanceof RSAPrivateCrtKeySpec,
274                "incorrect format expected RSAPrivateCrtKeySpec was %s", privateKeySpec);
275       RSAPrivateCrtKeySpec certKeySpec = RSAPrivateCrtKeySpec.class.cast(privateKeySpec);
276       return sha1(certKeySpec);
277    }
278 
279    /**
280     * Create a SHA-1 digest of the DER encoded private key.
281     * 
282     * @param publicExponent
283     * @param modulus
284     * 
285     * @return hex sha1HexColonDelimited ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
286     */
287    public static String sha1(RSAPrivateCrtKeySpec privateKey) {
288       try {
289          String sha1 = Joiner.on(":").join(
290                   Splitter.fixedLength(2).split(
291                            hex(CryptoStreams.sha1(KeyFactory.getInstance("RSA").generatePrivate(privateKey)
292                                     .getEncoded()))));
293          return sha1;
294       } catch (InvalidKeySpecException e) {
295          propagate(e);
296          return null;
297       } catch (NoSuchAlgorithmException e) {
298          propagate(e);
299          return null;
300       }
301    }
302 
303    /**
304     * @return true if the keypair has the same fingerprint as supplied
305     */
306    public static boolean publicKeyHasFingerprint(RSAPublicKeySpec publicKey, String fingerprint) {
307       return fingerprint(publicKey.getPublicExponent(), publicKey.getModulus()).equals(fingerprint);
308    }
309 
310    /**
311     * @param publicKeyOpenSSH
312     *           RSA public key in OpenSSH format
313     * @param fingerprint
314     *           ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
315     * @return true if the keypair has the same fingerprint as supplied
316     */
317    public static boolean publicKeyHasFingerprint(String publicKeyOpenSSH, String fingerprint) {
318       return publicKeyHasFingerprint(publicKeySpecFromOpenSSH(publicKeyOpenSSH), fingerprint);
319    }
320 
321    /**
322     * Create a fingerprint per the following <a
323     * href="http://tools.ietf.org/html/draft-friedl-secsh-fingerprint-00" >spec</a>
324     * 
325     * @param publicExponent
326     * @param modulus
327     * 
328     * @return hex fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
329     */
330    public static String fingerprint(BigInteger publicExponent, BigInteger modulus) {
331       byte[] keyBlob = keyBlob(publicExponent, modulus);
332       return Joiner.on(":").join(Splitter.fixedLength(2).split(hex(md5(keyBlob))));
333    }
334 
335    public static byte[] keyBlob(BigInteger publicExponent, BigInteger modulus) {
336       try {
337          ByteArrayOutputStream out = new ByteArrayOutputStream();
338          writeLengthFirst("ssh-rsa".getBytes(), out);
339          writeLengthFirst(publicExponent.toByteArray(), out);
340          writeLengthFirst(modulus.toByteArray(), out);
341          return out.toByteArray();
342       } catch (IOException e) {
343          propagate(e);
344          return null;
345       }
346    }
347 
348    // http://www.ietf.org/rfc/rfc4253.txt
349    static void writeLengthFirst(byte[] array, ByteArrayOutputStream out) throws IOException {
350       out.write((array.length >>> 24) & 0xFF);
351       out.write((array.length >>> 16) & 0xFF);
352       out.write((array.length >>> 8) & 0xFF);
353       out.write((array.length >>> 0) & 0xFF);
354       if (array.length == 1 && array[0] == (byte) 0x00)
355          out.write(new byte[0]);
356       else
357          out.write(array);
358    }
359 }