Skip to content

Commit

Permalink
Merge pull request #279 from hierynomus/issue-276
Browse files Browse the repository at this point in the history
Support for OpenSSH new key file format (fixes #276)
  • Loading branch information
hierynomus authored Oct 31, 2016
2 parents 63927a3 + 677f482 commit d95b4db
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 257 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

import java.util.Arrays;

import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.CURVE_ED25519_SHA512;

/**
* Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality.
* The code uses the equality of the keys as an indicator whether they're the same during host key verification.
Expand All @@ -32,7 +34,7 @@ public class Ed25519PublicKey extends EdDSAPublicKey {
public Ed25519PublicKey(EdDSAPublicKeySpec spec) {
super(spec);

EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512");
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512);
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed 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 com.hierynomus.sshj.userauth.keyprovider;

import java.io.BufferedReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.schmizz.sshj.common.*;
import net.schmizz.sshj.common.Buffer.PlainBuffer;
import net.schmizz.sshj.userauth.keyprovider.BaseFileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyFormat;

import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.CURVE_ED25519_SHA512;

/**
* Reads a key file in the new OpenSSH format.
* The format is described in the following document: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
*/
public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
private static final Logger logger = LoggerFactory.getLogger(OpenSSHKeyV1KeyFile.class);
private static final String BEGIN = "-----BEGIN ";
private static final String END = "-----END ";
private static final byte[] AUTH_MAGIC = "openssh-key-v1\0".getBytes();
public static final String OPENSSH_PRIVATE_KEY = "OPENSSH PRIVATE KEY-----";

public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {

@Override
public FileKeyProvider create() {
return new OpenSSHKeyV1KeyFile();
}

@Override
public String getName() {
return KeyFormat.OpenSSHv1.name();
}
}

@Override
protected KeyPair readKeyPair() throws IOException {
BufferedReader reader = new BufferedReader(resource.getReader());
try {
if (!checkHeader(reader)) {
throw new IOException("This key is not in 'openssh-key-v1' format");
}

String keyFile = readKeyFile(reader);
byte[] decode = Base64.decode(keyFile);
PlainBuffer keyBuffer = new PlainBuffer(decode);
return readDecodedKeyPair(keyBuffer);

} catch (GeneralSecurityException e) {
throw new SSHRuntimeException(e);
} finally {
IOUtils.closeQuietly(reader);
}
}

private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOException, GeneralSecurityException {
byte[] bytes = new byte[AUTH_MAGIC.length];
keyBuffer.readRawBytes(bytes); // byte[] AUTH_MAGIC
if (!ByteArrayUtils.equals(bytes, 0, AUTH_MAGIC, 0, AUTH_MAGIC.length)) {
throw new IOException("This key does not contain the 'openssh-key-v1' format magic header");
}

String cipherName = keyBuffer.readString(); // string ciphername
String kdfName = keyBuffer.readString(); // string kdfname
String kdfOptions = keyBuffer.readString(); // string kdfoptions

int nrKeys = keyBuffer.readUInt32AsInt(); // int number of keys N; Should be 1
if (nrKeys != 1) {
throw new IOException("We don't support having more than 1 key in the file (yet).");
}
PublicKey publicKey = readPublicKey(new PlainBuffer(keyBuffer.readBytes())); // string publickey1
PlainBuffer privateKeyBuffer = new PlainBuffer(keyBuffer.readBytes()); // string (possibly) encrypted, padded list of private keys
if ("none".equals(cipherName)) {
logger.debug("Reading unencrypted keypair");
return readUnencrypted(privateKeyBuffer, publicKey);
} else {
logger.info("Keypair is encrypted with: " + cipherName + ", " + kdfName + ", " + kdfOptions);
throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
}
}

private PublicKey readPublicKey(final PlainBuffer plainBuffer) throws Buffer.BufferException, GeneralSecurityException {
return KeyType.fromString(plainBuffer.readString()).readPubKeyFromBuffer(plainBuffer);
}

private String readKeyFile(final BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line = reader.readLine();
while (!line.startsWith(END)) {
sb.append(line);
line = reader.readLine();
}
return sb.toString();
}

private boolean checkHeader(final BufferedReader reader) throws IOException {
String line = reader.readLine();
while (line != null && !line.startsWith(BEGIN)) {
line = reader.readLine();
}
line = line.substring(BEGIN.length());
return line.startsWith(OPENSSH_PRIVATE_KEY);
}

private KeyPair readUnencrypted(final PlainBuffer keyBuffer, final PublicKey publicKey) throws IOException, GeneralSecurityException {
int privKeyListSize = keyBuffer.available();
if (privKeyListSize % 8 != 0) {
throw new IOException("The private key section must be a multiple of the block size (8)");
}
int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1
int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2
if (checkInt1 != checkInt2) {
throw new IOException("The checkInts differed, the key was not correctly decoded.");
}
// The private key section contains both the public key and the private key
String keyType = keyBuffer.readString(); // string keytype
logger.info("Read key type: {}", keyType);

byte[] pubKey = keyBuffer.readBytes(); // string publickey (again...)
keyBuffer.readUInt32();
byte[] privKey = new byte[32];
keyBuffer.readRawBytes(privKey); // string privatekey
keyBuffer.readRawBytes(new byte[32]); // string publickey (again...)
String comment = keyBuffer.readString(); // string comment
byte[] padding = new byte[keyBuffer.available()];
keyBuffer.readRawBytes(padding); // char[] padding
for (int i = 0; i < padding.length; i++) {
if ((int) padding[i] != i + 1) {
throw new IOException("Padding of key format contained wrong byte at position: " + i);
}
}
return new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName(CURVE_ED25519_SHA512))));
}
}
3 changes: 2 additions & 1 deletion src/main/java/net/schmizz/sshj/DefaultConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import net.schmizz.sshj.transport.random.JCERandom;
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import org.slf4j.Logger;
Expand Down Expand Up @@ -113,7 +114,7 @@ protected void initRandomFactory(boolean bouncyCastleRegistered) {

protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) {
if (bouncyCastleRegistered) {
setFileKeyProviderFactories(new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory());
setFileKeyProviderFactories(new OpenSSHKeyV1KeyFile.Factory(), new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory(), new PuTTYKeyFile.Factory());
}
}

