Skip to content

Commit

Permalink
Added support for reading SSH RSA public keys
Browse files Browse the repository at this point in the history
  • Loading branch information
exceptionfactory committed Jul 12, 2024
1 parent 0e3e6e4 commit 301b566
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 67 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ The ssh-ed25519 type reads SSH public keys encoded according to the SSH protocol

The ssh-ed25519 type encrypts a File Key with ChaCha20-Poly1305.

The ssh-rsa type reads SSH public keys encoded according to the SSH protocol.

- [RFC 4253](https://www.rfc-editor.org/rfc/rfc4253) The Secure Shell (SSH) Transport Layer Protocol

The ssh-rsa type encrypts a File Key with RSA-OAEP.

- [RFC 8017](https://www.rfc-editor.org/rfc/rfc8017) PKCS #1: RSA Cryptography Specifications Version 2.2
Expand Down Expand Up @@ -264,7 +268,8 @@ key encoded according to [RFC 8709 Section 4](https://www.rfc-editor.org/rfc/rfc
The `SshRsaRecipientStanzaReaderFactory` creates instances of `RecipientStanzaReader` using an RSA private key or an
[OpenSSH Version 1 Private Key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key).

The `SshRsaRecipientStanzaWriterFactory` creates instances of `RecipientStanzaWriter` using an RSA public key.
The `SshRsaRecipientStanzaWriterFactory` creates instances of `RecipientStanzaWriter` using an RSA public key or an SSH
RSA public key encoded according to [RFC 4253 Section 6.6](https://www.rfc-editor.org/rfc/rfc4253#section-6.6).

The SSH Ed25519 implementation uses Elliptic Curve Diffie-Hellman with Curve25519 as defined in
[RFC 7748 Section 6.1](https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1). As integrated in the age reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,83 +16,29 @@
package com.exceptionfactory.jagged.ssh;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;

/**
* SSH Ed25519 Public Key Reader implementation based on ssh-ed25519 format described in RFC 8709 Section 4
*/
class SshEd25519PublicKeyReader extends SshPublicKeyReader<Ed25519PublicKey> {
private static final int ENCODED_LENGTH = 68;

private static final String ALGORITHM_FORMAT = SshEd25519RecipientIndicator.STANZA_TYPE.getIndicator();

private static final byte[] ALGORITHM = ALGORITHM_FORMAT.getBytes(StandardCharsets.UTF_8);

private static final byte SPACE_SEPARATOR = 32;

private static final Base64.Decoder DECODER = Base64.getDecoder();
/**
* SSH Ed25519 Public Key Reader constructor configures the expected ssh-ed25519 algorithm
*/
SshEd25519PublicKeyReader() {
super(SshEd25519RecipientIndicator.STANZA_TYPE.getIndicator());
}

/**
* Read Public Key
* Read Ed25519 Public Key from block of 32 bytes
*
* @param inputBuffer Input Buffer to be read
* @param decodedBuffer Buffer of bytes decoded from Base64 public key
* @return Ed25519 Public Key
* @throws GeneralSecurityException Thrown on failures to parse input buffer
* @throws GeneralSecurityException Thrown when block length not equal to expected key length
*/
@Override
public Ed25519PublicKey read(final ByteBuffer inputBuffer) throws GeneralSecurityException {
Objects.requireNonNull(inputBuffer, "Input Buffer required");

final byte[] algorithm = new byte[ALGORITHM.length];
inputBuffer.get(algorithm);

final Ed25519PublicKey publicKey;

if (Arrays.equals(ALGORITHM, algorithm)) {
final byte separator = inputBuffer.get();
if (SPACE_SEPARATOR == separator) {
publicKey = readEncodedPublicKey(inputBuffer);
} else {
throw new InvalidKeyException("Algorithm format space separator not found");
}
} else {
throw new InvalidKeyException(String.format("Public key algorithm format [%s] not found", ALGORITHM_FORMAT));
}

return publicKey;
}

private Ed25519PublicKey readEncodedPublicKey(final ByteBuffer inputBuffer) throws InvalidKeyException {
final Ed25519PublicKey publicKey;

if (inputBuffer.remaining() >= ENCODED_LENGTH) {
final byte[] encoded = new byte[ENCODED_LENGTH];
inputBuffer.get(encoded);

final byte[] decoded = DECODER.decode(encoded);
final ByteBuffer decodedBuffer = ByteBuffer.wrap(decoded);

final byte[] algorithm = readBlock(decodedBuffer);
if (Arrays.equals(ALGORITHM, algorithm)) {
publicKey = readPublicKey(decodedBuffer);
} else {
throw new InvalidKeyException(String.format("Encoded key algorithm [%s] not found", ALGORITHM_FORMAT));
}
} else {
final int remaining = inputBuffer.remaining();
final String message = String.format("Encoded public key length [%d] less than required [%d]", remaining, ENCODED_LENGTH);
throw new InvalidKeyException(message);
}

return publicKey;
}

private Ed25519PublicKey readPublicKey(final ByteBuffer decodedBuffer) throws InvalidKeyException {
protected Ed25519PublicKey readPublicKey(final ByteBuffer decodedBuffer) throws GeneralSecurityException {
final byte[] block = readBlock(decodedBuffer);
if (EllipticCurveKeyType.ED25519.getKeyLength() == block.length) {
return new Ed25519PublicKey(block);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,79 @@
package com.exceptionfactory.jagged.ssh;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;

/**
* SSH Public Key Reader with shared methods
*
* @param <T> Public Key Type
*/
abstract class SshPublicKeyReader<T extends PublicKey> implements PublicKeyReader<T> {
private static final int INTEGER_LENGTH = 4;

private static final byte SPACE_SEPARATOR = 32;

private static final Base64.Decoder DECODER = Base64.getDecoder();

private final String keyAlgorithm;

private final byte[] keyAlgorithmBinary;

/**
* SSH Public Key Reader constructor with required key algorithm
*
* @param keyAlgorithm Public key algorithm
*/
SshPublicKeyReader(final String keyAlgorithm) {
this.keyAlgorithm = Objects.requireNonNull(keyAlgorithm, "Algorithm required");
this.keyAlgorithmBinary = keyAlgorithm.getBytes(StandardCharsets.US_ASCII);
}

/**
* Read Public Key
*
* @param inputBuffer Input Buffer to be read
* @return RSA Public Key
* @throws GeneralSecurityException Thrown on failures to parse input buffer
*/
@Override
public T read(final ByteBuffer inputBuffer) throws GeneralSecurityException {
Objects.requireNonNull(inputBuffer, "Input Buffer required");

final T publicKey;

final byte[] algorithm = new byte[keyAlgorithmBinary.length];
inputBuffer.get(algorithm);

if (Arrays.equals(keyAlgorithmBinary, algorithm)) {
final byte separator = inputBuffer.get();
if (SPACE_SEPARATOR == separator) {
publicKey = readEncodedPublicKey(inputBuffer);
} else {
throw new InvalidKeyException("Algorithm format space separator not found");
}
} else {
throw new InvalidKeyException(String.format("Public key algorithm format [%s] not found", keyAlgorithm));
}

return publicKey;
}

/**
* Read Public Key from decoded buffer
*
* @param decodedBuffer Buffer of bytes decoded from Base64 public key
* @return Public Key
* @throws GeneralSecurityException Thrown when the decoded buffer does not contain valid key information
*/
protected abstract T readPublicKey(ByteBuffer decodedBuffer) throws GeneralSecurityException;

/**
* Read length-delimited array of bytes
*
Expand All @@ -33,6 +97,10 @@ abstract class SshPublicKeyReader<T extends PublicKey> implements PublicKeyReade
* @throws InvalidKeyException Thrown on invalid number of bytes indicated to be read
*/
protected byte[] readBlock(final ByteBuffer buffer) throws InvalidKeyException {
if (buffer.remaining() < INTEGER_LENGTH) {
throw new InvalidKeyException(String.format("Public Key buffer size [%d] less than required", buffer.remaining()));
}

final int length = buffer.getInt();
if (length > buffer.remaining()) {
throw new InvalidKeyException(String.format("Public Key block length [%d] not valid", length));
Expand All @@ -42,4 +110,40 @@ protected byte[] readBlock(final ByteBuffer buffer) throws InvalidKeyException {
buffer.get(block);
return block;
}

private T readEncodedPublicKey(final ByteBuffer inputBuffer) throws GeneralSecurityException {
final T publicKey;

final byte[] encoded = readEncodedBytes(inputBuffer);
final byte[] decoded = DECODER.decode(encoded);
final ByteBuffer decodedBuffer = ByteBuffer.wrap(decoded);

final byte[] algorithm = readBlock(decodedBuffer);
if (Arrays.equals(keyAlgorithmBinary, algorithm)) {
publicKey = readPublicKey(decodedBuffer);
} else {
throw new InvalidKeyException(String.format("Encoded key algorithm [%s] not found", keyAlgorithm));
}

return publicKey;
}

private byte[] readEncodedBytes(final ByteBuffer inputBuffer) {
final int startPosition = inputBuffer.position();

int endPosition = startPosition;
while (inputBuffer.hasRemaining()) {
final byte character = inputBuffer.get();
if (SPACE_SEPARATOR == character) {
break;
}
endPosition = inputBuffer.position();
}

final int length = endPosition - startPosition;
final byte[] encoded = new byte[length];
inputBuffer.position(startPosition);
inputBuffer.get(encoded);
return encoded;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 Jagged 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.exceptionfactory.jagged.ssh;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;

/**
* SSH RSA Public Key Reader implementation based on ssh-rsa format described in RFC 4253 Section 6.6
*/
class SshRsaPublicKeyReader extends SshPublicKeyReader<RSAPublicKey> {
private static final String KEY_ALGORITHM = "RSA";

/**
* SSH RSA Public Key Reader constructor configures the expected ssh-rsa algorithm
*/
SshRsaPublicKeyReader() {
super(SshRsaRecipientIndicator.STANZA_TYPE.getIndicator());
}

/**
* Read RSA Public Key from encoded public exponent and modulus
*
* @param decodedBuffer Buffer of bytes decoded from Base64 public key
* @return RSA Public Key
* @throws GeneralSecurityException Thrown on failures to generate RSA public key from specification
*/
@Override
protected RSAPublicKey readPublicKey(final ByteBuffer decodedBuffer) throws GeneralSecurityException {
final byte[] publicExponentBlock = readBlock(decodedBuffer);
final BigInteger publicExponent = new BigInteger(publicExponentBlock);

final byte[] modulusBlock = readBlock(decodedBuffer);
final BigInteger modulus = new BigInteger(modulusBlock);

final RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
final KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
final PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
return (RSAPublicKey) publicKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import com.exceptionfactory.jagged.RecipientStanzaWriter;

import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.interfaces.RSAPublicKey;

/**
Expand All @@ -36,4 +38,18 @@ private SshRsaRecipientStanzaWriterFactory() {
public static RecipientStanzaWriter newRecipientStanzaWriter(final RSAPublicKey rsaPublicKey) {
return new SshRsaRecipientStanzaWriter(rsaPublicKey);
}

/**
* Create new ssh-rsa Recipient Stanza Writer using an RSA Public Key
*
* @param encoded Byte array containing an SSH RSA public key
* @return ssh-rsa Recipient Stanza Writer
* @throws GeneralSecurityException Thrown in failure to read public key
*/
public static RecipientStanzaWriter newRecipientStanzaWriter(final byte[] encoded) throws GeneralSecurityException {
final SshRsaPublicKeyReader publicKeyReader = new SshRsaPublicKeyReader();
final ByteBuffer inputBuffer = ByteBuffer.wrap(encoded);
final RSAPublicKey rsaPublicKey = publicKeyReader.read(inputBuffer);
return newRecipientStanzaWriter(rsaPublicKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ void testReadLengthLessThanRequired() {
inputBuffer.put(SPACE_SEPARATOR);
inputBuffer.flip();

final InvalidKeyException exception = assertThrows(InvalidKeyException.class, () -> reader.read(inputBuffer));
assertTrue(exception.getMessage().contains(Integer.toString(REQUIRED_LENGTH)));
assertThrows(InvalidKeyException.class, () -> reader.read(inputBuffer));
}

@Test
Expand Down
Loading

0 comments on commit 301b566

Please sign in to comment.