-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Git ssh private key binding(GSoC-21) #1111
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
base: master
Are you sure you want to change the base?
Changes from 33 commits
e9c550a
4fd5c4c
e212567
78a47fb
818599a
6eeecee
2bc3140
e45ed33
9030a2c
9312a70
a8f38f3
9e17779
b53385b
588c389
d70831c
443c0f7
65624c0
a55e3dd
d38eea0
f57be59
df3d33c
0996235
47588ae
7549c54
d1eeb22
96c0103
d3ee23a
c218b0b
d596240
8922ada
a44b388
a42ac94
598eb80
5c47934
752a94a
035e6c2
dcba7c9
fbba6f9
69fafe5
ee925a7
4a5e7de
63680a0
8c525eb
4ec3a3d
638989b
e830aef
be399fb
73b01ba
7ef7820
b419b4c
9bc6d3a
82022ef
3f82b61
30064ed
319442e
ea86cda
dcc83b3
29cdcd7
e13d669
dc83e1c
583ee18
cbf6215
74500c1
1ff2fcb
fc81ba3
c6d9b3b
607a5ab
1662e96
c0d14aa
99535a8
85dfeed
d7f3b55
2efc015
e9d572d
be8b2e9
a98514f
649cd85
1766ec2
74b4f16
f168ecb
71bd8fb
b6eebfd
b6eeac2
66009a8
06c8073
187b399
e12419b
828cc94
dd86551
cdefb57
1ce0079
0e5b9fb
1354b1f
eefb41d
83171e3
3a238c0
c63559c
4654472
b3b2f69
8396478
b097fb5
0a40c33
1785381
78651ae
825eda8
4a5fbdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
package jenkins.plugins.git; | ||
|
||
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; | ||
import com.cloudbees.plugins.credentials.common.StandardCredentials; | ||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import hudson.EnvVars; | ||
import hudson.FilePath; | ||
import hudson.Launcher; | ||
import hudson.model.Run; | ||
import hudson.model.TaskListener; | ||
import hudson.plugins.git.GitSCM; | ||
import hudson.plugins.git.GitTool; | ||
import hudson.util.ListBoxModel; | ||
import hudson.util.Secret; | ||
import hudson.Extension; | ||
import jenkins.model.Jenkins; | ||
import org.jenkinsci.Symbol; | ||
import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor; | ||
import org.jenkinsci.plugins.credentialsbinding.MultiBinding; | ||
import org.jenkinsci.plugins.credentialsbinding.impl.AbstractOnDiskBinding; | ||
import org.jenkinsci.plugins.credentialsbinding.impl.UnbindableDir; | ||
import org.jenkinsci.plugins.gitclient.CliGitAPIImpl; | ||
import org.jenkinsci.plugins.gitclient.Git; | ||
import org.jenkinsci.plugins.gitclient.GitClient; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
|
||
import javax.annotation.Nullable; | ||
import java.io.IOException; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.LinkedHashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
|
||
|
||
public class GitSSHPrivateKeyBinding extends MultiBinding<SSHUserPrivateKey> implements GitCredentialBindings, SSHKeyUtils { | ||
final static private String PRIVATE_KEY = "PRIVATE_KEY"; | ||
final static private String PASSPHRASE = "PASSPHRASE"; | ||
final private String gitToolName; | ||
private transient boolean unixNodeType; | ||
|
||
@DataBoundConstructor | ||
public GitSSHPrivateKeyBinding(String gitToolName, String credentialsId) { | ||
super(credentialsId); | ||
this.gitToolName = gitToolName; | ||
//Variables could be added if needed | ||
} | ||
|
||
public String getGitToolName() { | ||
return this.gitToolName; | ||
} | ||
|
||
private void setUnixNodeType(boolean value) { | ||
this.unixNodeType = value; | ||
} | ||
|
||
@Override | ||
public MultiEnvironment bind(@NonNull Run<?, ?> run, @Nullable FilePath filePath, | ||
@Nullable Launcher launcher, @NonNull TaskListener taskListener) throws IOException, InterruptedException { | ||
final Map<String, String> secretValues = new LinkedHashMap<>(); | ||
final Map<String, String> publicValues = new LinkedHashMap<>(); | ||
SSHUserPrivateKey credentials = getCredentials(run); | ||
setCredentialPairBindings(credentials, secretValues, publicValues); | ||
GitTool cliGitTool = getCliGitTool(run, this.gitToolName, taskListener); | ||
if (cliGitTool != null && filePath != null && launcher != null) { | ||
final UnbindableDir unbindTempDir = UnbindableDir.create(filePath); | ||
final GitClient git = getGitClientInstance(cliGitTool.getGitExe(), unbindTempDir.getDirPath(), new EnvVars(), taskListener); | ||
final String sshExePath = getSSHExePathInWin(git); | ||
setUnixNodeType(isCurrentNodeOSUnix(launcher)); | ||
setGitEnvironmentVariables(git, publicValues); | ||
if (isGitVersionAtLeast(git, 2, 3, 0, 0)) { | ||
secretValues.put("GIT_SSH_COMMAND", getSSHCmd(credentials, unbindTempDir.getDirPath(), sshExePath)); | ||
} else { | ||
SSHScriptFile sshScript = new SSHScriptFile(credentials, sshExePath, unixNodeType); | ||
secretValues.put("GIT_SSH", sshScript.write(credentials, unbindTempDir.getDirPath()).getRemote()); | ||
} | ||
return new MultiEnvironment(secretValues, publicValues, unbindTempDir.getUnbinder()); | ||
} else { | ||
taskListener.getLogger().println("JGit and JGitApache type Git tools are not supported by this binding"); | ||
return new MultiEnvironment(secretValues, publicValues); | ||
} | ||
} | ||
|
||
@Override | ||
public Set<String> variables(@NonNull Run<?, ?> run) { | ||
Set<String> keys = new LinkedHashSet<>(); | ||
keys.add(PRIVATE_KEY); | ||
keys.add(PASSPHRASE); | ||
return keys; | ||
} | ||
|
||
@Override | ||
public void setCredentialPairBindings(@NonNull StandardCredentials credentials, Map<String, String> secretValues, Map<String, String> publicValues) { | ||
SSHUserPrivateKey sshUserCredentials = (SSHUserPrivateKey) credentials; | ||
secretValues.put(PRIVATE_KEY, SSHKeyUtils.getPrivateKey(sshUserCredentials)); | ||
secretValues.put(PASSPHRASE, SSHKeyUtils.getPassphrase(sshUserCredentials)); | ||
} | ||
|
||
/*package*/void setGitEnvironmentVariables(@NonNull GitClient git, Map<String, String> publicValues) throws IOException, InterruptedException { | ||
setGitEnvironmentVariables(git, null, publicValues); | ||
} | ||
|
||
@Override | ||
public void setGitEnvironmentVariables(@NonNull GitClient git, Map<String, String> secretValues, Map<String, String> publicValues) throws IOException, InterruptedException { | ||
if (unixNodeType && isGitVersionAtLeast(git, 2, 3, 0, 0)) { | ||
publicValues.put("GIT_TERMINAL_PROMPT", "false"); | ||
} else { | ||
publicValues.put("GCM_INTERACTIVE", "false"); | ||
} | ||
} | ||
|
||
@Override | ||
public GitClient getGitClientInstance(String gitToolExe, FilePath repository, EnvVars env, TaskListener listener) throws IOException, InterruptedException { | ||
Git gitInstance = Git.with(listener, env).using(gitToolExe); | ||
return gitInstance.getClient(); | ||
} | ||
|
||
@Override | ||
protected Class<SSHUserPrivateKey> type() { | ||
return SSHUserPrivateKey.class; | ||
} | ||
|
||
private boolean isGitVersionAtLeast(GitClient git, int major, int minor, int rev, int bugfix) { | ||
return ((CliGitAPIImpl) git).isCliGitVerAtLeast(major, minor, rev, bugfix); | ||
} | ||
|
||
private String getSSHCmd(SSHUserPrivateKey credentials, FilePath tempDir,String sshExePath) throws IOException, InterruptedException { | ||
if (unixNodeType) { | ||
return "ssh -i " | ||
+ "\"" | ||
+ getPrivateKeyFile(credentials, tempDir).getRemote() | ||
+ "\" " | ||
+ "-o StrictHostKeyChecking=no $@"; | ||
} else { | ||
return "\"" + sshExePath + "\"" | ||
+ " -i " | ||
+ "\"" | ||
+ getPrivateKeyFile(credentials, tempDir).getRemote() | ||
+ "\"" | ||
+ " -o StrictHostKeyChecking=no"; | ||
} | ||
} | ||
|
||
protected final class SSHScriptFile extends AbstractOnDiskBinding<SSHUserPrivateKey> { | ||
|
||
private final String sshExePath; | ||
private final boolean unixNodeType; | ||
|
||
protected SSHScriptFile(SSHUserPrivateKey credentials, String sshExePath, boolean unixNodeType) { | ||
super(SSHKeyUtils.getPrivateKey(credentials) + ":" + SSHKeyUtils.getPassphrase(credentials), credentials.getId()); | ||
this.sshExePath = sshExePath; | ||
this.unixNodeType = unixNodeType; | ||
} | ||
|
||
@Override | ||
protected FilePath write(SSHUserPrivateKey credentials, FilePath workspace) throws IOException, InterruptedException { | ||
FilePath tempFile; | ||
if (unixNodeType) { | ||
tempFile = workspace.createTempFile("gitSSHScript", ".sh"); | ||
tempFile.write( | ||
"ssh -i " | ||
+ getPrivateKeyFile(credentials, workspace).getRemote() | ||
+ " -o StrictHostKeyChecking=no $@", null); | ||
tempFile.chmod(0500); | ||
} else { | ||
tempFile = workspace.createTempFile("gitSSHScript", ".bat"); | ||
tempFile.write("@echo off\r\n" | ||
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. For the same reason, I think whenever a string is getting complex, it should be formatted using a String.format(). |
||
+ "\"" | ||
+ this.sshExePath | ||
+ "\"" | ||
+ " -i " | ||
+ "\"" | ||
+ getPrivateKeyFile(credentials, workspace).getRemote() | ||
+ "\"" | ||
+ " -o StrictHostKeyChecking=no", null); | ||
} | ||
return tempFile; | ||
} | ||
|
||
@Override | ||
protected Class<SSHUserPrivateKey> type() { | ||
return SSHUserPrivateKey.class; | ||
} | ||
} | ||
|
||
@Symbol("GitSSHPrivateKey") | ||
arpoch marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
@Extension | ||
public static final class DescriptorImpl extends BindingDescriptor<SSHUserPrivateKey> { | ||
|
||
@NonNull | ||
@Override | ||
public String getDisplayName() { | ||
return Messages.GitSSHPrivateKeyBinding_DisplayName(); | ||
} | ||
|
||
public ListBoxModel doFillGitToolNameItems() { | ||
Check warningCode scanning / Jenkins Security Scan Stapler: Missing permission check
Potential missing permission check in DescriptorImpl#doFillGitToolNameItems
|
||
ListBoxModel items = new ListBoxModel(); | ||
List<GitTool> toolList = Jenkins.get().getDescriptorByType(GitSCM.DescriptorImpl.class).getGitTools(); | ||
for (GitTool t : toolList){ | ||
if(t.getClass().equals(GitTool.class)){ | ||
items.add(t.getName()); | ||
} | ||
} | ||
return items; | ||
} | ||
|
||
@Override | ||
protected Class<SSHUserPrivateKey> type() { | ||
return SSHUserPrivateKey.class; | ||
} | ||
|
||
@Override | ||
public boolean requiresWorkspace() { | ||
return true; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package jenkins.plugins.git; | ||
|
||
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile; | ||
import net.schmizz.sshj.userauth.password.PasswordFinder; | ||
import net.schmizz.sshj.userauth.password.Resource; | ||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter; | ||
import org.bouncycastle.util.io.pem.PemObject; | ||
|
||
import java.io.IOException; | ||
import java.io.StringWriter; | ||
|
||
public class OpenSSHKeyFormatImpl { | ||
|
||
private final String privateKey; | ||
Check warningCode scanning / Jenkins Security Scan Jenkins: Plaintext password storage
Variable should be reviewed whether it stored a password and is serialized to disk: privateKey
|
||
private final String passphrase; | ||
|
||
public OpenSSHKeyFormatImpl(final String privateKey, final String passphrase) { | ||
this.privateKey = privateKey; | ||
this.passphrase = passphrase; | ||
} | ||
|
||
public static boolean isOpenSSHFormat(String privateKey) { | ||
final String HEADER = "-----BEGIN OPENSSH PRIVATE KEY-----"; | ||
return privateKey.regionMatches(false, 0, HEADER, 0, HEADER.length()); | ||
} | ||
|
||
public String getDecodedPrivateKey() throws IOException { | ||
OpenSSHKeyV1KeyFile openSSHProtectedKey = new OpenSSHKeyV1KeyFile(); | ||
openSSHProtectedKey.init(privateKey, "", new AcquirePassphrase(passphrase.toCharArray())); | ||
byte[] content = openSSHProtectedKey.getPrivate().getEncoded(); | ||
StringWriter sw = new StringWriter(); | ||
JcaPEMWriter pemWriter = new JcaPEMWriter(sw); | ||
PemObject pemObject = new PemObject("PRIVATE KEY", content); | ||
pemWriter.writeObject(pemObject); | ||
pemWriter.flush(); | ||
pemWriter.close(); | ||
return sw.toString(); | ||
} | ||
|
||
private final static class AcquirePassphrase implements PasswordFinder { | ||
|
||
char[] p; | ||
|
||
AcquirePassphrase(char[] passphrase) { | ||
this.p = passphrase; | ||
} | ||
|
||
@Override | ||
public char[] reqPassword(Resource<?> resource) { | ||
return p; | ||
} | ||
|
||
@Override | ||
public boolean shouldRetry(Resource<?> resource) { | ||
return false; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package jenkins.plugins.git; | ||
|
||
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; | ||
import hudson.FilePath; | ||
import hudson.util.Secret; | ||
import jenkins.bouncycastle.api.PEMEncodable; | ||
import org.jenkinsci.plugins.gitclient.CliGitAPIImpl; | ||
import org.jenkinsci.plugins.gitclient.GitClient; | ||
|
||
import java.io.IOException; | ||
import java.security.UnrecoverableKeyException; | ||
|
||
public interface SSHKeyUtils { | ||
|
||
static String getPrivateKey(SSHUserPrivateKey credentials) { | ||
return credentials.getPrivateKeys().get(0); | ||
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. Possible NPE, should we not first check the size of getPrivateKeys()? |
||
} | ||
|
||
static String getPassphrase(SSHUserPrivateKey credentials) { | ||
return Secret.toString(credentials.getPassphrase()); | ||
} | ||
|
||
static boolean isPrivateKeyEncrypted(String passphrase) { | ||
return passphrase.isEmpty() ? false : true; | ||
} | ||
|
||
default String getSSHExePathInWin(GitClient git) throws IOException, InterruptedException { | ||
return ((CliGitAPIImpl) git).getSSHExecutable().getAbsolutePath(); | ||
} | ||
|
||
default FilePath getPrivateKeyFile(SSHUserPrivateKey credentials, FilePath workspace) throws InterruptedException, IOException { | ||
FilePath tempKeyFile = workspace.createTempFile("private", ".key"); | ||
final String privateKeyValue = getPrivateKey(credentials); | ||
final String passphraseValue = getPassphrase(credentials); | ||
try { | ||
if (isPrivateKeyEncrypted(passphraseValue)) { | ||
if (OpenSSHKeyFormatImpl.isOpenSSHFormat(privateKeyValue)) { | ||
OpenSSHKeyFormatImpl openSSHKeyFormat = new OpenSSHKeyFormatImpl(privateKeyValue, passphraseValue); | ||
tempKeyFile.write(openSSHKeyFormat.getDecodedPrivateKey(), null); | ||
} else { | ||
tempKeyFile.write(PEMEncodable.decode(privateKeyValue, passphraseValue.toCharArray()).encode(), null); | ||
} | ||
} else { | ||
tempKeyFile.write(privateKeyValue, null); | ||
} | ||
tempKeyFile.chmod(0500); | ||
return tempKeyFile; | ||
} catch (UnrecoverableKeyException e) { | ||
e.printStackTrace(); | ||
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. again why are we dumping the whole stack trace? We should have a consistent logging statement strategy. 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. All the exceptions related to private key operations(encryption/decryption/write) are being caught here, are you suggesting to use the logger of the current build to output specific reasons why the exception/error was generated or are you suggesting to not handle the exceptions with try/catch here and use the throws keyword to let the current build handle that for us. |
||
return null; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
|
||
<?jelly escape-by-default='true'?> | ||
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler" xmlns:c="/lib/credentials"> | ||
<f:entry title="${%Credentials}" field="credentialsId"> | ||
<c:select expressionAllowed="${expressionAllowed}"/> | ||
</f:entry> | ||
<f:entry title="${%Git Tool Name}" field="gitToolName"> | ||
<f:select /> | ||
</f:entry> | ||
</j:jelly> |
Uh oh!
There was an error while loading. Please reload this page.