Expand Down
11 changes: 8 additions & 3 deletions src/main/java/net/schmizz/sshj/SSHClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ public void authPassword(String username, PasswordFinder pfinder, PasswordUpdate
public void authPublickey(String username)
throws UserAuthException, TransportException {
final String base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator;
authPublickey(username, base + "id_rsa", base + "id_dsa");
authPublickey(username, base + "id_rsa", base + "id_dsa", base + "id_ed25519", base + "id_ecdsa");
}

/**
Expand Down Expand Up @@ -524,8 +524,13 @@ public KeyProvider loadKeys(String location, char[] passphrase)
}

/**
* Creates a {@link KeyProvider} instance from given location on the file system. Currently only PKCS8 format
* private key files are supported (OpenSSH uses this format).
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
* <ul>
* <li>PKCS8 (OpenSSH uses this format)</li>
* <li>PKCS5</li>
* <li>Putty keyfile</li>
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
* </ul>
* <p/>
*
* @param location the location of the key file
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed 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 net.schmizz.sshj.userauth.keyprovider;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;

import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.password.*;

public abstract class BaseFileKeyProvider implements FileKeyProvider {
protected Resource<?> resource;
protected PasswordFinder pwdf;
protected KeyPair kp;

protected KeyType type;

@Override
public void init(Reader location) {
assert location != null;
resource = new PrivateKeyReaderResource(location);
}

@Override
public void init(Reader location, PasswordFinder pwdf) {
init(location);
this.pwdf = pwdf;
}

@Override
public void init(File location) {
assert location != null;
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
}

@Override
public void init(File location, PasswordFinder pwdf) {
init(location);
this.pwdf = pwdf;
}

@Override
public void init(String privateKey, String publicKey) {
assert privateKey != null;
assert publicKey == null;
resource = new PrivateKeyStringResource(privateKey);
}

@Override
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
init(privateKey, publicKey);
this.pwdf = pwdf;
}

@Override
public PrivateKey getPrivate()
throws IOException {
return kp != null ? kp.getPrivate() : (kp = readKeyPair()).getPrivate();
}

@Override
public PublicKey getPublic()
throws IOException {
return kp != null ? kp.getPublic() : (kp = readKeyPair()).getPublic();
}

@Override
public KeyType getType()
throws IOException {
return type != null ? type : (type = KeyType.fromKey(getPublic()));
}


protected abstract KeyPair readKeyPair() throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public enum KeyFormat {
PKCS5,
PKCS8,
OpenSSH,
OpenSSHv1,
PuTTY,
Unknown
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import net.schmizz.sshj.common.IOUtils;

import java.io.*;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;

public class KeyProviderUtil {

Expand Down Expand Up @@ -88,10 +89,12 @@ private static String readHeader(Reader privateKey) throws IOException {

private static KeyFormat keyFormatFromHeader(String header, boolean separatePubKey) {
if (header.startsWith("-----BEGIN") && header.endsWith("PRIVATE KEY-----")) {
if (separatePubKey) {
if (separatePubKey && header.contains(OpenSSHKeyV1KeyFile.OPENSSH_PRIVATE_KEY)) {
return KeyFormat.OpenSSHv1;
} else if (separatePubKey) {
// Can delay asking for password since have unencrypted pubkey
return KeyFormat.OpenSSH;
} else if (header.indexOf("BEGIN PRIVATE KEY") != -1 || header.indexOf("BEGIN ENCRYPTED PRIVATE KEY") != -1) {
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
return KeyFormat.PKCS8;
} else {
return KeyFormat.PKCS5;
Expand Down
Loading

0 comments on commit d95b4db

Please sign in to comment.