diff --git a/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK17).launch b/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK17).launch new file mode 100644 index 0000000..411ddc9 --- /dev/null +++ b/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK17).launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK8).launch b/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK8).launch new file mode 100644 index 0000000..12ad597 --- /dev/null +++ b/sshsig-bcprov/SshPublicKeyEncoderTest (bcprov)(JDK8).launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshPublicKeyEncoderTest.java b/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshPublicKeyEncoderTest.java new file mode 100644 index 0000000..37b4f1f --- /dev/null +++ b/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshPublicKeyEncoderTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Jan Henrik Wiesner + * + * 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 de.profhenry.sshsig.bcprov; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.PublicKey; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import de.profhenry.sshsig.core.SshPublicKeyEncoder; + +/** + * Unit tests for {@link SshPublicKeyEncoder}. + *

+ * + * @author profhenry + */ +public class SshPublicKeyEncoderTest { + + @BeforeAll + static void setup() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + @Disabled + void testEncodeDsaPublicKey() throws Exception { + PublicKey tPublicKey = SshKeyUtil.readDsaKeyPair().getPublic(); + + SshPublicKeyEncoder tPublicKeyEncoder = new SshPublicKeyEncoder(); + byte[] tSshEncodedPublicKey = tPublicKeyEncoder.encodePublicKey(tPublicKey); + + assertThat(tSshEncodedPublicKey).asBase64Encoded() + .isEqualTo( + "AAAAB3NzaC1kc3MAAACBAP8BC5pi3uxvOtlb1ikWNeLUubiaT0l7aMOhAbOFmtivswcg0r+rZiX/UOjhxCATRiaTXV42byLnf0cKzQrZ5QEnm8uTf1aK0gdT/bdQaZwWmkMB2LdHQ4xEt3slsDLYoXwwzV+stpRcFQ1VyUlJQV754HqHZT8RbkUBJHIsIqslAAAAFQCKvLgJmuxvJy6DvdRB5Fm/VZR5PwAAAIEA1y5UQItEth5WSS6166Aujc+7x9jgaztoC4Uo0iskMM5D0oVrJSyVwCAOcNPEeya4zNnJgkD16Wco3XOryBcgjWScEoTgict2J8rnaUWDkyOMptfbiF+oU39INh3m+2tvIfsgX81bAQJD0STYiF9J3G/PQYjvRIxhybtHJHGcuaUAAACBANTDVZW7LC9PbzQ1e2SxMOznf76/WV8RfwfKALh0DpJvyaEuOQkgwO/AzSNoN1mJgkn7mjXKgsJLYB/49d1arv8n/nG+7oLGdKYlKlXOkJ2bW+yxnFwLvBTBniCbdyylPP1iNbw2SArns44xxBszjTedAZcpJQiv+ThZI3Bzg5dM"); + } + + @Test + void testEncodeRsaPublicKey() throws Exception { + PublicKey tPublicKey = SshKeyUtil.readRsaKeyPair().getPublic(); + + SshPublicKeyEncoder tPublicKeyEncoder = new SshPublicKeyEncoder(); + byte[] tSshEncodedPublicKey = tPublicKeyEncoder.encodePublicKey(tPublicKey); + + assertThat(tSshEncodedPublicKey).asBase64Encoded() + .isEqualTo( + "AAAAB3NzaC1yc2EAAAADAQABAAABgQDg9Lf347GyGfq+SZjWnLEFY61tz3czkkpeU71piNtCD9M18vsonIKmLRwUC4dKBE+UJQf7F79Mx/Z6XqgNCTP9tAVj1YMKtIIXbl6F4hkMWZr1XTjq78jCk3yWx7BA9CYaTxK185l3WbcZovrx9iTrVafi6+cXhxAC4HYHQYjB/1YubPhWIJ4mtqo7e22xP84Kdr/aYmSJbx0vaUjRJQaFJkYVi2Sb8GYAagd5YQ5aODU6CuY/ycp18UMQ56G/uSR19O+OGXrHbF2GEZTko6ESOAbu7EjquU1fOL3xeh/3GYNtYjeztQFCXGz2iXrKNk+wMjHGvrg8w11NdgpT983UMwA8bV8kctz4qaH/89HhR49pkLSxOc9AMzjSL8N4bueId598KnfutEzT0N+Ghwsi+1fDrohCTRx2x+PyLYe5syehjn/IxhnQlKEvtRitjUqCn32mAufx2BbCl8rykPTUxBE/QAUYPlA/Surv8j9yE8tEpDIdEBc78kpwBxH60p8="); + } + + @Test + void testEncodeEd25519PublicKey() throws Exception { + PublicKey tPublicKey = SshKeyUtil.readEd25519KeyPair().getPublic(); + + SshPublicKeyEncoder tPublicKeyEncoder = new SshPublicKeyEncoder(); + byte[] tSshEncodedPublicKey = tPublicKeyEncoder.encodePublicKey(tPublicKey); + + assertThat(tSshEncodedPublicKey).asBase64Encoded() + .isEqualTo("AAAAC3NzaC1lZDI1NTE5AAAAIDuDUZReuNqkLI5pqXRzx6+LtMtLMji2HPoDccHOE4XF"); + } +} diff --git a/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshSignatureGeneratorTest.java b/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshSignatureGeneratorTest.java index c3104c3..44d3230 100644 --- a/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshSignatureGeneratorTest.java +++ b/sshsig-bcprov/src/test/java/de/profhenry/sshsig/bcprov/SshSignatureGeneratorTest.java @@ -118,7 +118,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } @@ -179,7 +179,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } @@ -259,7 +259,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } @@ -327,7 +327,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } @@ -394,7 +394,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } @@ -449,7 +449,7 @@ void testVerifyWithSshKeyGen() throws SshSignatureException, IOException, Interr + "." + sshSignatureGenerator.getHashAlgorithm() + ".sig"; - tSignature.write(Paths.get(tSignatureFileName)); + tSignature.writeAsPem(Paths.get(tSignatureFileName)); verifyUsingSshKeygen(MESSAGE, NAMESPACE, tSignatureFileName); } diff --git a/sshsig-core/SshPublicKeyEncoderTest (core)(JDK17).launch b/sshsig-core/SshPublicKeyEncoderTest (core)(JDK17).launch new file mode 100644 index 0000000..ee63525 --- /dev/null +++ b/sshsig-core/SshPublicKeyEncoderTest (core)(JDK17).launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sshsig-core/SshPublicKeyEncoderTest (core)(JDK8).launch b/sshsig-core/SshPublicKeyEncoderTest (core)(JDK8).launch new file mode 100644 index 0000000..ad3f0b0 --- /dev/null +++ b/sshsig-core/SshPublicKeyEncoderTest (core)(JDK8).launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sshsig-core/src/main/java/de/profhenry/sshsig/core/JcaSingingBackend.java b/sshsig-core/src/main/java/de/profhenry/sshsig/core/JcaSingingBackend.java index e777af0..65549e6 100644 --- a/sshsig-core/src/main/java/de/profhenry/sshsig/core/JcaSingingBackend.java +++ b/sshsig-core/src/main/java/de/profhenry/sshsig/core/JcaSingingBackend.java @@ -35,12 +35,14 @@ * For signing you need to specify a {@link KeyPair}, the {@link PrivateKey} is required for the actual signing process, * the {@link PublicKey} is required because it gets embedded into the SSH signature. *

