<attribute name="test" value="true"/>
</attributes>
</classpathentry>
+ <classpathentry kind="src" path="tst-rsrc">
+ <attributes>
+ <attribute name="test" value="true"/>
+ </attributes>
+ </classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>
--- /dev/null
+/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle binary
\ No newline at end of file
+load(
+ "@com_googlesource_gerrit_bazlets//tools:genrule2.bzl",
+ "genrule2",
+)
load(
"@com_googlesource_gerrit_bazlets//tools:junit.bzl",
"junit_tests",
)
+DEPS = [
+ "//lib:eddsa",
+ "//lib:junit",
+ "//lib:sshd-osgi",
+ "//lib:sshd-sftp",
+ "//org.eclipse.jgit:jgit",
+ "//org.eclipse.jgit.junit:junit",
+ "//org.eclipse.jgit.junit.ssh:junit-ssh",
+ "//org.eclipse.jgit.ssh.apache:ssh-apache",
+]
+
+HELPERS = ["tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java"]
+
junit_tests(
name = "sshd_apache",
- srcs = glob(["tst/**/*.java"]),
+ srcs = glob(
+ ["tst/**/*.java"],
+ exclude = HELPERS,
+ ),
tags = ["sshd"],
- deps = [
- "//lib:eddsa",
- "//lib:junit",
- "//lib:sshd-osgi",
- "//lib:sshd-sftp",
- "//org.eclipse.jgit:jgit",
- "//org.eclipse.jgit.junit.ssh:junit-ssh",
- "//org.eclipse.jgit.ssh.apache:ssh-apache",
+ runtime_deps = [":tst_rsrc"],
+ deps = DEPS + [
+ ":helpers",
],
)
+
+java_library(
+ name = "helpers",
+ testonly = 1,
+ srcs = HELPERS,
+ deps = DEPS,
+)
+
+genrule2(
+ name = "tst_rsrc",
+ srcs = glob(["tst-rsrc/**"]),
+ outs = ["tst_rsrc.jar"],
+ cmd = "tar cf - $(SRCS) | tar -C $$TMP --strip-components=2 -xf - && cd $$TMP && zip -qr $$ROOT/$@ .",
+)
org.apache.sshd.common.keyprovider;version="[2.14.0,2.15.0)",
org.apache.sshd.common.session;version="[2.14.0,2.15.0)",
org.apache.sshd.common.signature;version="[2.14.0,2.15.0)",
+ org.apache.sshd.common.util.buffer;version="[2.14.0,2.15.0)",
org.apache.sshd.common.util.net;version="[2.14.0,2.15.0)",
org.apache.sshd.common.util.security;version="[2.14.0,2.15.0)",
org.apache.sshd.core;version="[2.14.0,2.15.0)",
org.apache.sshd.server.forward;version="[2.14.0,2.15.0)",
org.eclipse.jgit.api;version="[7.1.0,7.2.0)",
org.eclipse.jgit.api.errors;version="[7.1.0,7.2.0)",
+ org.eclipse.jgit.internal.signing.ssh;version="[7.1.0,7.2.0)",
org.eclipse.jgit.internal.storage.file;version="[7.1.0,7.2.0)",
org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.1.0,7.2.0)",
org.eclipse.jgit.junit;version="[7.1.0,7.2.0)",
org.eclipse.jgit.junit.ssh;version="[7.1.0,7.2.0)",
org.eclipse.jgit.lib;version="[7.1.0,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.0,7.2.0)",
org.eclipse.jgit.transport;version="[7.1.0,7.2.0)",
org.eclipse.jgit.transport.sshd;version="[7.1.0,7.2.0)",
org.eclipse.jgit.transport.sshd.agent;version="[7.1.0,7.2.0)",
org.eclipse.jgit.util;version="[7.1.0,7.2.0)",
org.junit;version="[4.13,5.0.0)",
org.junit.experimental.theories;version="[4.13,5.0.0)",
- org.junit.runner;version="[4.13,5.0.0)"
+ org.junit.rules;version="[4.13.0,5.0.0)",
+ org.junit.runner;version="[4.13,5.0.0)",
+ org.junit.runners;version="[4.13.0,5.0.0)",
+ org.slf4j;version="[1.7.0,2.0.0)"
<sourceDirectory>src/</sourceDirectory>
<testSourceDirectory>tst/</testSourceDirectory>
+ <testResources>
+ <testResource>
+ <directory>tst-rsrc/</directory>
+ </testResource>
+ </testResources>
+
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
--- /dev/null
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAJAhCMgzIQjI
+MwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQ
+AAAEBmcXpast20+B4IzA0Xex2CKYiiWJj3NFJ5F0kil113vcdEl+iOTEbf1RC3uicECtid
++SaIMsAw7wrlWhOQTyBVAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
--- /dev/null
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV
--- /dev/null
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAJAa2jfBGto3
+wQAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgw
+AAAEBothGMqFaA5aTO8MLx9wm1oDRfzQCSsu7uJwrOiUFTTTtLjJwgHqhPHhQ3yX0367TX
+pSAzOSttPsT7ZdsbfmODAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
--- /dev/null
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDtLjJwgHqhPHhQ3yX0367TXpSAzOSttPsT7ZdsbfmOD
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINFZ5NKywAWh1G1P6BiBKArmYKs1BDhJBOawJKlS29VXAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAEAAAABAAAADGV4cGlyZWRfY2VydAAAAAAAAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgx0SX6I5MRt/VELe6JwQK2J35JogywDDvCuVaE5BPIFUAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNf8i5dhRqWRe06epIRrZ5V+QZHq3ZrlJtlx98UJya9GAeCrJ5oHwBjr5O5TL5wNJS5Hz+T1qsJNFU9d1wdcuwI=
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILzuED1RSloB/enTghTEKSACVOuEARP0f8UVXSRwEXN6AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAIAAAABAAAADW5vX3ByaW5jaXBhbHMAAAAAAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBwEQ2D0OHn4QDHnsINlgWUWpmhukseQCJu3Adulz28fFtewp1LLqkBy50wR6vJe1ifYbY4hzReXOSyoTmHSXEN
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIHNW2bzSS61lvgHippv3Ymx4cVEAXBVCb8lFXHnVpsSyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAYAAAABAAAAB3Rlc3RlcjIAAAAWAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAFMAAAALc3NoLWVkMjU1MTkAAABAuJ8zBazcaYTbUEr9QtoYox0MkVBg+8LANxJxc885M2vmg9yPHpTfV/emupqhBwuYcPJSskTxl7WX4TUNvhMsAA==
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGKXzyrvDzj9ObQ4SuzqytK6nomOV8DhgdzODfWuup1sAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAQAAAABAAAABW90aGVyAAAAFQAAABFvdGhlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABA1ycFqWehyC6pIISEkXSTtHbatLWl9HHAoUFouQiDdubAnMDRSkyHipXR62rq+8yEAvtqm1mXBzO8nLalkF9xAA==
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICSl1xsyTWb23YlKo21musxOzj4L4eD2coTkHbBw2uOyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAMAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDyjzq/0Egm1OxwrvqPZKUihE3w357Ji9Nd3j7VnUuvSYTXAdB9P0E+a2hyCcemmsil1MsvWTiCSSOsrHVB6FEO
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFmWKr9gNSQT0vna7k3uOyUF9CTcMGw2zxTFBf2Ev8TzAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAgAAAABAAAABnRlc3RlcgAAACkAAAAPZm9vQGV4YW1wbGUuY29tAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABAqlSX2GzLz5U+hN/gF9UUyAkE6h5BgVFYhsyf1MR/B7Hoxa29wGLbJpUplrqEHMxoud2zfH2Nhj00unc3lr5bBA== ./signing_key.pub
--- /dev/null
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiwAAAJDdMhQO3TIU
+DgAAAAtzc2gtZWQyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiw
+AAAEA4NlTFs3h2zqt5pSZ5S3dJb42GE7EjG16coKj70eELNDeK9TvB/fuMTUhMLkpCNV5W
+XIVOSBYnG5vy3fzSi+OLAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGXo4+L/NyBl1VQDP39PxJP3LSzaqopqZGVP3cG0WoFAAAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OLAAAAAAAAAAUAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA/HwKB8J/kvkEsdxDou+UebnR9u30xPH6FEnbHLlfKbKMIXwLFIHnf9F6bTL36WhFDEDcSBGS19VBWBDRosM8L
--- /dev/null
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OL
--- /dev/null
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czgAAAJDhSMqA4UjK
+gAAAAAtzc2gtZWQyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czg
+AAAEB1yC00NMYEAVzhDj9odGVL0EonaIkf5jdUZ/czJ0+SPWATOZ8PcOKdY978fzIstnZ0
++FuefIWKp7wRZynQLdzOAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
--- /dev/null
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFEmoWkYraMju0JI0b/0RQtR6RYo/OVp53EVf48L/Pu/AAAAIBmHlkHFlA7HkoTZcau80PH5zduQu41m8BqnH/1v2BwVAAAAAAAAAAEAAAABAAAACGFfa2V5X2lkAAAAFgAAABJ0ZXN0ZXJAZXhhbXBsZS5jb20AAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg2ifM9NMuXwQf7H/H5LCMhMjVqugyyN+jmcMoJUL2YLAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQG1kXUido46YOnmwvkJuIAKyp6Q9Gr+lbdOQvU0St/Hc9HTTIxgDGyLpv0alIJpHOuSYUUUxDufvGKtLJK1duwg= ./signing_key.pub
--- /dev/null
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneOffset;
+
+import org.eclipse.jgit.api.CommitCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Common setup for SSH signature tests.
+ */
+public abstract class AbstractSshSignatureTest extends RepositoryTestCase {
+
+ @Rule
+ public TemporaryFolder keys = new TemporaryFolder();
+
+ protected File certs;
+
+ protected Instant commitTime;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ copyResource("other_key", keys.getRoot());
+ copyResource("other_key.pub", keys.getRoot());
+ copyResource("other_key-cert.pub", keys.getRoot());
+ copyResource("signing_key", keys.getRoot());
+ copyResource("signing_key.pub", keys.getRoot());
+ certs = keys.newFolder("certs");
+ copyResource("certs/expired.cert", certs);
+ copyResource("certs/no_principals.cert", certs);
+ copyResource("certs/other.cert", certs);
+ copyResource("certs/other-ca.cert", certs);
+ copyResource("certs/tester.cert", certs);
+ copyResource("certs/two_principals.cert", certs);
+ Repository repo = db;
+ StoredConfig config = repo.getConfig();
+ config.setString("gpg", null, "format", "ssh");
+ config.save();
+ // Run all tests with commit times on 2024-10-02T12:00:00Z. The test
+ // certificates are valid from 2024-09-01 to 2024-10-31, except the
+ // "expired" certificate which is valid only on 2024-09-01.
+ commitTime = Instant.parse("2024-10-02T12:00:00.00Z");
+ }
+
+ private void copyResource(String name, File directory) throws IOException {
+ try (InputStream in = this.getClass().getResourceAsStream(name)) {
+ int i = name.lastIndexOf('/');
+ String fileName = i < 0 ? name : name.substring(i + 1);
+ Files.copy(in, directory.toPath().resolve(fileName));
+ }
+ }
+
+ protected RevCommit createSignedCommit(String certificate,
+ String signingKey) throws Exception {
+ Repository repo = db;
+ Path key = keys.getRoot().toPath().resolve(signingKey);
+ if (certificate != null) {
+ Files.copy(certs.toPath().resolve(certificate),
+ keys.getRoot().toPath().resolve(signingKey),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ PersonIdent commitAuthor = new PersonIdent("tester",
+ "tester@example.com", commitTime, ZoneOffset.UTC);
+ try (Git git = Git.wrap(repo)) {
+ writeTrashFile("foo.txt", "foo");
+ git.add().addFilepattern("foo.txt").call();
+ CommitCommand commit = git.commit();
+ commit.setAuthor(commitAuthor);
+ commit.setCommitter(commitAuthor);
+ commit.setMessage("Message");
+ commit.setSign(Boolean.TRUE);
+ commit.setSigningKey(key.toAbsolutePath().toString());
+ return commit.call();
+ }
+ }
+
+ protected RevCommit checkSshSignature(RevCommit c) {
+ byte[] sig = c.getRawGpgSignature();
+ assertNotNull(sig);
+ String signature = new String(sig, StandardCharsets.US_ASCII);
+ assertTrue("Not an SSH signature:\n" + signature,
+ signature.startsWith(Constants.SSH_SIGNATURE_PREFIX));
+ return c;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link SshCertificateUtils}. They use a certificate valid from
+ * 2024-09-01 00:00:00 to 2024-09-02 00:00:00 UTC.
+ */
+public class SshCertificateUtilsTest {
+
+ private OpenSshCertificate certificate;
+
+ @Before
+ public void loadCertificate() throws Exception {
+ try (InputStream in = this.getClass().getResourceAsStream(
+ "certs/expired.cert")) {
+ List<AuthorizedKeyEntry> keys = AuthorizedKeyEntry
+ .readAuthorizedKeys(in, true);
+ if (keys.isEmpty()) {
+ certificate = null;
+ }
+ PublicKey key = keys.get(0).resolvePublicKey(null,
+ PublicKeyEntryResolver.FAILING);
+ assertTrue(
+ "Expected an OpenSshKeyCertificate but got a "
+ + key.getClass().getName(),
+ key instanceof OpenSshCertificate);
+ certificate = (OpenSshCertificate) key;
+ }
+ }
+
+ @Test
+ public void testValidUserCertificate() {
+ assertNull(SshCertificateUtils.verify(certificate, null));
+ Instant validTime = Instant.parse("2024-09-01T00:00:00.00Z");
+ assertNull(SshCertificateUtils.verify(certificate, validTime));
+ assertNull(SshCertificateUtils.checkExpiration(certificate, validTime));
+ }
+
+ @Test
+ public void testCheckTooEarly() {
+ Instant invalidTime = Instant.parse("2024-08-31T23:59:59.00Z");
+ assertNotNull(
+ SshCertificateUtils.checkExpiration(certificate, invalidTime));
+ assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+ }
+
+ @Test
+ public void testCheckExpired() {
+ Instant invalidTime = Instant.parse("2024-09-02T00:00:01.00Z");
+ assertNotNull(
+ SshCertificateUtils.checkExpiration(certificate, invalidTime));
+ assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+ }
+
+ @Test
+ public void testInvalidSignature() throws Exception {
+ // Modify the serialized certificate, then re-load it again. To check that
+ // serialization per se works fine, also check an unmodified version.
+ Buffer buffer = new ByteArrayBuffer();
+ buffer.putPublicKey(certificate);
+ int pos = buffer.rpos();
+ PublicKey unchanged = buffer.getPublicKey();
+ assertTrue(
+ "Expected an OpenSshCertificate but got a "
+ + unchanged.getClass().getName(),
+ unchanged instanceof OpenSshCertificate);
+ assertNull(SshCertificateUtils.verify((OpenSshCertificate) unchanged,
+ null));
+ buffer.rpos(pos);
+ // Change a byte. The test certificate has the key ID at offset 128.
+ // Changing a byte in the key ID should still result in a successful
+ // deserialization, but then fail the signature check.
+ buffer.array()[pos + 128]++;
+ PublicKey changed = buffer.getPublicKey();
+ assertTrue(
+ "Expected an OpenSshCertificate but got a "
+ + changed.getClass().getName(),
+ changed instanceof OpenSshCertificate);
+ assertNotNull(
+ SshCertificateUtils.verify((OpenSshCertificate) changed, null));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link SshSigner}.
+ */
+public class SshSignerTest extends AbstractSshSignatureTest {
+
+ @Test
+ public void testPlainSignature() throws Exception {
+ checkSshSignature(createSignedCommit(null, "signing_key.pub"));
+ }
+
+ @Test
+ public void testExpiredSignature() throws Exception {
+ Throwable t = assertThrows(Throwable.class,
+ () -> createSignedCommit("expired.cert",
+ "signing_key-cert.pub"));
+ // The exception or one of its causes should mention "[Ee]xpired" and
+ // "[Cc]ertificate" in the message
+ while (t != null) {
+ String message = t.getMessage();
+ if (message.contains("xpired") && message.contains("ertificate")) {
+ return;
+ }
+ t = t.getCause();
+ }
+ fail("Expected exception message not found");
+ }
+
+ @Test
+ public void testCertificateSignature() throws Exception {
+ checkSshSignature(createSignedCommit("tester.cert", "signing_key.pub"));
+ }
+
+ @Test
+ public void testNoPrincipalsSignature() throws Exception {
+ // Certificate has no principals; should still work
+ checkSshSignature(
+ createSignedCommit("no_principals.cert", "signing_key.pub"));
+ }
+
+ @Test
+ public void testOtherSignature() throws Exception {
+ // Certificate has a principal different that tester@example.com; should
+ // still work
+ checkSshSignature(createSignedCommit("other.cert", "signing_key.pub"));
+ }
+}
Bundle-ActivationPolicy: lazy
Bundle-Version: 7.1.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.internal.transport.sshd;version="7.1.0";x-internal:=true;
+Export-Package: org.eclipse.jgit.internal.signing.ssh;version="7.1.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.internal.transport.sshd;version="7.1.0";x-internal:=true;
uses:="org.apache.sshd.client,
org.apache.sshd.client.auth,
org.apache.sshd.client.auth.keyboard,
org.apache.sshd.sftp.client;version="[2.14.0,2.15.0)",
org.apache.sshd.sftp.common;version="[2.14.0,2.15.0)",
org.eclipse.jgit.annotations;version="[7.1.0,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.0,7.2.0)",
org.eclipse.jgit.errors;version="[7.1.0,7.2.0)",
org.eclipse.jgit.fnmatch;version="[7.1.0,7.2.0)",
org.eclipse.jgit.internal.storage.file;version="[7.1.0,7.2.0)",
org.eclipse.jgit.internal.transport.ssh;version="[7.1.0,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.0,7.2.0)",
org.eclipse.jgit.nls;version="[7.1.0,7.2.0)",
org.eclipse.jgit.transport;version="[7.1.0,7.2.0)",
org.eclipse.jgit.util;version="[7.1.0,7.2.0)",
--- /dev/null
+org.eclipse.jgit.signing.ssh.SshSignerFactory
\ No newline at end of file
sshCommandTimeout={0} timed out after {1} seconds while opening the channel
sshProcessStillRunning={0} is not yet completed, cannot get exit code
sshProxySessionCloseFailed=Error while closing proxy session {0}
+signCertAlgorithmMismatch=Certificate of type {0} with CA key {1} uses an incompatible signature algorithm {2}
+signCertAlgorithmUnknown=Certificate with CA key {0} is signed with an unknown algorithm {1}
+signCertificateExpired=Expired certificate with CA key {0}
+signCertificateInvalid=Certificate signature does not match on certificate with CA key {0}
+signCertificateTooEarly=Certificate with CA key {0} was not valid yet
+signDefaultKeyEmpty=git.ssh.defaultKeyCommand {0} returned no key
+signDefaultKeyFailed=git.ssh.defaultKeyCommand {0} failed with exit code {1}\n{2}
+signDefaultKeyInterrupted=git.ssh.defaultKeyCommand {0} was interrupted
+signInvalidKeyDSA=SSH signatures with DSA keys or certificates are not supported; use a different signing key.
+signLogFailure=SSH signature verification failed
+signNoAgent=No connector for ssh-agent found; maybe include org.eclipse.jgit.ssh.apache.agent in the application.
+signNoPublicKey=No public key found with signing key {0}
+signNoSigningKey=Git config user.signingKey or gpg.ssh.defaultKeyCommand must be set for SSH signing.
+signNotUserCertificate=Certificate with CA key {0} used for the SSH signature is not a user certificate.
+signPublicKeyError=Cannot read public key {0}
+signSeeLog=SSH signature verification failed; see the log for details
+signSignatureError=Could not create the signature
+signStderr=Cannot read stderr
+signTooManyPrivateKeys=Private key file {0} must contain exactly one private key
+signTooManyPublicKeys=Public key file {0} must contain exactly one public key
+signUnknownHashAlgorithm=SSH Signature has an unknown hash algorithm {0}
+signUnknownSignatureAlgorithm=SSH Signature has an unknown signature algorithm {0}
unknownProxyProtocol=Ignoring unknown proxy protocol {0}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for working with OpenSSH certificates.
+ */
+final class SshCertificateUtils {
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(SshCertificateUtils.class);
+
+ /**
+ * Verifies a certificate: checks that it is a user certificate and has a
+ * valid signature, and if a time is given, that the certificate is valid at
+ * that time.
+ *
+ * @param certificate
+ * {@link OpenSshCertificate} to verify
+ * @param signatureTime
+ * {@link Instant} to check whether the certificate is valid at
+ * that time; maybe {@code null}, in which case the valid-time
+ * check is skipped.
+ * @return {@code null} if the certificate is valid; otherwise a descriptive
+ * message
+ */
+ static String verify(OpenSshCertificate certificate,
+ Instant signatureTime) {
+ if (!OpenSshCertificate.Type.USER.equals(certificate.getType())) {
+ return MessageFormat.format(SshdText.get().signNotUserCertificate,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+ }
+ String message = verifySignature(certificate);
+ if (message == null && signatureTime != null) {
+ message = checkExpiration(certificate, signatureTime);
+ }
+ return message;
+ }
+
+ /**
+ * Verifies the signature on a certificate.
+ *
+ * @param certificate
+ * {@link OpenSshCertificate} to verify
+ * @return {@code null} if the signature is valid; otherwise a descriptive
+ * message
+ */
+ static String verifySignature(OpenSshCertificate certificate) {
+ // Verify the signature on the certificate.
+ //
+ // Note that OpenSSH certificates do not support chaining.
+ //
+ // ssh-keygen refuses to create a certificate for a certificate, so the
+ // certified key cannot be another OpenSshCertificate. Additionally,
+ // when creating a certificate ssh-keygen loads the CA private key to
+ // make the signature and reconstructs the public key that it stores in
+ // the certificate from that, so the CA public key also cannot be an
+ // OpenSshCertificate.
+ PublicKey caKey = certificate.getCaPubKey();
+ PublicKey certifiedKey = certificate.getCertPubKey();
+ if (caKey == null
+ || caKey instanceof OpenSshCertificate
+ || certifiedKey == null
+ || certifiedKey instanceof OpenSshCertificate) {
+ return SshdText.get().signCertificateInvalid;
+ }
+ // Verify that key type and algorithm match
+ String keyType = KeyUtils.getKeyType(caKey);
+ String certAlgorithm = certificate.getSignatureAlgorithm();
+ if (!KeyUtils.getCanonicalKeyType(keyType)
+ .equals(KeyUtils.getCanonicalKeyType(certAlgorithm))) {
+ return MessageFormat.format(
+ SshdText.get().signCertAlgorithmMismatch, keyType,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+ certAlgorithm);
+ }
+ BuiltinSignatures factory = BuiltinSignatures
+ .fromFactoryName(certAlgorithm);
+ if (factory == null || !factory.isSupported()) {
+ return MessageFormat.format(SshdText.get().signCertAlgorithmUnknown,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+ certAlgorithm);
+ }
+ Signature signer = factory.create();
+ try {
+ signer.initVerifier(null, caKey);
+ signer.update(null, getBlob(certificate));
+ if (signer.verify(null, certificate.getRawSignature())) {
+ return null;
+ }
+ } catch (Exception e) {
+ LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+ return SshdText.get().signSeeLog;
+ }
+ return MessageFormat.format(SshdText.get().signCertificateInvalid,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+ }
+
+ private static byte[] getBlob(OpenSshCertificate certificate) {
+ // Theoretically, this should be just certificate.getMessage(). But
+ // Apache MINA sshd has a bug and may return additional bytes if the
+ // certificate is not the first thing in the buffer it was read from.
+ // As a work-around, re-create the signed blob from scratch.
+ //
+ // This may be replaced by return certificate.getMessage() once the
+ // upstream bug is fixed.
+ //
+ // See https://github.com/apache/mina-sshd/issues/618
+ Buffer tmp = new ByteArrayBuffer();
+ tmp.putString(certificate.getKeyType());
+ tmp.putBytes(certificate.getNonce());
+ tmp.putRawPublicKeyBytes(certificate.getCertPubKey());
+ tmp.putLong(certificate.getSerial());
+ tmp.putInt(certificate.getType().getCode());
+ tmp.putString(certificate.getId());
+ Buffer list = new ByteArrayBuffer();
+ list.putStringList(certificate.getPrincipals(), false);
+ tmp.putBytes(list.getCompactData());
+ tmp.putLong(certificate.getValidAfter());
+ tmp.putLong(certificate.getValidBefore());
+ tmp.putCertificateOptions(certificate.getCriticalOptions());
+ tmp.putCertificateOptions(certificate.getExtensions());
+ tmp.putString(certificate.getReserved());
+ Buffer inner = new ByteArrayBuffer();
+ inner.putRawPublicKey(certificate.getCaPubKey());
+ tmp.putBytes(inner.getCompactData());
+ return tmp.getCompactData();
+ }
+
+ /**
+ * Checks whether a certificate is valid at a given time.
+ *
+ * @param certificate
+ * {@link OpenSshCertificate} to check
+ * @param signatureTime
+ * {@link Instant} to check
+ * @return {@code null} if the certificate is valid at the given instant;
+ * otherwise a descriptive message
+ */
+ static String checkExpiration(OpenSshCertificate certificate,
+ @NonNull Instant signatureTime) {
+ long instant = signatureTime.getEpochSecond();
+ if (Long.compareUnsigned(instant, certificate.getValidAfter()) < 0) {
+ return MessageFormat.format(SshdText.get().signCertificateTooEarly,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+ } else if (Long.compareUnsigned(instant,
+ certificate.getValidBefore()) > 0) {
+ return MessageFormat.format(SshdText.get().signCertificateExpired,
+ KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+ }
+ return null;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jgit.lib.Constants;
+
+/**
+ * Defines common constants for SSH signatures.
+ */
+final class SshSignatureConstants {
+
+ private static final String SIGNATURE_END = "-----END SSH SIGNATURE-----"; //$NON-NLS-1$
+
+ static final byte[] MAGIC = { 'S', 'S', 'H', 'S', 'I', 'G' };
+
+ static final int VERSION = 1;
+
+ static final String NAMESPACE = "git"; //$NON-NLS-1$
+
+ static final byte[] ARMOR_HEAD = Constants.SSH_SIGNATURE_PREFIX
+ .getBytes(StandardCharsets.US_ASCII);
+
+ static final byte[] ARMOR_END = SIGNATURE_END
+ .getBytes(StandardCharsets.US_ASCII);
+
+ private SshSignatureConstants() {
+ // No instantiation
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.signing.ssh;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StreamCorruptedException;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.internal.transport.sshd.agent.SshAgentClient;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProviderFactory;
+import org.eclipse.jgit.transport.sshd.agent.Connector;
+import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link Signer} to create SSH signatures.
+ *
+ * @see <a href=
+ * "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig">PROTOCOL.sshsig</a>
+ */
+public class SshSigner implements Signer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SshSigner.class);
+
+ private static final String GIT_KEY_PREFIX = "key::"; //$NON-NLS-1$
+
+ // Base64 encoded lines should not be longer than 75 characters, plus the
+ // newline.
+ private static final int LINE_LENGTH = 75;
+
+ @Override
+ public GpgSignature sign(Repository repository, GpgConfig config,
+ byte[] data, PersonIdent committer, String signingKey,
+ CredentialsProvider credentialsProvider) throws CanceledException,
+ IOException, UnsupportedSigningFormatException {
+ byte[] hash;
+ try {
+ hash = MessageDigest.getInstance("SHA512").digest(data); //$NON-NLS-1$
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedSigningFormatException(
+ MessageFormat.format(
+ SshdText.get().signUnknownHashAlgorithm, "SHA512"), //$NON-NLS-1$
+ e);
+ }
+ Buffer toSign = new ByteArrayBuffer();
+ toSign.putRawBytes(SshSignatureConstants.MAGIC);
+ toSign.putString(SshSignatureConstants.NAMESPACE);
+ toSign.putUInt(0); // reserved: zero-length string
+ toSign.putString("sha512"); //$NON-NLS-1$
+ toSign.putBytes(hash);
+ String key = signingKey;
+ if (StringUtils.isEmptyOrNull(key)) {
+ key = config.getSigningKey();
+ }
+ if (StringUtils.isEmptyOrNull(key)) {
+ key = defaultKeyCommand(repository, config);
+ // According to documentation, this is supposed to return a
+ // valid SSH public key prefixed with "key::". We don't enforce
+ // this: there might be older command implementations (like just
+ // calling "ssh-add -L") that return keys without prefix.
+ }
+ PublicKeyIdentity identity;
+ try {
+ identity = getIdentity(key, committer, credentialsProvider);
+ } catch (GeneralSecurityException e) {
+ throw new UnsupportedSigningFormatException(MessageFormat
+ .format(SshdText.get().signPublicKeyError, key), e);
+ }
+ String algorithm = KeyUtils
+ .getKeyType(identity.getKeyIdentity().getPublic());
+ switch (algorithm) {
+ case KeyPairProvider.SSH_DSS:
+ case KeyPairProvider.SSH_DSS_CERT:
+ throw new UnsupportedSigningFormatException(
+ SshdText.get().signInvalidKeyDSA);
+ case KeyPairProvider.SSH_RSA:
+ algorithm = KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS;
+ break;
+ case KeyPairProvider.SSH_RSA_CERT:
+ algorithm = KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS;
+ break;
+ default:
+ break;
+ }
+
+ Map.Entry<String, byte[]> rawSignature;
+ try {
+ rawSignature = identity.sign(null, algorithm,
+ toSign.getCompactData());
+ } catch (Exception e) {
+ throw new UnsupportedSigningFormatException(
+ SshdText.get().signSignatureError, e);
+ }
+ algorithm = rawSignature.getKey();
+ Buffer signature = new ByteArrayBuffer();
+ signature.putRawBytes(SshSignatureConstants.MAGIC);
+ signature.putUInt(SshSignatureConstants.VERSION);
+ signature.putPublicKey(identity.getKeyIdentity().getPublic());
+ signature.putString(SshSignatureConstants.NAMESPACE);
+ signature.putUInt(0); // reserved: zero-length string
+ signature.putString("sha512"); //$NON-NLS-1$
+ Buffer sig = new ByteArrayBuffer();
+ sig.putString(KeyUtils.getSignatureAlgorithm(algorithm,
+ identity.getKeyIdentity().getPublic()));
+ sig.putBytes(rawSignature.getValue());
+ signature.putBytes(sig.getCompactData());
+ return armor(signature.getCompactData());
+ }
+
+ private static String defaultKeyCommand(@NonNull Repository repository,
+ @NonNull GpgConfig config) throws IOException {
+ String command = config.getSshDefaultKeyCommand();
+ if (StringUtils.isEmptyOrNull(command)) {
+ return null;
+ }
+ FS fileSystem = repository.getFS();
+ if (fileSystem == null) {
+ fileSystem = FS.DETECTED;
+ }
+ ProcessBuilder builder = fileSystem.runInShell(command,
+ new String[] {});
+ ExecutionResult result = null;
+ try {
+ result = fileSystem.execute(builder, null);
+ int exitCode = result.getRc();
+ if (exitCode == 0) {
+ // The command is supposed to return a public key in its first
+ // line on stdout.
+ try (BufferedReader r = new BufferedReader(
+ new InputStreamReader(
+ result.getStdout().openInputStream(),
+ SystemReader.getInstance()
+ .getDefaultCharset()))) {
+ String line = r.readLine();
+ if (line != null) {
+ line = line.strip();
+ }
+ if (StringUtils.isEmptyOrNull(line)) {
+ throw new IOException(MessageFormat.format(
+ SshdText.get().signDefaultKeyEmpty, command));
+ }
+ return line;
+ }
+ }
+ TemporaryBuffer stderr = result.getStderr();
+ throw new IOException(MessageFormat.format(
+ SshdText.get().signDefaultKeyFailed, command,
+ Integer.toString(exitCode), toString(stderr)));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException(
+ MessageFormat.format(
+ SshdText.get().signDefaultKeyInterrupted, command),
+ e);
+ } finally {
+ if (result != null) {
+ if (result.getStderr() != null) {
+ result.getStderr().destroy();
+ }
+ if (result.getStdout() != null) {
+ result.getStdout().destroy();
+ }
+ }
+ }
+ }
+
+ private static String toString(TemporaryBuffer b) {
+ if (b != null) {
+ try {
+ return new String(b.toByteArray(4000),
+ SystemReader.getInstance().getDefaultCharset());
+ } catch (IOException e) {
+ LOG.warn("{}", SshdText.get().signStderr, e); //$NON-NLS-1$
+ }
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ private static PublicKeyIdentity getIdentity(String signingKey,
+ PersonIdent committer, CredentialsProvider credentials)
+ throws CanceledException, GeneralSecurityException, IOException {
+ if (StringUtils.isEmptyOrNull(signingKey)) {
+ throw new IllegalArgumentException(SshdText.get().signNoSigningKey);
+ }
+ PublicKey publicKey = null;
+ PrivateKey privateKey = null;
+ File keyFile = null;
+ if (signingKey.startsWith(GIT_KEY_PREFIX)) {
+ try (StringReader r = new StringReader(
+ signingKey.substring(GIT_KEY_PREFIX.length()))) {
+ publicKey = fromEntry(
+ AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+ }
+ } else if (signingKey.startsWith("~/") //$NON-NLS-1$
+ || signingKey.startsWith('~' + File.separator)) {
+ keyFile = new File(FS.DETECTED.userHome(), signingKey.substring(2));
+ } else {
+ try (StringReader r = new StringReader(signingKey)) {
+ publicKey = fromEntry(
+ AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+ } catch (IOException e) {
+ // Ignore and try to read as a file
+ keyFile = new File(signingKey);
+ }
+ }
+ if (keyFile != null && keyFile.isFile()) {
+ try {
+ publicKey = fromEntry(AuthorizedKeyEntry
+ .readAuthorizedKeys(keyFile.toPath()));
+ if (publicKey == null) {
+ throw new IOException(MessageFormat.format(
+ SshdText.get().signTooManyPublicKeys, keyFile));
+ }
+ // Try to find the private key so we don't go looking for
+ // the agent (or PKCS#11) in vain.
+ keyFile = getPrivateKeyFile(keyFile.getParentFile(),
+ keyFile.getName());
+ if (keyFile != null) {
+ try {
+ KeyPair pair = loadPrivateKey(keyFile.toPath(),
+ credentials);
+ if (pair != null) {
+ PublicKey pk = pair.getPublic();
+ if (pk == null) {
+ privateKey = pair.getPrivate();
+ } else {
+ PublicKey original = publicKey;
+ if (publicKey instanceof OpenSshCertificate cert) {
+ original = cert.getCertPubKey();
+ }
+ if (KeyUtils.compareKeys(original, pk)) {
+ privateKey = pair.getPrivate();
+ }
+ }
+ }
+ } catch (IOException e) {
+ // Apparently it wasn't a private key file. Ignore.
+ }
+ }
+ } catch (StreamCorruptedException e) {
+ // File is readable, but apparently not a public key. Try to
+ // load it as a private key.
+ KeyPair pair = loadPrivateKey(keyFile.toPath(), credentials);
+ if (pair != null) {
+ publicKey = pair.getPublic();
+ privateKey = pair.getPrivate();
+ }
+ }
+ }
+ if (publicKey == null) {
+ throw new IOException(MessageFormat
+ .format(SshdText.get().signNoPublicKey, signingKey));
+ }
+ if (publicKey instanceof OpenSshCertificate cert) {
+ String message = SshCertificateUtils.verify(cert,
+ committer.getWhenAsInstant());
+ if (message != null) {
+ throw new IOException(message);
+ }
+ }
+ if (privateKey == null) {
+ // Could be in the agent, or a PKCS#11 key. The normal procedure
+ // with PKCS#11 keys is to put them in the agent and let the agent
+ // deal with it.
+ //
+ // This may or may not work well. For instance, the agent might ask
+ // for a passphrase for PKCS#11 keys... also, the OpenSSH ssh-agent
+ // had a bug with signing using PKCS#11 certificates in the agent;
+ // see https://bugzilla.mindrot.org/show_bug.cgi?id=3613 . If there
+ // are troubles, we might do the PKCS#11 dance ourselves, but we'd
+ // need additional configuration for the PKCS#11 library. (Plus
+ // some refactoring in the Pkcs11Provider.)
+ return new AgentIdentity(publicKey);
+
+ }
+ return new KeyPairIdentity(new KeyPair(publicKey, privateKey));
+ }
+
+ private static File getPrivateKeyFile(File directory,
+ String publicKeyName) {
+ if (publicKeyName.endsWith(".pub")) { //$NON-NLS-1$
+ String privateKeyName = publicKeyName.substring(0,
+ publicKeyName.length() - 4);
+ if (!privateKeyName.isEmpty()) {
+ File keyFile = new File(directory, privateKeyName);
+ if (keyFile.isFile()) {
+ return keyFile;
+ }
+ if (privateKeyName.endsWith("-cert")) { //$NON-NLS-1$
+ privateKeyName = privateKeyName.substring(0,
+ privateKeyName.length() - 5);
+ if (!privateKeyName.isEmpty()) {
+ keyFile = new File(directory, privateKeyName);
+ if (keyFile.isFile()) {
+ return keyFile;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static KeyPair loadPrivateKey(Path path,
+ CredentialsProvider credentials)
+ throws CanceledException, GeneralSecurityException, IOException {
+ if (!Files.isRegularFile(path)) {
+ return null;
+ }
+ KeyPairResourceParser parser = SecurityUtils.getKeyPairResourceParser();
+ if (parser != null) {
+ PasswordProviderWrapper provider = null;
+ if (credentials != null) {
+ provider = new PasswordProviderWrapper(
+ () -> KeyPasswordProviderFactory.getInstance()
+ .apply(credentials));
+ }
+ try {
+ Collection<KeyPair> keyPairs = parser.loadKeyPairs(null, path,
+ provider);
+ if (keyPairs.size() != 1) {
+ throw new GeneralSecurityException(MessageFormat.format(
+ SshdText.get().signTooManyPrivateKeys, path));
+ }
+ return keyPairs.iterator().next();
+ } catch (AuthenticationCanceledException e) {
+ throw new CanceledException(e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ private static GpgSignature armor(byte[] data) throws IOException {
+ try (ByteArrayOutputStream b = new ByteArrayOutputStream()) {
+ b.write(SshSignatureConstants.ARMOR_HEAD);
+ b.write('\n');
+ String encoded = Base64.encodeBytes(data);
+ int length = encoded.length();
+ int column = 0;
+ for (int i = 0; i < length; i++) {
+ b.write(encoded.charAt(i));
+ column++;
+ if (column == LINE_LENGTH) {
+ b.write('\n');
+ column = 0;
+ }
+ }
+ if (column > 0) {
+ b.write('\n');
+ }
+ b.write(SshSignatureConstants.ARMOR_END);
+ b.write('\n');
+ return new GpgSignature(b.toByteArray());
+ }
+ }
+
+ private static PublicKey fromEntry(List<AuthorizedKeyEntry> entries)
+ throws GeneralSecurityException, IOException {
+ if (entries == null || entries.size() != 1) {
+ return null;
+ }
+ return entries.get(0).resolvePublicKey(null,
+ PublicKeyEntryResolver.FAILING);
+ }
+
+ @Override
+ public boolean canLocateSigningKey(Repository repository, GpgConfig config,
+ PersonIdent committer, String signingKey,
+ CredentialsProvider credentialsProvider) throws CanceledException {
+ String key = signingKey;
+ if (key == null) {
+ key = config.getSigningKey();
+ }
+ return !(StringUtils.isEmptyOrNull(key)
+ && StringUtils.isEmptyOrNull(config.getSshDefaultKeyCommand()));
+ }
+
+ private static class KeyPairIdentity implements PublicKeyIdentity {
+
+ private final @NonNull KeyPair pair;
+
+ KeyPairIdentity(@NonNull KeyPair pair) {
+ this.pair = pair;
+ }
+
+ @Override
+ public KeyPair getKeyIdentity() {
+ return pair;
+ }
+
+ @Override
+ public Entry<String, byte[]> sign(SessionContext session, String algo,
+ byte[] data) throws Exception {
+ BuiltinSignatures factory = BuiltinSignatures.fromFactoryName(algo);
+ if (factory == null || !factory.isSupported()) {
+ throw new GeneralSecurityException(MessageFormat.format(
+ SshdText.get().signUnknownSignatureAlgorithm, algo));
+ }
+ Signature signer = factory.create();
+ signer.initSigner(null, pair.getPrivate());
+ signer.update(null, data);
+ return new SimpleImmutableEntry<>(factory.getName(),
+ signer.sign(null));
+ }
+ }
+
+ private static class AgentIdentity extends KeyPairIdentity {
+
+ AgentIdentity(PublicKey publicKey) {
+ super(new KeyPair(publicKey, null));
+ }
+
+ @Override
+ public Entry<String, byte[]> sign(SessionContext session, String algo,
+ byte[] data) throws Exception {
+ ConnectorFactory factory = ConnectorFactory.getDefault();
+ Connector connector = factory == null ? null
+ : factory.create("", null); //$NON-NLS-1$
+ if (connector == null) {
+ throw new IOException(SshdText.get().signNoAgent);
+ }
+ try (SshAgentClient agent = new SshAgentClient(connector)) {
+ return agent.sign(null, getKeyIdentity().getPublic(), algo,
+ data);
+ }
+ }
+ }
+}
/***/ public String sshCommandTimeout;
/***/ public String sshProcessStillRunning;
/***/ public String sshProxySessionCloseFailed;
+ /***/ public String signCertAlgorithmMismatch;
+ /***/ public String signCertAlgorithmUnknown;
+ /***/ public String signCertificateExpired;
+ /***/ public String signCertificateInvalid;
+ /***/ public String signCertificateTooEarly;
+ /***/ public String signDefaultKeyEmpty;
+ /***/ public String signDefaultKeyFailed;
+ /***/ public String signDefaultKeyInterrupted;
+ /***/ public String signInvalidKeyDSA;
+ /***/ public String signLogFailure;
+ /***/ public String signNoAgent;
+ /***/ public String signNoPublicKey;
+ /***/ public String signNoSigningKey;
+ /***/ public String signNotUserCertificate;
+ /***/ public String signPublicKeyError;
+ /***/ public String signSeeLog;
+ /***/ public String signSignatureError;
+ /***/ public String signStderr;
+ /***/ public String signTooManyPrivateKeys;
+ /***/ public String signTooManyPublicKeys;
+ /***/ public String signUnknownHashAlgorithm;
+ /***/ public String signUnknownSignatureAlgorithm;
/***/ public String unknownProxyProtocol;
}
--- /dev/null
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.signing.ssh;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.internal.signing.ssh.SshSigner;
+import org.eclipse.jgit.lib.SignerFactory;
+
+/**
+ * Factory creating {@link SshSigner}s.
+ *
+ * @since 7.1
+ */
+public final class SshSignerFactory implements SignerFactory {
+
+ @Override
+ public GpgFormat getType() {
+ return GpgFormat.SSH;
+ }
+
+ @Override
+ public Signer create() {
+ return new SshSigner();
+ }
+}