-
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 70 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,224 @@ | ||||||
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.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) { | ||||||
setUnixNodeType(isCurrentNodeOSUnix(launcher)); | ||||||
final UnbindableDir unbindTempDir = UnbindableDir.create(filePath); | ||||||
final GitClient git = getGitClientInstance(cliGitTool.getGitExe(), unbindTempDir.getDirPath(), new EnvVars(), taskListener); | ||||||
final String sshExePath = getSSHPath(git); | ||||||
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.getSinglePrivateKey(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 getSSHPath(GitClient git) throws IOException, InterruptedException { | ||||||
if (unixNodeType) { | ||||||
return "ssh"; | ||||||
} else { | ||||||
return getSSHExePathInWin(git); | ||||||
} | ||||||
} | ||||||
|
||||||
private String getSSHCmd(SSHUserPrivateKey credentials, FilePath tempDir, String sshExePath) throws IOException, InterruptedException { | ||||||
if (unixNodeType) { | ||||||
return | ||||||
sshExePath | ||||||
+ " -i " | ||||||
+ getPrivateKeyFile(credentials, tempDir).getRemote() | ||||||
+ " -o StrictHostKeyChecking=no"; | ||||||
} else { | ||||||
return "\"" + sshExePath + "\"" | ||||||
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. Optional: For better maintainability, personally I would recommend using
Suggested change
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. Nice suggestion, will check this out. 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. ... This seems to go against the https://github.com/jenkinsci/git-client-plugin#ssh-host-key-verification feature. @MarkEWaite is that correct? 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. Agreed @sboardwell . This needs to be extended to support the host key checking strategy before it is merged. |
||||||
+ " -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.getSinglePrivateKey(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") | ||||||
@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,106 @@ | ||
package jenkins.plugins.git; | ||
|
||
import hudson.FilePath; | ||
import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser; | ||
import org.apache.sshd.common.NamedResource; | ||
import org.apache.sshd.common.config.keys.FilePasswordProvider; | ||
import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter; | ||
import org.apache.sshd.common.session.SessionContext; | ||
import org.apache.sshd.common.util.io.SecureByteArrayOutputStream; | ||
|
||
import javax.naming.SizeLimitExceededException; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.FileOutputStream; | ||
import java.security.GeneralSecurityException; | ||
import java.security.KeyPair; | ||
import java.util.Base64; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.Map; | ||
|
||
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; | ||
private static final String BEGIN_MARKER = OpenSSHKeyPairResourceParser.BEGIN_MARKER; | ||
private static final String END_MARKER = OpenSSHKeyPairResourceParser.END_MARKER; | ||
private static final String DASH_MARKER = "-----"; | ||
|
||
public OpenSSHKeyFormatImpl(final String privateKey, final String passphrase) { | ||
this.privateKey = privateKey; | ||
this.passphrase = passphrase; | ||
} | ||
|
||
private byte[] getEncData(String privateKey){ | ||
String data = privateKey | ||
.replace(DASH_MARKER+BEGIN_MARKER+DASH_MARKER,"") | ||
.replace(DASH_MARKER+END_MARKER+DASH_MARKER,"") | ||
.replaceAll("\\s",""); | ||
Base64.Decoder decoder = Base64.getDecoder(); | ||
return decoder.decode(data); | ||
} | ||
|
||
private KeyPair getOpenSSHKeyPair(SessionContext session, NamedResource resourceKey, | ||
String beginMarker, String endMarker, | ||
FilePasswordProvider passwordProvider, | ||
InputStream stream, Map<String, String> headers ) | ||
throws IOException, GeneralSecurityException, SizeLimitExceededException { | ||
OpenSSHKeyPairResourceParser openSSHParser = new OpenSSHKeyPairResourceParser(); | ||
Collection<KeyPair> keyPairs = openSSHParser.extractKeyPairs(session,resourceKey,beginMarker, | ||
endMarker, passwordProvider, | ||
stream, headers); | ||
if(keyPairs.size() > 1){ | ||
throw new SizeLimitExceededException("Expected KeyPair size to be 1"); | ||
}else { | ||
return Collections.unmodifiableCollection(keyPairs).iterator().next(); | ||
} | ||
} | ||
|
||
private File writePrivateKeyOpenSSHFormatted(File tempFile) { | ||
OpenSSHKeyPairResourceWriter privateKeyWriter = new OpenSSHKeyPairResourceWriter(); | ||
SecureByteArrayOutputStream privateKeyBuffer = new SecureByteArrayOutputStream(); | ||
ByteArrayInputStream stream = new ByteArrayInputStream(getEncData(this.privateKey)); | ||
KeyPair sshKeyPair = null; | ||
try { | ||
sshKeyPair = getOpenSSHKeyPair(null,null,"","", | ||
|
||
new AcquirePassphrase(this.passphrase), | ||
stream,null); | ||
privateKeyWriter.writePrivateKey(sshKeyPair, "", null, privateKeyBuffer); | ||
FileOutputStream privateKeyFileStream = new FileOutputStream(tempFile); | ||
privateKeyBuffer.writeTo(privateKeyFileStream); | ||
privateKeyFileStream.close(); | ||
} catch (IOException | SizeLimitExceededException | GeneralSecurityException e) { | ||
e.printStackTrace(); | ||
|
||
} | ||
return tempFile; | ||
} | ||
|
||
public static boolean isOpenSSHFormatted(String privateKey) { | ||
final String HEADER = DASH_MARKER+BEGIN_MARKER+DASH_MARKER; | ||
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. You may use .concat() function instead of the + operator. 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 all constants that mean compile-time this will be replaced with its calculate valued, with concat it should remain a function called every time. When concat constants I think better + operator. 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. instead you could move HEADER definition as constant and use here and in getEncData function |
||
return privateKey.regionMatches(false, 0, HEADER, 0, HEADER.length()); | ||
} | ||
|
||
public FilePath writeDecryptedOpenSSHKey(FilePath tempKeyFile) throws IOException, InterruptedException, GeneralSecurityException, SizeLimitExceededException { | ||
File tempFile = new File(tempKeyFile.toURI()); | ||
writePrivateKeyOpenSSHFormatted(tempFile); | ||
return new FilePath(tempFile); | ||
} | ||
|
||
private final static class AcquirePassphrase implements FilePasswordProvider { | ||
|
||
String passphrase; | ||
|
||
AcquirePassphrase(String passphrase) { | ||
this.passphrase = passphrase; | ||
} | ||
|
||
@Override | ||
public String getPassword(SessionContext session, NamedResource resourceKey, int retryIndex) throws IOException { | ||
return this.passphrase; | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.