From c9958e9b7a9b023acb214efcabfc89525859c588 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sat, 28 Sep 2024 15:58:20 +0200 Subject: [PATCH] SSH signing: implement a Signer Implement a Signer and its factory, and publish the factory for the ServiceLoader. SSH signatures can be created directly if the key is given via a file in user.signingKey and the private key can be found. Otherwise, signing is delegated to an SSH agent, if available. If a certificate is used as public key, the signer verifies the certificate (correct signature, and valid at the commit time). SSH signatures are documented at [1]. [1] https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig Bug: jgit-44 Change-Id: I3848ccc06ba3be5e868f879bd5705fee1b39c632 Signed-off-by: Thomas Wolf --- org.eclipse.jgit.ssh.apache.test/.classpath | 5 + .../.gitattributes | 1 + org.eclipse.jgit.ssh.apache.test/BUILD | 47 +- .../META-INF/MANIFEST.MF | 8 +- org.eclipse.jgit.ssh.apache.test/pom.xml | 6 + .../eclipse/jgit/internal/signing/ssh/ca_key | 7 + .../jgit/internal/signing/ssh/ca_key.pub | 1 + .../eclipse/jgit/internal/signing/ssh/ca_key2 | 7 + .../jgit/internal/signing/ssh/ca_key2.pub | 1 + .../internal/signing/ssh/certs/expired.cert | 1 + .../signing/ssh/certs/no_principals.cert | 1 + .../internal/signing/ssh/certs/other-ca.cert | 1 + .../internal/signing/ssh/certs/other.cert | 1 + .../internal/signing/ssh/certs/tester.cert | 1 + .../signing/ssh/certs/two_principals.cert | 1 + .../jgit/internal/signing/ssh/other_key | 7 + .../internal/signing/ssh/other_key-cert.pub | 1 + .../jgit/internal/signing/ssh/other_key.pub | 1 + .../jgit/internal/signing/ssh/signing_key | 7 + .../internal/signing/ssh/signing_key-cert.pub | 1 + .../jgit/internal/signing/ssh/signing_key.pub | 1 + .../signing/ssh/AbstractSshSignatureTest.java | 115 +++++ .../signing/ssh/SshCertificateUtilsTest.java | 107 ++++ .../internal/signing/ssh/SshSignerTest.java | 62 +++ .../META-INF/MANIFEST.MF | 5 +- .../org.eclipse.jgit.lib.SignerFactory | 1 + .../transport/sshd/SshdText.properties | 22 + .../signing/ssh/SshCertificateUtils.java | 175 +++++++ .../signing/ssh/SshSignatureConstants.java | 38 ++ .../jgit/internal/signing/ssh/SshSigner.java | 485 ++++++++++++++++++ .../internal/transport/sshd/SshdText.java | 22 + .../jgit/signing/ssh/SshSignerFactory.java | 33 ++ 32 files changed, 1161 insertions(+), 11 deletions(-) create mode 100644 org.eclipse.jgit.ssh.apache.test/.gitattributes create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub create mode 100644 org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java create mode 100644 org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java create mode 100644 org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java create mode 100644 org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java create mode 100644 org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java diff --git a/org.eclipse.jgit.ssh.apache.test/.classpath b/org.eclipse.jgit.ssh.apache.test/.classpath index 6fdb99a4b2..5be47afffb 100644 --- a/org.eclipse.jgit.ssh.apache.test/.classpath +++ b/org.eclipse.jgit.ssh.apache.test/.classpath @@ -11,5 +11,10 @@ + + + + + diff --git a/org.eclipse.jgit.ssh.apache.test/.gitattributes b/org.eclipse.jgit.ssh.apache.test/.gitattributes new file mode 100644 index 0000000000..c1f1b7371a --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/.gitattributes @@ -0,0 +1 @@ +/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle binary \ No newline at end of file diff --git a/org.eclipse.jgit.ssh.apache.test/BUILD b/org.eclipse.jgit.ssh.apache.test/BUILD index b384464484..bb31f8b3c7 100644 --- a/org.eclipse.jgit.ssh.apache.test/BUILD +++ b/org.eclipse.jgit.ssh.apache.test/BUILD @@ -1,19 +1,48 @@ +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/$@ .", +) diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index 546dfd4a1d..c8300aa8c0 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -17,6 +17,7 @@ Import-Package: org.apache.sshd.client.config.hosts;version="[2.14.0,2.15.0)", 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)", @@ -24,15 +25,20 @@ Import-Package: org.apache.sshd.client.config.hosts;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)" diff --git a/org.eclipse.jgit.ssh.apache.test/pom.xml b/org.eclipse.jgit.ssh.apache.test/pom.xml index b3e3d31eea..8e123867e4 100644 --- a/org.eclipse.jgit.ssh.apache.test/pom.xml +++ b/org.eclipse.jgit.ssh.apache.test/pom.xml @@ -91,6 +91,12 @@ src/ tst/ + + + tst-rsrc/ + + + org.apache.maven.plugins diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key new file mode 100644 index 0000000000..b8de8c3353 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAJAhCMgzIQjI +MwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQ +AAAEBmcXpast20+B4IzA0Xex2CKYiiWJj3NFJ5F0kil113vcdEl+iOTEbf1RC3uicECtid ++SaIMsAw7wrlWhOQTyBVAAAADVRIV09AU0VBR044MDA= +-----END OPENSSH PRIVATE KEY----- diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub new file mode 100644 index 0000000000..842415b0e8 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 new file mode 100644 index 0000000000..a4af0479bc --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAJAa2jfBGto3 +wQAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgw +AAAEBothGMqFaA5aTO8MLx9wm1oDRfzQCSsu7uJwrOiUFTTTtLjJwgHqhPHhQ3yX0367TX +pSAzOSttPsT7ZdsbfmODAAAADVRIV09AU0VBR044MDA= +-----END OPENSSH PRIVATE KEY----- diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub new file mode 100644 index 0000000000..e46c87e83f --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDtLjJwgHqhPHhQ3yX0367TXpSAzOSttPsT7ZdsbfmOD diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert new file mode 100644 index 0000000000..9da63ec900 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINFZ5NKywAWh1G1P6BiBKArmYKs1BDhJBOawJKlS29VXAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAEAAAABAAAADGV4cGlyZWRfY2VydAAAAAAAAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgx0SX6I5MRt/VELe6JwQK2J35JogywDDvCuVaE5BPIFUAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNf8i5dhRqWRe06epIRrZ5V+QZHq3ZrlJtlx98UJya9GAeCrJ5oHwBjr5O5TL5wNJS5Hz+T1qsJNFU9d1wdcuwI= diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert new file mode 100644 index 0000000000..101e37469d --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILzuED1RSloB/enTghTEKSACVOuEARP0f8UVXSRwEXN6AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAIAAAABAAAADW5vX3ByaW5jaXBhbHMAAAAAAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBwEQ2D0OHn4QDHnsINlgWUWpmhukseQCJu3Adulz28fFtewp1LLqkBy50wR6vJe1ifYbY4hzReXOSyoTmHSXEN diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert new file mode 100644 index 0000000000..752fee1778 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIHNW2bzSS61lvgHippv3Ymx4cVEAXBVCb8lFXHnVpsSyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAYAAAABAAAAB3Rlc3RlcjIAAAAWAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAFMAAAALc3NoLWVkMjU1MTkAAABAuJ8zBazcaYTbUEr9QtoYox0MkVBg+8LANxJxc885M2vmg9yPHpTfV/emupqhBwuYcPJSskTxl7WX4TUNvhMsAA== diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert new file mode 100644 index 0000000000..15825f6055 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGKXzyrvDzj9ObQ4SuzqytK6nomOV8DhgdzODfWuup1sAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAQAAAABAAAABW90aGVyAAAAFQAAABFvdGhlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABA1ycFqWehyC6pIISEkXSTtHbatLWl9HHAoUFouQiDdubAnMDRSkyHipXR62rq+8yEAvtqm1mXBzO8nLalkF9xAA== diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert new file mode 100644 index 0000000000..a2b241c241 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICSl1xsyTWb23YlKo21musxOzj4L4eD2coTkHbBw2uOyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAMAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDyjzq/0Egm1OxwrvqPZKUihE3w357Ji9Nd3j7VnUuvSYTXAdB9P0E+a2hyCcemmsil1MsvWTiCSSOsrHVB6FEO diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert new file mode 100644 index 0000000000..5f7164a7fc --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFmWKr9gNSQT0vna7k3uOyUF9CTcMGw2zxTFBf2Ev8TzAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAgAAAABAAAABnRlc3RlcgAAACkAAAAPZm9vQGV4YW1wbGUuY29tAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABAqlSX2GzLz5U+hN/gF9UUyAkE6h5BgVFYhsyf1MR/B7Hoxa29wGLbJpUplrqEHMxoud2zfH2Nhj00unc3lr5bBA== ./signing_key.pub diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key new file mode 100644 index 0000000000..ee3f922c2f --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiwAAAJDdMhQO3TIU +DgAAAAtzc2gtZWQyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiw +AAAEA4NlTFs3h2zqt5pSZ5S3dJb42GE7EjG16coKj70eELNDeK9TvB/fuMTUhMLkpCNV5W +XIVOSBYnG5vy3fzSi+OLAAAADVRIV09AU0VBR044MDA= +-----END OPENSSH PRIVATE KEY----- diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub new file mode 100644 index 0000000000..2be08be740 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGXo4+L/NyBl1VQDP39PxJP3LSzaqopqZGVP3cG0WoFAAAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OLAAAAAAAAAAUAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA/HwKB8J/kvkEsdxDou+UebnR9u30xPH6FEnbHLlfKbKMIXwLFIHnf9F6bTL36WhFDEDcSBGS19VBWBDRosM8L diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub new file mode 100644 index 0000000000..0255005400 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OL diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key new file mode 100644 index 0000000000..3dd37be6b2 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czgAAAJDhSMqA4UjK +gAAAAAtzc2gtZWQyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czg +AAAEB1yC00NMYEAVzhDj9odGVL0EonaIkf5jdUZ/czJ0+SPWATOZ8PcOKdY978fzIstnZ0 ++FuefIWKp7wRZynQLdzOAAAADVRIV09AU0VBR044MDA= +-----END OPENSSH PRIVATE KEY----- diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub new file mode 100644 index 0000000000..de191d1870 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFEmoWkYraMju0JI0b/0RQtR6RYo/OVp53EVf48L/Pu/AAAAIBmHlkHFlA7HkoTZcau80PH5zduQu41m8BqnH/1v2BwVAAAAAAAAAAEAAAABAAAACGFfa2V5X2lkAAAAFgAAABJ0ZXN0ZXJAZXhhbXBsZS5jb20AAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg2ifM9NMuXwQf7H/H5LCMhMjVqugyyN+jmcMoJUL2YLAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQG1kXUido46YOnmwvkJuIAKyp6Q9Gr+lbdOQvU0St/Hc9HTTIxgDGyLpv0alIJpHOuSYUUUxDufvGKtLJK1duwg= ./signing_key.pub diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub new file mode 100644 index 0000000000..e1210e72c0 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java new file mode 100644 index 0000000000..e13379be72 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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; + } +} diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java new file mode 100644 index 0000000000..79ca21fa35 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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 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)); + } +} diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java new file mode 100644 index 0000000000..b3a4482d23 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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")); + } +} diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index 69f23a5ebe..512c19f4c2 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -8,7 +8,8 @@ Bundle-Localization: OSGI-INF/l10n/plugin 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, @@ -90,10 +91,12 @@ Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)", 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)", diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory new file mode 100644 index 0000000000..80f22c055f --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory @@ -0,0 +1 @@ +org.eclipse.jgit.signing.ssh.SshSignerFactory \ No newline at end of file diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index 7da7181887..e51c80a5ac 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -125,4 +125,26 @@ sshClosingDown=Apache MINA sshd session factory is closing down; cannot create n 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 diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java new file mode 100644 index 0000000000..040c6d4368 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java new file mode 100644 index 0000000000..bc72196a22 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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 + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java new file mode 100644 index 0000000000..8cfe5f4766 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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 PROTOCOL.sshsig + */ +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 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 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 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 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 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); + } + } + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 05f04ac5b2..6f7a4e9ef4 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -147,6 +147,28 @@ public final class SshdText extends TranslationBundle { /***/ 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; } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java new file mode 100644 index 0000000000..5459b5360a --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024, Thomas Wolf 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(); + } +} -- 2.39.5