- * The SSH * * @author profhenry */ public class JcaSingingBackend implements SigningBackend { + /** + * The logger. + */ private static final Logger LOGGER = LoggerFactory.getLogger(JcaSingingBackend.class); @Override @@ -70,14 +72,18 @@ public SigningResult signData(KeyPair aKeyPair, byte[] someDataToSign) throws Ss if ("RSA".equals(tPrivateKey.getAlgorithm())) { return signRsa(tPrivateKey, tPublicKey, someDataToSign); } - if ("EdDSA".equals(tPrivateKey.getAlgorithm())) { - // used by JDK17 and net.i2p.crypto - // JDK17 uses EdDSA for Ed25519 and Ed448 - // TODO i would love to prevent Ed448 but i think this is not possible when compiling againt JDK8 :-/ + if ("Ed25519".equals(tPrivateKey.getAlgorithm())) { + // used by org.bouncycastle:bcprov in default configuration return signEd25519(tPrivateKey, tPublicKey, someDataToSign); } - if ("Ed25519".equals(tPrivateKey.getAlgorithm())) { - // used by org.bouncycastle + if ("EdDSA".equals(tPrivateKey.getAlgorithm())) { + // used by + // - JDK17 + // - net.i2p.crypto:eddsa + // - org.bouncycastle:bcprov (with activated org.bouncycastle.emulate.oracle property) + + // EdDSA is also used for Ed448 (at least when using JDK17) + // TODO i would love to prevent using Ed448 but i think this is not possible when compiling againt JDK8 :-/ return signEd25519(tPrivateKey, tPublicKey, someDataToSign); } diff --git a/sshsig-core/src/main/java/de/profhenry/sshsig/core/PemWriter.java b/sshsig-core/src/main/java/de/profhenry/sshsig/core/PemWriter.java new file mode 100644 index 0000000..a989c07 --- /dev/null +++ b/sshsig-core/src/main/java/de/profhenry/sshsig/core/PemWriter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Jan Henrik Wiesner + * + * 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 de.profhenry.sshsig.core; + +import java.io.PrintWriter; +import java.io.Writer; +import java.util.Base64; + +/** + * @author profhenry + */ +public class PemWriter { + + /** + * The default line length of the base 64 encoded content. + *

+ * RFC7468 states that the base 64 content MUST wrap after 64 chars. + */ + private static final int DEFAULT_LINE_LENGTH = 64; + + private final PrintWriter printWriter; + + private final int lineLength; + + public PemWriter(Writer aWriter, int aLineLength) { + printWriter = new PrintWriter(aWriter); + lineLength = aLineLength; + } + + public PemWriter(Writer aWriter) { + this(aWriter, DEFAULT_LINE_LENGTH); + } + + public void writeData(String aLabel, byte[] someBytes) { + writeHeader(aLabel); + writeData(someBytes); + writeFooter(aLabel); + printWriter.flush(); + } + + private void writeHeader(String aLabel) { + printWriter.println("-----BEGIN " + aLabel + "-----"); + } + + private void writeData(byte[] someBytes) { + String tEncoded = Base64.getEncoder().encodeToString(someBytes); + + for (int i = 0; i < tEncoded.length(); i += lineLength) { + printWriter.println(tEncoded.substring(i, Math.min(tEncoded.length(), i + lineLength))); + } + } + + private void writeFooter(String aLabel) { + printWriter.println("-----END " + aLabel + "-----"); + } +} diff --git a/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshBuffer.java b/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshBuffer.java index 6031113..44e99d0 100644 --- a/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshBuffer.java +++ b/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshBuffer.java @@ -129,7 +129,7 @@ public void appendBigInteger(BigInteger aBigInteger) { /** * Appends a sequence of a string and a byte array. *

- * First writes the overall bytes required by this sequence followed by the string and the byte array (eqch with + * First writes the overall bytes required by this sequence followed by the string and the byte array (each with * their length fields as well). *

* Added bytes: 4 + 4 + number of chars + 4 + length of the byte array diff --git a/sshsig-core/src/main/java/de/profhenry/sshsig/core/PublicKeyEncoder.java b/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshPublicKeyEncoder.java similarity index 70% rename from sshsig-core/src/main/java/de/profhenry/sshsig/core/PublicKeyEncoder.java rename to sshsig-core/src/main/java/de/profhenry/sshsig/core/SshPublicKeyEncoder.java index 70f535d..5c7f6f5 100644 --- a/sshsig-core/src/main/java/de/profhenry/sshsig/core/PublicKeyEncoder.java +++ b/sshsig-core/src/main/java/de/profhenry/sshsig/core/SshPublicKeyEncoder.java @@ -20,29 +20,57 @@ import java.security.interfaces.RSAPublicKey; /** + * Encoder for public keys in SSH format. + *

+ * This encoder returns a blob containing the SSH public key as specified in the SSH protocol.
+ * The second column of a SSH public key file contains this blob (base64 encoded). + *

+ * * @author profhenry */ -public class PublicKeyEncoder { +public class SshPublicKeyEncoder { + /** key format identifier for DSA keys **/ private static final String KEY_FORMAT_IDENTIFIER_DSS = "ssh-dss"; + /** key format identifier for RSA keys **/ private static final String KEY_FORMAT_IDENTIFIER_RSA = "ssh-rsa"; + /** key format identifier for ED25519 keys **/ private static final String KEY_FORMAT_IDENTIFIER_ED25519 = "ssh-ed25519"; + /** + * Encodes a public key in SSH format. + *

+ * + * @param aPublicKey a public key + * @return the encoded public key + * @throws SshSignatureException in case the public key could no be encoded + */ public byte[] encodePublicKey(PublicKey aPublicKey) throws SshSignatureException { + // In case of DSA or RSA public keys the detection is easy because JCA provides interfaces for those. + // This approach should also work in case other JCA Provider (like bouncy castler) are used. if (aPublicKey instanceof DSAPublicKey) { return encodeDsaPublicKey((DSAPublicKey) aPublicKey); } if (aPublicKey instanceof RSAPublicKey) { return encodeRsaPublicKey((RSAPublicKey) aPublicKey); } - if ("EdDSA".equals(aPublicKey.getAlgorithm())) { - // used by JDK17 and net.i2p.crypto + // The handling of ED25519 public keys is a bit more complicated. + // Since JDK8 comes with no support for ED25519 there is no interface to compile against. + // However we decided to add some support for ED25519. For using ED25519 you need a JDK17 as runtime JVM and/or + // a JCA security provider which adds ED25519 support. + + // We decided to use the algorithm provided by the public key as incidator if we have an ED25519 key. + if ("Ed25519".equals(aPublicKey.getAlgorithm())) { + // used by org.bouncycastle:bcprov in default configuration return encodeEd25519PublicKey(aPublicKey); } - if ("Ed25519".equals(aPublicKey.getAlgorithm())) { - // used by org.bouncycastle + if ("EdDSA".equals(aPublicKey.getAlgorithm())) { + // used by + // - JDK17 + // - net.i2p.crypto:eddsa + // - org.bouncycastle:bcprov (with activated org.bouncycastle.emulate.oracle property) return encodeEd25519PublicKey(aPublicKey); } throw new SshSignatureException("Could not encode public key (" + aPublicKey.getClass().getName() + ")!"); @@ -51,7 +79,7 @@ public byte[] encodePublicKey(PublicKey aPublicKey) throws SshSignatureException /** * Encodes a DSA public key. *

- * According to RTC 4253 DSA public key encoding is + * According to RTC 4253 the DSA public key encoding is *