-
Notifications
You must be signed in to change notification settings - Fork 697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JENKINS-75011] Use Apache Mina as ssh transport layer, remove trilead #1022
Changes from all commits
92c7809
f7deaff
4549402
3b52047
bcfbd7c
8225f35
bda7f3c
471b5d0
9f45558
480c37f
243088d
0726489
7680d4e
fe9a901
9abf818
9a4fc3b
7ceaeea
4e9d26d
7b68bae
abea87f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package hudson.plugins.ec2.ssh.proxy; | ||
|
||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.time.Duration; | ||
import java.util.Base64; | ||
import org.apache.sshd.client.session.ClientProxyConnector; | ||
import org.apache.sshd.client.session.ClientSession; | ||
import org.apache.sshd.common.io.IoSession; | ||
import org.apache.sshd.common.util.buffer.ByteArrayBuffer; | ||
|
||
/** | ||
* {@link ClientProxyConnector} that issue an HTTP CONNECT to connect through an HTTP proxy. | ||
*/ | ||
public class ProxyCONNECTListener implements ClientProxyConnector { | ||
|
||
private static final long timeout = Duration.ofSeconds(10).toMillis(); | ||
|
||
public final String targetHost; | ||
public final int targetPort; | ||
public final String proxyUser; | ||
public final String proxyPass; | ||
Check warning Code scanning / Jenkins Security Scan Jenkins: Plaintext password storage Warning
Field should be reviewed whether it stores a password and is serialized to disk: proxyPass
|
||
|
||
public ProxyCONNECTListener(String targetHost, int targetPort, String proxyUser, String proxyPass) { | ||
this.targetHost = targetHost; | ||
this.targetPort = targetPort; | ||
this.proxyUser = proxyUser; | ||
this.proxyPass = proxyPass; | ||
} | ||
|
||
@Override | ||
public void sendClientProxyMetadata(ClientSession session) throws Exception { | ||
proxyCONNECT(session.getIoSession()); | ||
} | ||
|
||
public void proxyCONNECT(IoSession ioSession) { | ||
StringBuilder connectRequest = new StringBuilder(); | ||
|
||
// Based on https://www.rfc-editor.org/rfc/rfc7231#section-4.3.6 | ||
connectRequest | ||
.append("CONNECT ") | ||
.append(targetHost) | ||
.append(':') | ||
.append(targetPort) | ||
.append(" HTTP/1.0\r\n"); | ||
// Host should be included https://datatracker.ietf.org/doc/html/rfc2616#section-14.23 | ||
connectRequest | ||
.append("Host: ") | ||
.append(targetHost) | ||
.append(':') | ||
.append(targetPort) | ||
.append("\r\n"); | ||
|
||
if ((proxyUser != null) && (proxyPass != null)) { | ||
String credentials = proxyUser + ":" + proxyPass; | ||
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.ISO_8859_1)); | ||
connectRequest.append("Proxy-Authorization: Basic "); | ||
connectRequest.append(encoded); | ||
connectRequest.append("\r\n"); | ||
} | ||
|
||
// End of the header | ||
connectRequest.append("\r\n"); | ||
|
||
try { | ||
ioSession | ||
.writeBuffer(new ByteArrayBuffer(connectRequest.toString().getBytes(StandardCharsets.US_ASCII))) | ||
.await(timeout); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package hudson.plugins.ec2.util; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import java.io.IOException; | ||
import java.io.StringReader; | ||
import java.security.KeyFactory; | ||
import java.security.KeyPair; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.PrivateKey; | ||
import java.security.PublicKey; | ||
import java.security.interfaces.DSAParams; | ||
import java.security.interfaces.DSAPrivateKey; | ||
import java.security.interfaces.ECPrivateKey; | ||
import java.security.interfaces.ECPublicKey; | ||
import java.security.interfaces.RSAPrivateCrtKey; | ||
import java.security.spec.DSAPublicKeySpec; | ||
import java.security.spec.ECField; | ||
import java.security.spec.ECParameterSpec; | ||
import java.security.spec.EllipticCurve; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.RSAPublicKeySpec; | ||
import java.security.spec.X509EncodedKeySpec; | ||
import org.bouncycastle.asn1.ASN1BitString; | ||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; | ||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; | ||
import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; | ||
import org.bouncycastle.openssl.PEMEncryptedKeyPair; | ||
import org.bouncycastle.openssl.PEMKeyPair; | ||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; | ||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; | ||
|
||
/** | ||
* Utility class to parse PEM. | ||
*/ | ||
public abstract class KeyHelper { | ||
private KeyHelper() {} | ||
|
||
/** | ||
* Decodes a PEM-encoded key pair into a {@link KeyPair} object. This method supports | ||
* various types of PEM input such as encrypted private keys, public keys, and key pairs. | ||
* | ||
* @param pem The PEM-formatted string containing the key data. | ||
* @param password The password used to decrypt encrypted key pairs, if applicable. Can be null if no password is required. | ||
* @return A {@link KeyPair} containing the public and private keys. If a public key is provided without a matching private key, | ||
* the private key in the returned {@link KeyPair} will be null. | ||
* @throws IOException If an error occurs during parsing or decryption of the PEM input. | ||
* @throws IllegalArgumentException If the provided PEM input cannot be parsed or is of an unsupported type. | ||
*/ | ||
public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String password) throws IOException { | ||
try (org.bouncycastle.openssl.PEMParser pemParser = | ||
new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { | ||
Object object = pemParser.readObject(); | ||
if (object == null) { | ||
throw new IllegalArgumentException("Failed to parse PEM input"); | ||
} | ||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); | ||
|
||
if (object instanceof PEMEncryptedKeyPair) { | ||
PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) object) | ||
.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might throw a NPE when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added: 9abf818 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since the password can be null, it would be better to remove the |
||
PrivateKey privateKey = converter.getPrivateKey(decryptedKeyPair.getPrivateKeyInfo()); | ||
PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo()); | ||
return new KeyPair(publicKey, privateKey); | ||
} else if (object instanceof PrivateKeyInfo) { | ||
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) object; | ||
PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); | ||
PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKeyInfo, privateKey); | ||
return new KeyPair(publicKey, privateKey); | ||
} else if (object instanceof SubjectPublicKeyInfo) { | ||
PublicKey publicKey = converter.getPublicKey((SubjectPublicKeyInfo) object); | ||
return new KeyPair(publicKey, null); | ||
} else if (object instanceof PEMKeyPair) { | ||
SubjectPublicKeyInfo publicKeyInfo = ((PEMKeyPair) object).getPublicKeyInfo(); | ||
PrivateKeyInfo privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo(); | ||
return new KeyPair(converter.getPublicKey(publicKeyInfo), converter.getPrivateKey(privateKeyInfo)); | ||
} else { | ||
throw new IllegalArgumentException( | ||
"Unsupported PEM object type: " + object.getClass().getName()); | ||
} | ||
} catch (Exception e) { | ||
throw new IOException("Failed to parse PEM input", e); | ||
} | ||
} | ||
|
||
/* visible for testing */ | ||
/** | ||
* Extract a {@link PublicKey} from the given {@link PrivateKey} | ||
* @param privateKey the private key to extract from | ||
* @return the corresponding public key or null if the extraction is not possible | ||
*/ | ||
static PublicKey generatePublicKeyFromPrivateKey(PrivateKeyInfo privateKeyInfo, @NonNull PrivateKey privateKey) { | ||
try { | ||
if (privateKey instanceof RSAPrivateCrtKey) | ||
return KeyFactory.getInstance("RSA") | ||
.generatePublic(new RSAPublicKeySpec( | ||
((RSAPrivateCrtKey) privateKey).getModulus(), | ||
((RSAPrivateCrtKey) privateKey).getPublicExponent())); | ||
else if (privateKey instanceof DSAPrivateKey) { | ||
DSAParams dsaParams = ((DSAPrivateKey) privateKey).getParams(); | ||
return KeyFactory.getInstance("DSA") | ||
.generatePublic(new DSAPublicKeySpec( | ||
dsaParams.getG().modPow(((DSAPrivateKey) privateKey).getX(), dsaParams.getP()), | ||
dsaParams.getP(), | ||
dsaParams.getQ(), | ||
dsaParams.getG())); | ||
} else if (privateKey instanceof ECPrivateKey) { | ||
ASN1BitString asn1BitString = org.bouncycastle.asn1.sec.ECPrivateKey.getInstance( | ||
privateKeyInfo.getPrivateKey().getOctets()) | ||
.getPublicKey(); | ||
return KeyFactory.getInstance("EC") | ||
.generatePublic(new X509EncodedKeySpec( | ||
new SubjectPublicKeyInfo(privateKeyInfo.getPrivateKeyAlgorithm(), asn1BitString) | ||
.getEncoded())); | ||
} else if (privateKey instanceof EdDSAPrivateKey) { | ||
return ((EdDSAPrivateKey) privateKey).getPublicKey(); | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we only supporting two algorithms? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I rewrote it in a more robust way. Anyway, I think that this is not used because we get a |
||
return null; | ||
} | ||
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Determines the SSH algorithm identifier corresponding to the given server public key. | ||
* This method matches the key type to the appropriate SSH algorithm string. | ||
* When an {@link ECPublicKey} is given, an NIST curse will be assumed. | ||
* | ||
* @param serverKey The server's {@link PublicKey} object for which the SSH algorithm identifier | ||
* needs to be determined. | ||
* @return A {@code String} representing the SSH algorithm identifier for the given server key, | ||
* or {@code null} if the key type is unsupported or cannot be determined. | ||
*/ | ||
public static String getSshAlgorithm(@NonNull PublicKey serverKey) { | ||
switch (serverKey.getAlgorithm()) { | ||
case "RSA": | ||
return "ssh-rsa"; | ||
case "EC": | ||
if (serverKey instanceof ECPublicKey) { | ||
ECPublicKey ecPublicKey = (ECPublicKey) serverKey; | ||
ECParameterSpec params = ecPublicKey.getParams(); | ||
if (params != null) { | ||
|
||
EllipticCurve curve = params.getCurve(); | ||
ECField field = (curve != null) ? curve.getField() : null; | ||
if (field != null) { | ||
int fieldSize = field.getFieldSize(); | ||
// Assume NIST curve | ||
return "ecdsa-sha2-nistp" + fieldSize; | ||
} | ||
} | ||
} | ||
return null; | ||
case "EdDSA": | ||
case "Ed25519": | ||
return "ssh-ed25519"; | ||
default: | ||
return null; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we refactor the class name as it is no longer implementing the Interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept the name as this is a public
class
.