View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm.provider.git;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.Writer;
24  import java.nio.file.FileSystem;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.attribute.PosixFilePermissions;
28  import java.security.GeneralSecurityException;
29  import java.security.KeyPair;
30  import java.security.PublicKey;
31  import java.util.ArrayList;
32  import java.util.List;
33  
34  import org.apache.maven.scm.provider.git.sshd.git.pack.GitPackCommandFactory;
35  import org.apache.sshd.common.config.keys.KeyUtils;
36  import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyEncryptionContext;
37  import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter;
38  import org.apache.sshd.git.GitLocationResolver;
39  import org.apache.sshd.server.SshServer;
40  import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
41  import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
42  import org.apache.sshd.server.session.ServerSession;
43  import org.apache.sshd.util.test.CommonTestSupportUtils;
44  import org.apache.sshd.util.test.CoreTestSupportUtils;
45  import org.bouncycastle.openssl.PKCS8Generator;
46  import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
47  import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
48  import org.bouncycastle.util.io.pem.PemObject;
49  
50  /**
51   * Local Git SSH server for testing purposes.
52   * It acts on top of an existing repository root directory.
53   * It uses <a href="https://mina.apache.org/sshd-project/">Apache MINA SSHD</a> for the SSH server implementation.
54   * <p>
55   * The server generates a key pair during initialization and accepts connections using the private key which can be
56   * extracted via {@link GitSshServer#writePrivateKeyAsPkcs8(Path, String)}.
57   * Alternatively one may use a custom key pair and add the public key using {@link GitSshServer#addPublicKey(PublicKey)}.
58   */
59  public class GitSshServer {
60  
61      protected final SshServer sshServer;
62      protected final KeyPair keyPair;
63      protected final List<PublicKey> acceptedPublicKeys;
64  
65      public GitSshServer() throws GeneralSecurityException {
66          sshServer = CoreTestSupportUtils.setupTestServer(getClass());
67          keyPair = CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 2048);
68          acceptedPublicKeys = new ArrayList<>();
69          acceptedPublicKeys.add(keyPair.getPublic());
70          PublickeyAuthenticator authenticator = new KeySetPublickeyAuthenticator("onlykey", acceptedPublicKeys);
71          sshServer.setPublickeyAuthenticator(authenticator);
72      }
73  
74      /**
75       * Writes a private key which is accepted by this server to the specified file in PKCS8 format.
76       * If a passphrase is provided, the key will be encrypted using OpenSSH's format.
77       * If no passphrase is provided, the key will be written as an unencrypted PKCS8 private key.
78       * For the same server instance the private key is always the same, so it can be reused.
79       *
80       * @param file the file to write the private key to
81       * @param passphrase the passphrase for encryption, or null for unencrypted
82       * @throws IOException if an I/O error occurs
83       * @throws GeneralSecurityException if a security error occurs
84       */
85      public void writePrivateKeyAsPkcs8(Path file, String passphrase) throws IOException, GeneralSecurityException {
86          // encryption only optional
87          if (passphrase != null) {
88              // encryption with format outlined in https://dnaeon.github.io/openssh-private-key-binary-format/
89              OpenSSHKeyPairResourceWriter writer = new OpenSSHKeyPairResourceWriter();
90              OpenSSHKeyEncryptionContext context = new OpenSSHKeyEncryptionContext();
91              context.setCipherType("192");
92              context.setPassword(passphrase);
93              try (OutputStream output = Files.newOutputStream(file)) {
94                  writer.writePrivateKey(keyPair, "comment", context, output);
95              }
96          } else {
97              // wrap unencrypted private key as regular PKCS8 private key
98              PKCS8Generator pkcs8Generator = new JcaPKCS8Generator(keyPair.getPrivate(), null);
99              PemObject pemObject = pkcs8Generator.generate();
100 
101             try (Writer writer = Files.newBufferedWriter(file);
102                     JcaPEMWriter pw = new JcaPEMWriter(writer)) {
103                 pw.writeObject(pemObject);
104             }
105         }
106 
107         if (file.getFileSystem().supportedFileAttributeViews().contains("posix")) {
108             // must only be readable/writeable by me
109             Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rwx------"));
110         }
111     }
112 
113     public void addPublicKey(PublicKey publicKey) {
114         if (publicKey == null) {
115             throw new IllegalArgumentException("Public key must not be null");
116         }
117         acceptedPublicKeys.add(publicKey);
118     }
119 
120     public int getPort() {
121         if (!sshServer.isStarted()) {
122             throw new IllegalStateException("SSH server is not started");
123         }
124         return sshServer.getPort();
125     }
126 
127     public void start(Path repositoryRoot) throws IOException {
128         GitLocationResolver gitLocationResolver = new GitLocationResolver() {
129             @Override
130             public Path resolveRootDirectory(String command, String[] args, ServerSession session, FileSystem fs)
131                     throws IOException {
132                 return repositoryRoot;
133             }
134         };
135         // use patched version of GitPackCommandFactory including https://github.com/apache/mina-sshd/pull/794
136         sshServer.setCommandFactory(new GitPackCommandFactory(gitLocationResolver));
137         sshServer.start();
138     }
139 
140     public void stop() throws IOException {
141         sshServer.stop();
142     }
143 }