├── .github └── CODEOWNERS ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── spotify │ └── sshagentproxy │ ├── AgentInput.java │ ├── AgentOutput.java │ ├── AgentProxies.java │ ├── AgentProxy.java │ ├── AgentReplyHeaders.java │ ├── ByteIterator.java │ ├── DefaultIdentity.java │ ├── IdentitiesAnswerHeaders.java │ ├── Identity.java │ ├── Rsa.java │ ├── SignResponseHeaders.java │ └── TraditionalKeyParser.java └── test └── java └── com └── spotify └── sshagentproxy ├── AgentInputTest.java ├── AgentOutputTest.java ├── AgentProxiesTest.java ├── ByteIteratorTest.java ├── IdentitiesAnswerHeadersTest.java ├── RsaTest.java ├── SignResponseHeadersTest.java └── TestConstants.java /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @spotify/docker-admins 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | install: true 7 | 8 | script: mvn clean test 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | 13 | notifications: 14 | email: false 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh-agent-proxy 2 | 3 | [![Build Status](https://travis-ci.org/spotify/ssh-agent-proxy.svg?branch=master)](https://travis-ci.org/spotify/ssh-agent-proxy) 4 | [![codecov](https://codecov.io/gh/spotify/ssh-agent-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/spotify/ssh-agent-proxy) 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.spotify/ssh-agent-proxy.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.spotify%22%20ssh-agent-proxy) 6 | [![License](https://img.shields.io/github/license/spotify/ssh-agent-proxy.svg)](LICENSE) 7 | 8 | A Java library that talks to the local ssh-agent. This project is currently in beta phase. 9 | 10 | * [Download](#download) 11 | * [Getting started](#getting-started) 12 | * [Prerequisites](#prerequisites) 13 | * [Code of conduct](#code-of-conduct) 14 | 15 | ## Download 16 | 17 | Download the latest JAR or grab [via Maven][maven-search]. 18 | 19 | ```xml 20 | 21 | com.spotify 22 | ssh-agent-proxy 23 | 0.1.5 24 | 25 | ``` 26 | 27 | ## Getting started 28 | 29 | ```java 30 | import org.apache.commons.codec.binary.Hex; 31 | 32 | final byte[] dataToSign = {0xa, 0x2, (byte) 0xff}; 33 | final AgentProxy agentProxy = AgentProxies.newInstance(); 34 | final List identities = agentProxy.list(); 35 | for (final Identity identity : identities) { 36 | if (identity.getPublicKey().getAlgorithm().equals("RSA")) { 37 | final byte[] signedData = agentProxy.sign(identity, dataToSign); 38 | System.out.println(Hex.encodeHexString(signedData)); 39 | } 40 | } 41 | ``` 42 | 43 | 44 | ## Prerequisities 45 | 46 | Any platform that has the following 47 | 48 | * Java 7+ 49 | * Maven 3 (for compiling) 50 | 51 | ## Releasing 52 | 53 | To cut the Maven release: 54 | 55 | ``` 56 | mvn clean [-B -Dinvoker.skip -DskipTests -Darguments='-Dinvoker.skip -DskipTests'] \ 57 | -Dgpg.keyname= \ 58 | release:clean release:prepare release:perform 59 | ``` 60 | 61 | 62 | ## Code of conduct 63 | 64 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are 65 | expected to honor this code. 66 | 67 | [code-of-conduct]: https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md 68 | [maven-search]: https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.spotify%22%20ssh-agent-proxy 69 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.spotify 7 | foss-root 8 | 7 9 | 10 | 11 | ssh-agent-proxy 12 | 0.2.2-SNAPSHOT 13 | jar 14 | ssh-agent-proxy 15 | A Java library that talks to the local ssh-agent. 16 | https://github.com/spotify/ssh-agent-proxy 17 | 18 | 19 | 20 | The Apache Software License, Version 2.0 21 | http://www.apache.org/licenses/LICENSE-2.0.txt 22 | repo 23 | 24 | 25 | 26 | 27 | scm:git:https://github.com/spotify/ssh-agent-proxy.git 28 | scm:git:git@github.com:spotify/ssh-agent-proxy.git 29 | https://github.com/spotify/ssh-agent-proxy 30 | HEAD 31 | 32 | 33 | 34 | 35 | ossrh 36 | https://oss.sonatype.org/content/repositories/snapshots 37 | 38 | 39 | 40 | 41 | 42 | dxia 43 | David Xia 44 | dxia@spotify.com 45 | 46 | 47 | 48 | 49 | 50 | com.github.jnr 51 | jnr-unixsocket 52 | 0.38.19 53 | 54 | 55 | commons-codec 56 | commons-codec 57 | 1.15 58 | 59 | 60 | org.slf4j 61 | slf4j-api 62 | 1.7.36 63 | 64 | 65 | com.google.guava 66 | guava 67 | 17.0 68 | 69 | 70 | 71 | 72 | junit 73 | junit 74 | 4.13.1 75 | test 76 | 77 | 78 | org.mockito 79 | mockito-all 80 | 1.10.19 81 | test 82 | 83 | 84 | 85 | 86 | 87 | 88 | maven-checkstyle-plugin 89 | 90 | true 91 | warning 92 | 93 | 94 | 95 | maven-enforcer-plugin 96 | 97 | 98 | maven-failsafe-plugin 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-compiler-plugin 103 | 3.3 104 | 105 | 1.7 106 | 1.7 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/AgentInput.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static com.google.common.base.Preconditions.checkNotNull; 40 | import static com.google.common.base.Strings.isNullOrEmpty; 41 | 42 | import com.google.common.base.Objects; 43 | import com.google.common.base.Throwables; 44 | import com.google.common.collect.Lists; 45 | import java.io.Closeable; 46 | import java.io.IOException; 47 | import java.io.InputStream; 48 | import java.security.InvalidKeyException; 49 | import java.security.NoSuchAlgorithmException; 50 | import java.security.spec.InvalidKeySpecException; 51 | import java.util.Iterator; 52 | import java.util.List; 53 | import org.slf4j.Logger; 54 | import org.slf4j.LoggerFactory; 55 | 56 | /** 57 | * A class that represents ssh-agent input. 58 | */ 59 | class AgentInput implements Closeable { 60 | 61 | private static final Logger log = LoggerFactory.getLogger(AgentInput.class); 62 | 63 | private final InputStream in; 64 | 65 | AgentInput(final InputStream in) { 66 | checkNotNull(in, "InputStream cannot be null."); 67 | this.in = in; 68 | } 69 | 70 | /** 71 | * Return a list of {@link Identity} from the bytes in the ssh-agent's {@link InputStream}. 72 | * @return A list of {@link Identity} 73 | */ 74 | List readIdentitiesAnswer() throws IOException { 75 | // Read the first 9 bytes from the InputStream which are the 76 | // SSH2_AGENT_IDENTITIES_ANSWER headers. Return an IdentitiesAnswerHeaders object that 77 | // represents the message. 78 | final byte[] headerBytes = readBytes(9, "SSH2_AGENT_IDENTITIES_ANSWER"); 79 | log.debug("Received SSH2_AGENT_IDENTITIES_ANSWER message from ssh-agent."); 80 | final IdentitiesAnswerHeaders headers = IdentitiesAnswerHeaders.from(headerBytes); 81 | 82 | // 5 is the sum of the number of bytes of response code and count 83 | final byte[] bytes = readBytes(headers.getLength() - 5); 84 | final Iterator byteIterator = new ByteIterator(bytes); 85 | 86 | final List identities = Lists.newArrayList(); 87 | while (byteIterator.hasNext()) { 88 | final byte[] keyBlob = byteIterator.next(); 89 | final byte[] keyComment = byteIterator.next(); 90 | try { 91 | identities.add(DefaultIdentity.from(keyBlob, new String(keyComment))); 92 | } catch (InvalidKeyException | InvalidKeySpecException | NoSuchAlgorithmException 93 | | UnsupportedOperationException e) { 94 | log.warn("Unable to parse SSH identity. Skipping. {}", e); 95 | } 96 | } 97 | 98 | return identities; 99 | } 100 | 101 | /** 102 | * Return an array of bytes from the ssh-agent representing data signed by a private SSH key. 103 | * @return An array of signed bytes. 104 | */ 105 | byte[] readSignResponse() throws IOException { 106 | // Read the first 9 bytes from the InputStream which are the SSH2_AGENT_SIGN_RESPONSE headers. 107 | final byte[] headerBytes = readBytes(9, "SSH2_AGENT_SIGN_RESPONSE"); 108 | log.debug("Received SSH2_AGENT_SIGN_RESPONSE message from ssh-agent."); 109 | final SignResponseHeaders headers = SignResponseHeaders.from(headerBytes); 110 | 111 | // Read the rest of the SSH2_AGENT_SIGN_RESPONSE message from ssh-agent. 112 | // 5 is the sum of the number of bytes of response code and response length 113 | final byte[] bytes = readBytes(headers.getLength() - 5); 114 | final ByteIterator iterator = new ByteIterator(bytes); 115 | final byte[] responseType = iterator.next(); 116 | 117 | final String signatureFormatId = new String(responseType); 118 | if (!signatureFormatId.equals(Rsa.RSA_LABEL)) { 119 | throw new RuntimeException("I unexpectedly got a non-Rsa signature format ID in the " 120 | + "SSH2_AGENT_SIGN_RESPONSE's signature blob."); 121 | } 122 | 123 | return iterator.next(); 124 | } 125 | 126 | /** 127 | * Read n bytes from the {@link InputStream}. 128 | * @param numBytes bytes to read 129 | * @return byte[] 130 | */ 131 | private byte[] readBytes(final int numBytes) throws IOException { 132 | return readBytes(numBytes, null); 133 | } 134 | 135 | /** 136 | * Read n bytes from the {@link InputStream}. 137 | * @param numBytes bytes to read 138 | * @param messageType An optional String indicating the expected SSH2 agent's message type. 139 | * @return byte[] 140 | */ 141 | private byte[] readBytes(final int numBytes, String messageType) throws IOException { 142 | final String errMsg = isNullOrEmpty(messageType) 143 | ? "Error reading from ssh-agent." 144 | : "Error reading " + messageType + " from ssh-agent."; 145 | 146 | final byte[] result = new byte[numBytes]; 147 | 148 | final int bytesRead; 149 | try { 150 | bytesRead = in.read(result, 0, numBytes); 151 | } catch (IOException e) { 152 | log.error(errMsg); 153 | throw Throwables.propagate(e); 154 | } 155 | 156 | if (bytesRead == -1) { 157 | log.error(errMsg); 158 | throw new IOException(errMsg); 159 | } 160 | 161 | return result; 162 | } 163 | 164 | @Override 165 | public void close() throws IOException { 166 | in.close(); 167 | } 168 | 169 | @Override 170 | public String toString() { 171 | return Objects.toStringHelper(this) 172 | .add("in", in) 173 | .toString(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/AgentOutput.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static com.google.common.base.Preconditions.checkNotNull; 40 | 41 | import com.google.common.base.Objects; 42 | import java.io.Closeable; 43 | import java.io.IOException; 44 | import java.io.OutputStream; 45 | import java.math.BigInteger; 46 | import java.nio.ByteBuffer; 47 | import java.security.interfaces.RSAPublicKey; 48 | import org.slf4j.Logger; 49 | import org.slf4j.LoggerFactory; 50 | 51 | /** 52 | * A class that represents the ssh-agent output. 53 | */ 54 | class AgentOutput implements Closeable { 55 | 56 | private static final Logger log = LoggerFactory.getLogger(AgentOutput.class); 57 | 58 | // Number of bytes in an int 59 | private static final int INT_BYTES = 4; 60 | 61 | // ssh-agent communication protocol constants 62 | private static final int SSH2_AGENTC_REQUEST_IDENTITIES = 11; 63 | private static final int SSH2_AGENTC_SIGN_REQUEST = 13; 64 | 65 | private final OutputStream out; 66 | 67 | AgentOutput(final OutputStream out) { 68 | checkNotNull(out, "OutputStream cannot be null."); 69 | this.out = out; 70 | } 71 | 72 | /** 73 | * Send a SSH2_AGENTC_REQUEST_IDENTITIES message to ssh-agent. 74 | */ 75 | void requestIdentities() throws IOException { 76 | writeField(out, SSH2_AGENTC_REQUEST_IDENTITIES); 77 | log.debug("Sent SSH2_AGENTC_REQUEST_IDENTITIES message to ssh-agent."); 78 | } 79 | 80 | /** 81 | * Convert int to a big-endian byte array containing the minimum number of bytes required to 82 | * represent it. Write those bytes to an {@link OutputStream}. 83 | * @param out {@link OutputStream} 84 | * @param num int 85 | */ 86 | private static void writeField(final OutputStream out, final int num) throws IOException { 87 | final byte[] bytes = BigInteger.valueOf(num).toByteArray(); 88 | writeField(out, bytes); 89 | } 90 | 91 | /** 92 | * Write bytes to an {@link OutputStream} and prepend with four bytes indicating their length. 93 | * @param out {@link OutputStream} 94 | * @param bytes Array of bytes. 95 | */ 96 | private static void writeField(final OutputStream out, final byte[] bytes) 97 | throws IOException { 98 | // All protocol messages are prefixed with their length in bytes, encoded 99 | // as a 32 bit unsigned integer. 100 | final ByteBuffer buffer = ByteBuffer.allocate(INT_BYTES + bytes.length); 101 | buffer.putInt(bytes.length); 102 | buffer.put(bytes); 103 | out.write(buffer.array()); 104 | out.flush(); 105 | } 106 | 107 | /** 108 | * Send a SSH2_AGENTC_SIGN_REQUEST message to ssh-agent. 109 | * @param rsaPublicKey The {@link RSAPublicKey} that tells ssh-agent which private key to use to 110 | * sign the data. 111 | * @param data The data in bytes to be signed. 112 | */ 113 | void signRequest(final RSAPublicKey rsaPublicKey, final byte[] data) throws IOException { 114 | // TODO (dxia) Support more than just Rsa keys 115 | final String keyType = Rsa.RSA_LABEL; 116 | final byte[] publicExponent = rsaPublicKey.getPublicExponent().toByteArray(); 117 | final byte[] modulus = rsaPublicKey.getModulus().toByteArray(); 118 | 119 | // Four bytes indicating length of string denoting key type 120 | // Four bytes indicating length of public exponent 121 | // Four bytes indicating length of modulus 122 | final int publicKeyLength = 4 + keyType.length() 123 | + 4 + publicExponent.length 124 | + 4 + modulus.length; 125 | 126 | // The message is made of: 127 | // Four bytes indicating length in bytes of rest of message 128 | // One byte indicating SSH2_AGENTC_SIGN_REQUEST 129 | // Four bytes denoting length of public key 130 | // Bytes representing the public key 131 | // Four bytes for length of data 132 | // Bytes representing data to be signed 133 | // Four bytes of flags 134 | final ByteBuffer buff = ByteBuffer.allocate( 135 | INT_BYTES + 1 + INT_BYTES + publicKeyLength + INT_BYTES + data.length + 4); 136 | 137 | // 13 = 138 | // One byte indicating SSH2_AGENTC_SIGN_REQUEST 139 | // Four bytes denoting length of public key 140 | // Four bytes for length of data 141 | // Four bytes of flags 142 | buff.putInt(publicKeyLength + data.length + 13); 143 | buff.put((byte) SSH2_AGENTC_SIGN_REQUEST); 144 | 145 | // Add the public key 146 | buff.putInt(publicKeyLength); 147 | buff.putInt(keyType.length()); 148 | for (final byte b : keyType.getBytes()) { 149 | buff.put(b); 150 | } 151 | buff.putInt(publicExponent.length); 152 | buff.put(publicExponent); 153 | buff.putInt(modulus.length); 154 | buff.put(modulus); 155 | 156 | // Add the data to be signed 157 | buff.putInt(data.length); 158 | buff.put(data); 159 | 160 | // Add empty flags 161 | buff.put(new byte[] {0, 0, 0, 0}); 162 | 163 | out.write(buff.array()); 164 | out.flush(); 165 | 166 | log.debug("Sent SSH2_AGENTC_SIGN_REQUEST message to ssh-agent."); 167 | } 168 | 169 | @Override 170 | public void close() throws IOException { 171 | out.close(); 172 | } 173 | 174 | @Override 175 | public String toString() { 176 | return Objects.toStringHelper(this) 177 | .add("out", out) 178 | .toString(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/AgentProxies.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static com.google.common.base.Strings.isNullOrEmpty; 40 | 41 | import com.google.common.annotations.VisibleForTesting; 42 | import com.google.common.base.Objects; 43 | import com.google.common.base.Throwables; 44 | import java.io.File; 45 | import java.io.IOException; 46 | import java.nio.channels.Channels; 47 | import java.security.interfaces.RSAPublicKey; 48 | import java.util.List; 49 | import jnr.unixsocket.UnixSocketAddress; 50 | import jnr.unixsocket.UnixSocketChannel; 51 | import org.slf4j.Logger; 52 | import org.slf4j.LoggerFactory; 53 | 54 | /** 55 | * This class contains a static factory method that creates a default implementation of 56 | * {@link AgentProxy}. 57 | */ 58 | public class AgentProxies { 59 | 60 | public static AgentProxy newInstance() { 61 | return DefaultAgentProxy.fromEnvironmentVariable(); 62 | } 63 | 64 | static AgentProxy withCustomInputOutput(final AgentInput in, final AgentOutput out) { 65 | return new DefaultAgentProxy(in, out); 66 | } 67 | 68 | static class DefaultAgentProxy implements AgentProxy { 69 | 70 | private static final Logger log = LoggerFactory.getLogger(DefaultAgentProxy.class); 71 | 72 | private final AgentInput in; 73 | private final AgentOutput out; 74 | 75 | public static DefaultAgentProxy fromEnvironmentVariable() { 76 | final String socketPath = System.getenv("SSH_AUTH_SOCK"); 77 | if (isNullOrEmpty(socketPath)) { 78 | throw new RuntimeException( 79 | "The environment variable SSH_AUTH_SOCK is not set. Please configure your ssh-agent."); 80 | } 81 | 82 | try { 83 | final UnixSocketChannel channel = UnixSocketChannel.open( 84 | new UnixSocketAddress(new File(socketPath))); 85 | 86 | log.debug("connected to " + channel.getRemoteSocketAddress()); 87 | 88 | return new DefaultAgentProxy(new AgentInput(Channels.newInputStream(channel)), 89 | new AgentOutput(Channels.newOutputStream(channel))); 90 | } catch (IOException e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | 95 | @VisibleForTesting 96 | DefaultAgentProxy(final AgentInput in, final AgentOutput out) { 97 | this.out = out; 98 | this.in = in; 99 | } 100 | 101 | @Override 102 | public List list() throws IOException { 103 | out.requestIdentities(); 104 | return in.readIdentitiesAnswer(); 105 | } 106 | 107 | @Override 108 | public byte[] sign(final Identity identity, final byte[] data) throws IOException { 109 | // TODO (dxia) Support other SSH keys 110 | final String keyFormat = identity.getKeyFormat(); 111 | if (!keyFormat.equals(Rsa.RSA_LABEL)) { 112 | throw Throwables.propagate(new RuntimeException(String.format( 113 | "Unknown key type %s. This code currently only supports %s.", 114 | keyFormat, Rsa.RSA_LABEL))); 115 | } 116 | 117 | out.signRequest((RSAPublicKey) identity.getPublicKey(), data); 118 | return in.readSignResponse(); 119 | } 120 | 121 | @Override 122 | public void close() throws IOException { 123 | out.close(); 124 | in.close(); 125 | } 126 | 127 | @Override 128 | public String toString() { 129 | return Objects.toStringHelper(this) 130 | .toString(); 131 | } 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/AgentProxy.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import java.io.Closeable; 40 | import java.io.IOException; 41 | import java.util.List; 42 | 43 | public interface AgentProxy extends Closeable { 44 | 45 | /** 46 | * Get a list of public keys from the ssh-agent. 47 | * @return A list of {@link Identity} 48 | */ 49 | List list() throws IOException; 50 | 51 | /** 52 | * Ask the ssh-agent to hash and sign some data in the form of an array of bytes. 53 | * 54 | * According to the ssh-agent specs: 55 | * "Upon receiving this request, the agent will look up the private key that 56 | * corresponds to the public key contained in key_blob. It will use this 57 | * private key to sign the "data" and produce a signature blob using the 58 | * key type-specific method described in RFC 4253 section 6.6 "Public Key 59 | * Algorithms". 60 | * @param identity An array of bytes for data to be signed. 61 | * @param data An array of bytes for data to be signed. 62 | * @return An array of bytes of signed data. 63 | */ 64 | byte[] sign(final Identity identity, final byte[] data) throws IOException; 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/AgentReplyHeaders.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import java.nio.ByteBuffer; 40 | import java.util.Arrays; 41 | 42 | /** 43 | * An abstract class that represents a message headers from the ssh-agent. 44 | * There are always three headers consisting of the first four bytes, fifth byte, and next four 45 | * bytes. What these bytes mean depends on the message type. 46 | */ 47 | abstract class AgentReplyHeaders { 48 | 49 | /** 50 | * Get first four bytes as an int. 51 | * @param bytes Bytes to parse 52 | * @return int 53 | */ 54 | protected static int first(final byte[] bytes) { 55 | return intFromSubArray(bytes, 0, 4); 56 | } 57 | 58 | /** 59 | * Get fifth byte as an int. 60 | * @param bytes Bytes to parse 61 | * @return int 62 | */ 63 | protected static int second(final byte[] bytes) { 64 | return bytes[4]; 65 | } 66 | 67 | /** 68 | * Get sixth through ninth byte as an int. 69 | * @param bytes Bytes to parse 70 | * @return int 71 | */ 72 | protected static int third(final byte[] bytes) { 73 | return intFromSubArray(bytes, 5, 9); 74 | } 75 | 76 | /** 77 | * Take a slice of an array of bytes and interpret it as an int. 78 | * @param bytes Array of bytes 79 | * @param from Start index in the array 80 | * @param to End index in the array 81 | * @return int 82 | */ 83 | private static int intFromSubArray(final byte[] bytes, final int from, final int to) { 84 | final byte[] subBytes = Arrays.copyOfRange(bytes, from, to); 85 | final ByteBuffer wrap = ByteBuffer.wrap(subBytes); 86 | return wrap.getInt(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/ByteIterator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Objects; 40 | import java.util.Arrays; 41 | import java.util.Iterator; 42 | 43 | /** 44 | * Takes an array of bytes. On every call to next(), it reads the first four bytes as a length n 45 | * and returns the next n bytes after that. 46 | */ 47 | class ByteIterator implements Iterator { 48 | 49 | private final byte[] data; 50 | private int cursor; 51 | 52 | ByteIterator(final byte[] data) { 53 | this.data = data; 54 | this.cursor = 0; 55 | } 56 | 57 | @Override 58 | public boolean hasNext() { 59 | return this.cursor < this.data.length; 60 | } 61 | 62 | @Override 63 | public byte[] next() { 64 | int num = s2i(Arrays.copyOfRange(this.data, this.cursor, this.data.length)); 65 | this.cursor += 4; 66 | final byte[] bytes = Arrays.copyOfRange(this.data, this.cursor, this.cursor + num); 67 | this.cursor += num; 68 | return bytes; 69 | } 70 | 71 | /** 72 | * Read four bytes off the provided byte string and return the value as a big endian 73 | * 32 bit unsigned integer. 74 | * @param bytes Array of bytes. 75 | * @return int 76 | */ 77 | private int s2i(final byte[] bytes) { 78 | int num = 0; 79 | for (int i = 0; i < 4; i++) { 80 | num += (bytes[i] & 0xff) << ((3 - i) * 8); 81 | } 82 | return num; 83 | } 84 | 85 | @Override 86 | public void remove() { 87 | throw new UnsupportedOperationException(); 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return Objects.toStringHelper(this) 93 | .toString(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/DefaultIdentity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Objects; 40 | import java.security.InvalidKeyException; 41 | import java.security.NoSuchAlgorithmException; 42 | import java.security.PublicKey; 43 | import java.security.spec.InvalidKeySpecException; 44 | import java.util.Iterator; 45 | 46 | public class DefaultIdentity implements Identity { 47 | 48 | private static final String RSA_LABEL = "ssh-rsa"; 49 | private static final String DSS_LABEL = "ssh-dss"; 50 | 51 | private final String keyFormat; 52 | private final PublicKey publicKey; 53 | private final String comment; 54 | private final byte[] keyBlob; 55 | 56 | private DefaultIdentity(final String keyFormat, final PublicKey publicKey, final String comment, 57 | final byte[] keyBlob) { 58 | this.keyFormat = keyFormat; 59 | this.publicKey = publicKey; 60 | this.comment = comment; 61 | this.keyBlob = keyBlob; 62 | } 63 | 64 | public static Identity from(final byte[] keyBlob, final String comment) 65 | throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException { 66 | final Iterator keyBlobIterator = new ByteIterator(keyBlob); 67 | final String keyFormat = new String(keyBlobIterator.next()); 68 | 69 | final PublicKey publicKey; 70 | switch (keyFormat) { 71 | case RSA_LABEL: 72 | publicKey = Rsa.from(keyBlob); 73 | break; 74 | case DSS_LABEL: 75 | default: 76 | throw new UnsupportedOperationException(String.format( 77 | "Got unsupported key format '%s'. Skipping.", keyFormat)); 78 | } 79 | 80 | keyBlobIterator.next(); 81 | return new DefaultIdentity(keyFormat, publicKey, comment, keyBlob); 82 | } 83 | 84 | @Override 85 | public String getKeyFormat() { 86 | return keyFormat; 87 | } 88 | 89 | public PublicKey getPublicKey() { 90 | return publicKey; 91 | } 92 | 93 | public String getComment() { 94 | return comment; 95 | } 96 | 97 | @Override 98 | public byte[] getKeyBlob() { 99 | return keyBlob; 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return Objects.toStringHelper(this) 105 | .add("keyFormat", keyFormat) 106 | .add("publicKey", publicKey) 107 | .add("comment", comment) 108 | .toString(); 109 | } 110 | 111 | @Override 112 | public boolean equals(final Object obj) { 113 | if (this == obj) { 114 | return true; 115 | } 116 | if (obj == null || getClass() != obj.getClass()) { 117 | return false; 118 | } 119 | 120 | final DefaultIdentity that = (DefaultIdentity) obj; 121 | 122 | if (keyFormat != null ? !keyFormat.equals(that.keyFormat) : that.keyFormat != null) { 123 | return false; 124 | } 125 | if (publicKey != null ? !publicKey.equals(that.publicKey) : that.publicKey != null) { 126 | return false; 127 | } 128 | return !(comment != null ? !comment.equals(that.comment) : that.comment != null); 129 | 130 | } 131 | 132 | @Override 133 | public int hashCode() { 134 | int result = keyFormat != null ? keyFormat.hashCode() : 0; 135 | result = 31 * result + (publicKey != null ? publicKey.hashCode() : 0); 136 | result = 31 * result + (comment != null ? comment.hashCode() : 0); 137 | return result; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/IdentitiesAnswerHeaders.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Objects; 40 | 41 | /** 42 | * A class that represents SSH2_AGENT_IDENTITIES_ANSWER headers from the ssh-agent. 43 | */ 44 | class IdentitiesAnswerHeaders extends AgentReplyHeaders { 45 | 46 | // ssh-agent communication protocol constants 47 | static final int SSH2_AGENT_IDENTITIES_ANSWER = 12; 48 | 49 | private final int length; 50 | private final int responseCode; 51 | private final int count; 52 | 53 | private IdentitiesAnswerHeaders(final int length, final int responseCode, final int count) { 54 | this.length = length; 55 | this.responseCode = responseCode; 56 | this.count = count; 57 | } 58 | 59 | static IdentitiesAnswerHeaders from(final byte[] bytes) { 60 | if (bytes.length != 9) { 61 | throw new IllegalArgumentException("SSH2_AGENT_IDENTITIES_ANSWER headers need to be 9 bytes" 62 | + " (received " + bytes.length + ")"); 63 | } 64 | 65 | // First four bytes represents length in bytes of rest of message 66 | final int length = first(bytes); 67 | 68 | // Next byte is the response code 69 | final int responseCode = second(bytes); 70 | if (responseCode != SSH2_AGENT_IDENTITIES_ANSWER) { 71 | throw new RuntimeException("Got the wrong response code for SSH2_AGENT_IDENTITIES_ANSWER" 72 | + " (expected: " + SSH2_AGENT_IDENTITIES_ANSWER 73 | + ", received: " + responseCode + ")."); 74 | } 75 | 76 | // Next four bytes is the number of keys the agent has 77 | final int count = third(bytes); 78 | 79 | return new IdentitiesAnswerHeaders(length, responseCode, count); 80 | } 81 | 82 | int getLength() { 83 | return length; 84 | } 85 | 86 | int getResponseCode() { 87 | return responseCode; 88 | } 89 | 90 | int getCount() { 91 | return count; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return Objects.toStringHelper(this) 97 | .add("length", length) 98 | .add("responseCode", responseCode) 99 | .add("count", count) 100 | .toString(); 101 | } 102 | 103 | @Override 104 | public boolean equals(final Object obj) { 105 | if (this == obj) { 106 | return true; 107 | } 108 | if (obj == null || getClass() != obj.getClass()) { 109 | return false; 110 | } 111 | 112 | final IdentitiesAnswerHeaders that = (IdentitiesAnswerHeaders) obj; 113 | 114 | if (length != that.length) { 115 | return false; 116 | } 117 | if (responseCode != that.responseCode) { 118 | return false; 119 | } 120 | return count == that.count; 121 | 122 | } 123 | 124 | @Override 125 | public int hashCode() { 126 | int result = length; 127 | result = 31 * result + responseCode; 128 | result = 31 * result + count; 129 | return result; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/Identity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import java.security.PublicKey; 40 | 41 | /** 42 | * Represents a key held by ssh-agent. 43 | */ 44 | public interface Identity { 45 | 46 | /** 47 | * Return the key format as a string, e.g. "ssh-rsa", "ssh-dss", etc. 48 | */ 49 | String getKeyFormat(); 50 | 51 | /** 52 | * Return the {@link PublicKey}. 53 | */ 54 | PublicKey getPublicKey(); 55 | 56 | /** 57 | * Return the key comment as a string. 58 | */ 59 | String getComment(); 60 | 61 | /** 62 | * Return an array of bytes encoded as per RFC 4253 section 6.6 "Public Key Algorithms" 63 | * for either of the supported key types: "ssh-dss" or "ssh-rsa". 64 | */ 65 | byte[] getKeyBlob(); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/Rsa.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Objects; 40 | import java.security.InvalidKeyException; 41 | import java.security.KeyFactory; 42 | import java.security.NoSuchAlgorithmException; 43 | import java.security.interfaces.RSAPublicKey; 44 | import java.security.spec.InvalidKeySpecException; 45 | import java.security.spec.RSAPublicKeySpec; 46 | import java.util.Iterator; 47 | import org.apache.commons.codec.binary.Base64; 48 | 49 | class Rsa { 50 | 51 | static final String RSA_LABEL = "ssh-rsa"; 52 | 53 | private Rsa() { 54 | } 55 | 56 | /** 57 | * Create an {@link RSAPublicKey} from bytes. 58 | * @param key Array of bytes representing Rsa public key. 59 | * @return {@link RSAPublicKey} 60 | */ 61 | static RSAPublicKey from(final byte[] key) 62 | throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException { 63 | 64 | final String s = new String(key); 65 | final byte[] encoded; 66 | final String decoded; 67 | if (s.startsWith(RSA_LABEL)) { 68 | decoded = s.split(" ")[1]; 69 | encoded = Base64.decodeBase64(decoded); 70 | } else { 71 | encoded = key; 72 | decoded = Base64.encodeBase64String(key); 73 | } 74 | 75 | final Iterator fields = new ByteIterator(encoded); 76 | final String sigType = new String(fields.next()); 77 | if (!sigType.equals(RSA_LABEL)) { 78 | throw new RuntimeException(String.format( 79 | "Unknown key type %s. This code currently only supports %s.", sigType, RSA_LABEL)); 80 | } 81 | 82 | final RSAPublicKeySpec keySpec = 83 | TraditionalKeyParser.parsePemPublicKey(RSA_LABEL + " " + decoded + " "); 84 | final KeyFactory keyFactory = KeyFactory.getInstance("Rsa"); 85 | return (RSAPublicKey) keyFactory.generatePublic(keySpec); 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return Objects.toStringHelper(this) 91 | .toString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/SignResponseHeaders.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Objects; 40 | 41 | /** 42 | * A class that represents SSH2_AGENT_SIGN_RESPONSE headers from the ssh-agent. 43 | */ 44 | class SignResponseHeaders extends AgentReplyHeaders { 45 | 46 | // ssh-agent communication protocol constants 47 | static final int SSH2_AGENT_SIGN_RESPONSE = 14; 48 | 49 | private final int length; 50 | private final int responseCode; 51 | private final int responseLength; 52 | 53 | private SignResponseHeaders(final int length, 54 | final int responseCode, 55 | final int responseLength) { 56 | this.length = length; 57 | this.responseCode = responseCode; 58 | this.responseLength = responseLength; 59 | } 60 | 61 | static SignResponseHeaders from(final byte[] bytes) { 62 | if (bytes.length != 9) { 63 | throw new IllegalArgumentException("SSH2_AGENT_SIGN_RESPONSE headers need to be 9 bytes"); 64 | } 65 | 66 | // First four bytes represents length in bytes of rest of message 67 | final int length = first(bytes); 68 | 69 | // Next byte is the response code 70 | final int responseCode = second(bytes); 71 | if (responseCode != SSH2_AGENT_SIGN_RESPONSE) { 72 | throw new RuntimeException("Got the wrong response code for SSH2_AGENT_SIGN_RESPONSE."); 73 | } 74 | 75 | // Next four bytes is the response length 76 | final int responseLength = third(bytes); 77 | 78 | return new SignResponseHeaders(length, responseCode, responseLength); 79 | } 80 | 81 | int getLength() { 82 | return length; 83 | } 84 | 85 | int getResponseCode() { 86 | return responseCode; 87 | } 88 | 89 | int getResponseLength() { 90 | return responseLength; 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return Objects.toStringHelper(this) 96 | .add("length", length) 97 | .add("responseCode", responseCode) 98 | .add("responseLength", responseLength) 99 | .toString(); 100 | } 101 | 102 | @Override 103 | public boolean equals(final Object obj) { 104 | if (this == obj) { 105 | return true; 106 | } 107 | if (obj == null || getClass() != obj.getClass()) { 108 | return false; 109 | } 110 | 111 | final SignResponseHeaders that = (SignResponseHeaders) obj; 112 | 113 | if (length != that.length) { 114 | return false; 115 | } 116 | if (responseCode != that.responseCode) { 117 | return false; 118 | } 119 | return responseLength == that.responseLength; 120 | 121 | } 122 | 123 | @Override 124 | public int hashCode() { 125 | int result = length; 126 | result = 31 * result + responseCode; 127 | result = 31 * result + responseLength; 128 | return result; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/sshagentproxy/TraditionalKeyParser.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Charsets; 40 | import com.google.common.io.BaseEncoding; 41 | import com.google.common.primitives.UnsignedBytes; 42 | import java.math.BigInteger; 43 | import java.nio.ByteBuffer; 44 | import java.nio.ByteOrder; 45 | import java.security.InvalidKeyException; 46 | import java.security.spec.RSAPrivateKeySpec; 47 | import java.security.spec.RSAPublicKeySpec; 48 | import java.util.ArrayList; 49 | import java.util.Arrays; 50 | import java.util.List; 51 | import java.util.regex.Matcher; 52 | import java.util.regex.Pattern; 53 | 54 | /** 55 | * A set of utilities to parse private and public Rsa PEM keys as produced by ssh-keygen. 56 | */ 57 | class TraditionalKeyParser { 58 | 59 | private static final Pattern PUBLIC_KEY_PATTERN = Pattern.compile("^ssh-rsa (.+) .*$"); 60 | private static final Pattern PRIVATE_KEY_PATTERN = 61 | Pattern.compile("^-+BEGIN RSA PRIVATE KEY-+([^-]+)-+END RSA PRIVATE KEY-+$"); 62 | private static final int INTEGER_SIZE = Integer.SIZE; 63 | private static final String PUBLIC_KEY_TYPE = "ssh-rsa"; 64 | 65 | public static RSAPublicKeySpec parsePemPublicKey(String pemPublicKey) throws InvalidKeyException { 66 | Matcher matcher = PUBLIC_KEY_PATTERN.matcher(pemPublicKey); 67 | if (!matcher.matches()) { 68 | throw new InvalidKeyException(); 69 | } 70 | String pemKey = matcher.group(1); 71 | BaseEncoding encoding = BaseEncoding.base64(); 72 | byte[] derKey = encoding.decode(pemKey); 73 | ByteBuffer byteBuffer = ByteBuffer.wrap(derKey); 74 | byteBuffer.order(ByteOrder.BIG_ENDIAN); 75 | byte[] typeBytes = readVariableLengthOpaque(byteBuffer); 76 | byte[] expBytes = readVariableLengthOpaque(byteBuffer); 77 | byte[] modBytes = readVariableLengthOpaque(byteBuffer); 78 | if (typeBytes == null || expBytes == null || modBytes == null) { 79 | throw new InvalidKeyException(); 80 | } 81 | String type = new String(typeBytes, Charsets.US_ASCII); 82 | if (!type.equals(PUBLIC_KEY_TYPE)) { 83 | throw new InvalidKeyException(); 84 | } 85 | BigInteger exp = new BigInteger(expBytes); 86 | BigInteger mod = new BigInteger(modBytes); 87 | return new RSAPublicKeySpec(mod, exp); 88 | } 89 | 90 | public static RSAPrivateKeySpec parsePemPrivateKey(String pemPrivateKey) 91 | throws InvalidKeyException { 92 | pemPrivateKey = pemPrivateKey.replace("\n", ""); 93 | Matcher matcher = PRIVATE_KEY_PATTERN.matcher(pemPrivateKey); 94 | if (!matcher.matches()) { 95 | throw new InvalidKeyException(); 96 | } 97 | String pemKey = matcher.group(1); 98 | BaseEncoding encoding = BaseEncoding.base64(); 99 | byte[] derKey = encoding.decode(pemKey); 100 | List fields; 101 | try { 102 | fields = parsePrivateKeyAsn1(ByteBuffer.wrap(derKey)); 103 | } catch (IllegalArgumentException e) { 104 | throw new InvalidKeyException(e); 105 | } 106 | BigInteger mod = new BigInteger(fields.get(1)); 107 | BigInteger exp = new BigInteger(fields.get(3)); 108 | return new RSAPrivateKeySpec(mod, exp); 109 | } 110 | 111 | /** 112 | * This is a simplistic ASN.1 parser that can only parse a collection of primitive types. 113 | * 114 | * @param byteBuffer the raw byte representation of a Pcks1 private key. 115 | * @return A list of bytes array that represent the content of the original ASN.1 collection. 116 | */ 117 | private static List parsePrivateKeyAsn1(ByteBuffer byteBuffer) { 118 | final List collection = new ArrayList<>(); 119 | while (byteBuffer.hasRemaining()) { 120 | byte type = byteBuffer.get(); 121 | int length = UnsignedBytes.toInt(byteBuffer.get()); 122 | if ((length & 0x80) != 0) { 123 | int numberOfOctets = length ^ 0x80; 124 | length = 0; 125 | for (int i = 0; i < numberOfOctets; ++i) { 126 | int lengthChunk = UnsignedBytes.toInt(byteBuffer.get()); 127 | length += lengthChunk << (numberOfOctets - i - 1) * 8; 128 | } 129 | } 130 | if (length < 0) { 131 | throw new IllegalArgumentException(); 132 | } 133 | if (type == 0x30) { 134 | int position = byteBuffer.position(); 135 | byte[] data = Arrays.copyOfRange(byteBuffer.array(), position, position + length); 136 | return parsePrivateKeyAsn1(ByteBuffer.wrap(data)); 137 | } 138 | if (type == 0x02) { 139 | byte[] segment = new byte[length]; 140 | byteBuffer.get(segment); 141 | collection.add(segment); 142 | } 143 | } 144 | return collection; 145 | } 146 | 147 | private static byte[] readVariableLengthOpaque(ByteBuffer byteBuffer) { 148 | if (byteBuffer.position() + INTEGER_SIZE > byteBuffer.limit()) { 149 | return null; 150 | } 151 | int length = byteBuffer.getInt(); 152 | if (byteBuffer.position() + length > byteBuffer.limit()) { 153 | return null; 154 | } 155 | byte[] bytes = new byte[length]; 156 | byteBuffer.get(bytes); 157 | return bytes; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/AgentInputTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static org.hamcrest.CoreMatchers.equalTo; 40 | import static org.hamcrest.CoreMatchers.hasItems; 41 | import static org.junit.Assert.assertArrayEquals; 42 | import static org.junit.Assert.assertThat; 43 | 44 | import java.io.ByteArrayInputStream; 45 | import java.util.List; 46 | import org.junit.Test; 47 | 48 | public class AgentInputTest extends TestConstants { 49 | 50 | private static final byte[] BYTES = new byte[] { 51 | 0, 0, 2, 121, 12, 0, 0, 0, 2, 0, 0, 1, 23, 52 | 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 1, 0, 53 | -76, -73, -41, 64, 111, 53, 14, -80, 13, 118, 77, 98, 85, -78, -76, 36, 27, -39, 127, -117, 54 | 124, 118, -42, -37, -29, -88, 75, -63, -43, 68, -43, -91, -88, -57, -26, 102, -128, -83, -87, 55 | 60, 75, 62, -17, -106, 64, 35, -127, -27, -43, -100, -109, -62, -100, -12, 103, 3, -25, 91, 56 | -98, -41, -33, 110, 47, 99, -87, -7, -80, -62, 10, 122, -44, -16, 55, 8, 20, -104, -45, -24, 57 | -7, 21, -23, 9, 106, -97, 85, -69, 8, 92, 122, 44, -49, 95, -25, 45, 100, 75, -112, -123, 58 | 119, 9, 70, 85, -96, -67, -99, -98, -54, -96, 75, 84, -58, -102, 100, 33, 84, 91, -73, -74, 59 | -49, 2, -48, 71, 48, -110, -73, 123, -120, -97, -43, -108, 66, 52, -33, -2, 119, -106, 74, 60 | 25, -26, 0, 53, -92, -104, -60, -95, 9, -28, -38, -32, 40, -43, 48, -15, 115, -101, 94, 5, 61 | -69, 83, 71, 121, 1, 36, -112, 7, -47, -1, -13, 13, 4, -45, -86, 21, -47, -31, 64, -2, 115, 62 | 34, 79, -106, -1, -25, 24, 107, 0, -105, -50, 100, -23, -2, 114, 80, 55, -51, -21, 121, -101, 63 | -97, 74, -108, 82, -116, -15, -39, 27, -43, -93, -34, 21, 25, -43, -40, -84, 57, 88, -46, 76, 64 | 74, -120, 91, -16, -92, 35, 114, -11, -1, -90, -82, 3, -34, 34, -75, 53, 63, -108, -39, -84, 65 | -78, -126, -117, -43, -106, 29, 1, -117, 63, 29, 93, -101, 81, 94, 104, -87, -10, 115, 66 | 0, 0, 0, 23, 47, 85, 115, 101, 114, 115, 47, 100, 120, 105, 97, 47, 46, 115, 115, 104, 47, 67 | 105, 100, 95, 114, 115, 97, 0, 0, 1, 23, 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 68 | 3, 1, 0, 1, 0, 0, 1, 1, 0, -101, -45, 102, -66, 72, -24, 64, 113, 40, -125, -113, 31, 65, 31, 69 | 75, 113, -64, 67, 71, -70, 62, 108, 93, -77, 60, -49, 89, -109, -24, 106, 36, -116, -25, -42, 70 | 116, 90, -45, 31, 60, 0, 20, -74, -18, 8, 114, -66, 65, 3, 28, -102, 22, -17, 31, -41, 91, 71 | -71, 109, -63, 93, -106, 24, -59, 19, -125, -100, 95, -79, 20, 3, 63, -95, -104, 13, -72, 72 | -106, -8, 40, 35, 21, -102, 55, -86, -32, 112, -106, 98, -6, -36, -109, -12, -76, 110, 33, 73 | 66, -53, 76, -37, -38, 112, -44, -29, -123, 74, 91, 84, -63, 11, 76, 107, 121, -40, 38, -25, 74 | -3, 99, -58, -119, -78, -3, 37, -50, 95, 37, 21, -85, 31, 38, -10, 29, -17, -89, 86, 111, 75 | -123, -29, 103, -16, -119, 118, 43, -62, -9, 85, -42, -59, -74, -71, -19, -51, 38, -112, -91, 76 | -11, 11, -56, -12, -118, -53, 37, 112, 101, 24, 92, 101, 5, 21, -57, 86, 81, -54, -124, -74, 77 | 49, 99, 101, 44, 29, 101, 38, -126, 118, -87, -4, 80, -94, -9, -87, 94, -120, 111, -25, 103, 78 | -125, -17, -45, -118, -39, -55, -14, 7, 40, 49, 75, 113, 103, 93, -78, 107, -8, -84, -20, 75, 79 | 1, -101, -59, 108, -57, -93, 110, -28, -82, 93, 119, 88, -50, 77, 91, 9, 109, 48, -119, 3, 80 | -99, -113, 65, 3, -74, -122, 109, -88, 105, -51, 50, 90, 99, -18, 98, 14, 28, 94, 41, 119, 81 | 68, -51, -116, 17, 0, 0, 0, 31, 47, 85, 115, 101, 114, 115, 47, 100, 120, 105, 97, 47, 46, 82 | 115, 115, 104, 47, 105, 100, 95, 114, 115, 97, 46, 101, 120, 97, 109, 112, 108, 101, 83 | }; 84 | 85 | @Test 86 | public void testReadIdentitiesAnswer() throws Exception { 87 | final ByteArrayInputStream in = new ByteArrayInputStream(BYTES); 88 | final AgentInput agentIn = new AgentInput(in); 89 | final List identities = agentIn.readIdentitiesAnswer(); 90 | 91 | assertThat(identities.size(), equalTo(2)); 92 | assertThat(identities, hasItems( 93 | DefaultIdentity.from(KEY_BLOB1, COMMENT1), 94 | DefaultIdentity.from(KEY_BLOB2, COMMENT2) 95 | )); 96 | } 97 | 98 | @Test 99 | public void testReadSignResponse() throws Exception { 100 | final byte[] headers = new byte[] { 101 | 0, 0, 1, 20, 14, 0, 0, 1, 15, 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 1, 0, 102 | }; 103 | final byte[] expectedSignedBytes = new byte[] { 104 | 105, 106, 27, 119, -107, -25, 105 | -88, 101, 23, -27, -28, 34, -121, 2, -90, -58, -14, -68, 74, -17, 20, 41, 15, 81, 100, 106 | -110, -112, 22, -114, 29, 89, -108, -27, 123, 81, -27, 3, -63, 45, -78, 47, -55, -116, 84, 107 | -15, -110, 66, -71, -45, 104, 20, -42, 127, 39, -53, 89, 63, -120, 40, -71, -2, -20, -75, 108 | -9, -108, -128, -56, 34, 34, -13, -78, -104, -18, -28, -120, -118, -102, 17, 95, 0, 86, 109 | 89, 5, -91, -34, -18, -1, 94, 83, 41, 31, -112, 104, 96, 7, 17, -47, -122, -77, 113, -95, 110 | -109, 55, 46, 120, -118, 117, -27, 43, -8, -83, 124, -107, 96, 56, 35, -70, -121, 27, -82, 111 | 89, 48, 33, 74, 58, 8, 118, -69, 54, -67, 123, -63, -67, -88, 7, -30, 57, -102, 114, -72, 112 | 18, 13, 25, 81, 67, 95, 61, -114, 81, 68, 23, 126, -7, -8, 57, -76, -62, 32, -118, -18, 113 | 28, 64, 17, 115, 125, 106, -62, 12, -94, -15, -56, 46, -80, -55, 109, 62, 4, 122, 42, 114 | -124, -104, -23, 10, 64, 66, -32, -126, -77, 1, -46, 15, -91, 89, -105, 52, -87, -124, 115 | -26, -127, -67, 65, -89, -122, 105, 93, 105, 9, 55, 89, -94, 100, -114, -98, 127, 78, 98, 116 | 41, 67, -104, -50, -31, -102, -113, -14, -36, 54, 9, 4, -76, -102, 110, 125, 96, -27, 82, 117 | 19, 24, -121, 78, 6, 120, -88, -113, -46, -34, -100, -60, 12, -18, 68, 106, -49, -56, 118 | -107, -81, 127, 119 | }; 120 | final byte[] allBytes = new byte[headers.length + expectedSignedBytes.length]; 121 | System.arraycopy(headers, 0, allBytes, 0, headers.length); 122 | System.arraycopy(expectedSignedBytes, 0, allBytes, headers.length, expectedSignedBytes.length); 123 | final ByteArrayInputStream in = new ByteArrayInputStream(allBytes); 124 | 125 | final AgentInput agentIn = new AgentInput(in); 126 | final byte[] signedBytes = agentIn.readSignResponse(); 127 | 128 | assertArrayEquals(signedBytes, expectedSignedBytes); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/AgentOutputTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static org.mockito.Mockito.mock; 40 | import static org.mockito.Mockito.verify; 41 | 42 | import java.io.OutputStream; 43 | import java.security.KeyFactory; 44 | import java.security.interfaces.RSAPublicKey; 45 | import java.security.spec.RSAPublicKeySpec; 46 | import org.junit.Test; 47 | 48 | public class AgentOutputTest extends TestConstants { 49 | 50 | private final OutputStream out = mock(OutputStream.class); 51 | 52 | @Test 53 | public void testRequestIdentities() throws Exception { 54 | final AgentOutput agentOut = new AgentOutput(out); 55 | agentOut.requestIdentities(); 56 | verify(out).write(new byte[]{0, 0, 0, 1, 11}); 57 | } 58 | 59 | @Test 60 | public void testSignRequest() throws Exception { 61 | final AgentOutput agentOut = new AgentOutput(out); 62 | final RSAPublicKeySpec publicKeySpec = TraditionalKeyParser.parsePemPublicKey(PUBLIC_KEY2); 63 | final KeyFactory keyFactory = KeyFactory.getInstance("Rsa"); 64 | final RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec); 65 | final byte[] bytes = new byte[]{1, 2, 3, 4}; 66 | 67 | agentOut.signRequest(publicKey, bytes); 68 | verify(out).write(new byte[] { 69 | 0, 0, 1, 40, 13, 0, 0, 1, 23, 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 70 | 1, 0, 1, 0, 0, 1, 1, 0, -101, -45, 102, -66, 72, -24, 64, 113, 40, -125, -113, 31, 65, 71 | 31, 75, 113, -64, 67, 71, -70, 62, 108, 93, -77, 60, -49, 89, -109, -24, 106, 36, 72 | -116, -25, -42, 116, 90, -45, 31, 60, 0, 20, -74, -18, 8, 114, -66, 65, 3, 28, -102, 73 | 22, -17, 31, -41, 91, -71, 109, -63, 93, -106, 24, -59, 19, -125, -100, 95, -79, 20, 74 | 3, 63, -95, -104, 13, -72, -106, -8, 40, 35, 21, -102, 55, -86, -32, 112, -106, 98, 75 | -6, -36, -109, -12, -76, 110, 33, 66, -53, 76, -37, -38, 112, -44, -29, -123, 74, 91, 76 | 84, -63, 11, 76, 107, 121, -40, 38, -25, -3, 99, -58, -119, -78, -3, 37, -50, 95, 37, 77 | 21, -85, 31, 38, -10, 29, -17, -89, 86, 111, -123, -29, 103, -16, -119, 118, 43, -62, 78 | -9, 85, -42, -59, -74, -71, -19, -51, 38, -112, -91, -11, 11, -56, -12, -118, -53, 79 | 37, 112, 101, 24, 92, 101, 5, 21, -57, 86, 81, -54, -124, -74, 49, 99, 101, 44, 29, 80 | 101, 38, -126, 118, -87, -4, 80, -94, -9, -87, 94, -120, 111, -25, 103, -125, -17, 81 | -45, -118, -39, -55, -14, 7, 40, 49, 75, 113, 103, 93, -78, 107, -8, -84, -20, 75, 82 | 1, -101, -59, 108, -57, -93, 110, -28, -82, 93, 119, 88, -50, 77, 91, 9, 109, 48, -119, 83 | 3, -99, -113, 65, 3, -74, -122, 109, -88, 105, -51, 50, 90, 99, -18, 98, 14, 28, 94, 84 | 41, 119, 68, -51, -116, 17, 0, 0, 0, 4, 1, 2, 3, 4, 0, 0, 0, 0, 85 | }); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/AgentProxiesTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static org.hamcrest.CoreMatchers.equalTo; 40 | import static org.junit.Assert.assertArrayEquals; 41 | import static org.junit.Assert.assertThat; 42 | import static org.mockito.Mockito.mock; 43 | import static org.mockito.Mockito.when; 44 | 45 | import com.google.common.collect.ImmutableList; 46 | import com.google.common.collect.Lists; 47 | import java.util.List; 48 | import org.junit.Test; 49 | 50 | public class AgentProxiesTest extends TestConstants { 51 | 52 | private final AgentOutput out = mock(AgentOutput.class); 53 | private final AgentInput in = mock(AgentInput.class); 54 | 55 | @Test 56 | public void testList() throws Exception { 57 | final List expectedIds = ImmutableList.of( 58 | DefaultIdentity.from(KEY_BLOB1, COMMENT1), 59 | DefaultIdentity.from(KEY_BLOB2, COMMENT2) 60 | ); 61 | when(in.readIdentitiesAnswer()).thenReturn(expectedIds); 62 | 63 | final List identities = Lists.newArrayList(); 64 | try (final AgentProxy proxy = AgentProxies.withCustomInputOutput(in, out)) { 65 | identities.addAll(proxy.list()); 66 | } 67 | 68 | assertThat(identities, equalTo(expectedIds)); 69 | } 70 | 71 | @Test 72 | public void testSign() throws Exception { 73 | final Identity identity = DefaultIdentity.from(KEY_BLOB2, COMMENT2); 74 | when(in.readSignResponse()).thenReturn(SIGN_RESPONSE_DATA); 75 | 76 | final byte[] signed; 77 | try (final AgentProxy proxy = AgentProxies.withCustomInputOutput(in, out)) { 78 | signed = proxy.sign(identity, DATA); 79 | } 80 | 81 | assertArrayEquals(signed, SIGN_RESPONSE_DATA); 82 | } 83 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/ByteIteratorTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static org.hamcrest.CoreMatchers.equalTo; 40 | import static org.junit.Assert.assertFalse; 41 | import static org.junit.Assert.assertThat; 42 | 43 | import java.util.Arrays; 44 | import java.util.Iterator; 45 | import java.util.Random; 46 | import org.junit.Test; 47 | 48 | public class ByteIteratorTest { 49 | 50 | @Test 51 | public void testNext() { 52 | byte[] bytes = new byte[]{0, 0, 0, 3, 1, 2, 3, 0, 0, 0, 4, 0, 0, 1, 2}; 53 | final Iterator iterator = new ByteIterator(bytes); 54 | assertThat(iterator.next(), equalTo(new byte[] {1, 2, 3})); 55 | assertThat(iterator.next(), equalTo(new byte[] {0, 0, 1, 2})); 56 | assertFalse(iterator.hasNext()); 57 | } 58 | 59 | @Test 60 | public void testTurnByteIntoUnsignedInt() { 61 | // Test that signed bytes using 2's complement are turned into unsigned ints 62 | byte[] bytes = new byte[437]; 63 | new Random().nextBytes(bytes); 64 | bytes[0] = 0; 65 | bytes[1] = 0; 66 | bytes[2] = 1; 67 | bytes[3] = -79; 68 | 69 | final Iterator iterator = new ByteIterator(bytes); 70 | assertThat(iterator.next(), equalTo(Arrays.copyOfRange(bytes, 4, bytes.length))); 71 | assertFalse(iterator.hasNext()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/IdentitiesAnswerHeadersTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static com.spotify.sshagentproxy.IdentitiesAnswerHeaders.SSH2_AGENT_IDENTITIES_ANSWER; 40 | import static org.hamcrest.CoreMatchers.equalTo; 41 | import static org.junit.Assert.assertThat; 42 | 43 | import org.junit.Test; 44 | 45 | public class IdentitiesAnswerHeadersTest { 46 | 47 | @Test 48 | public void test() throws Exception { 49 | final byte[] bytes = new byte[]{ 50 | 0, 1, 0, 123, SSH2_AGENT_IDENTITIES_ANSWER, 0, 0, 121, 1 51 | }; 52 | 53 | final IdentitiesAnswerHeaders a = IdentitiesAnswerHeaders.from(bytes); 54 | assertThat(a.getLength(), equalTo((int) Math.pow(16, 4) + 123)); 55 | assertThat(a.getResponseCode(), equalTo(SSH2_AGENT_IDENTITIES_ANSWER)); 56 | assertThat(a.getCount(), equalTo((int) Math.pow(16, 2) * 121 + 1)); 57 | } 58 | 59 | @Test(expected = IllegalArgumentException.class) 60 | public void testAssertion() throws Exception { 61 | IdentitiesAnswerHeaders.from(new byte[] {0, 0, 1}); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/RsaTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static org.hamcrest.CoreMatchers.equalTo; 40 | import static org.junit.Assert.assertThat; 41 | 42 | import java.math.BigInteger; 43 | import java.security.interfaces.RSAPublicKey; 44 | import org.junit.Test; 45 | import org.junit.runner.RunWith; 46 | import org.mockito.runners.MockitoJUnitRunner; 47 | 48 | @RunWith(MockitoJUnitRunner.class) 49 | public class RsaTest extends TestConstants { 50 | 51 | @Test 52 | public void testFrom() throws Exception { 53 | final RSAPublicKey key = Rsa.from(PUBLIC_KEY2.getBytes()); 54 | assertThat(key.getAlgorithm(), equalTo("RSA")); 55 | assertThat(key.getPublicExponent(), equalTo(BigInteger.valueOf(65537))); 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/SignResponseHeadersTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import static com.spotify.sshagentproxy.SignResponseHeaders.SSH2_AGENT_SIGN_RESPONSE; 40 | import static org.hamcrest.CoreMatchers.equalTo; 41 | import static org.junit.Assert.assertThat; 42 | 43 | import org.junit.Test; 44 | 45 | public class SignResponseHeadersTest { 46 | 47 | @Test 48 | public void test() throws Exception { 49 | final byte[] bytes = new byte[]{ 50 | 0, 1, 0, 123, SSH2_AGENT_SIGN_RESPONSE, 0, 0, 121, 1 51 | }; 52 | final SignResponseHeaders a = SignResponseHeaders.from(bytes); 53 | assertThat(a.getLength(), equalTo((int) Math.pow(16, 4) + 123)); 54 | assertThat(a.getResponseCode(), equalTo(SSH2_AGENT_SIGN_RESPONSE)); 55 | assertThat(a.getResponseLength(), equalTo((int) Math.pow(16, 2) * 121 + 1)); 56 | } 57 | 58 | @Test(expected = IllegalArgumentException.class) 59 | public void testAssertion() throws Exception { 60 | SignResponseHeaders.from(new byte[] {0, 0, 1}); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/sshagentproxy/TestConstants.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * ssh-agent-proxy 4 | * -- 5 | * Copyright (C) 2016 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Copyright (c) 2015 Spotify AB. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | package com.spotify.sshagentproxy; 38 | 39 | import com.google.common.base.Throwables; 40 | import java.io.ByteArrayOutputStream; 41 | import java.io.IOException; 42 | 43 | abstract class TestConstants { 44 | 45 | static final byte[] KEY_BLOB1 = new byte[] { 46 | 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 1, 0, -76, -73, 47 | -41, 64, 111, 53, 14, -80, 13, 118, 77, 98, 85, -78, -76, 36, 27, -39, 127, -117, 124, 118, 48 | -42, -37, -29, -88, 75, -63, -43, 68, -43, -91, -88, -57, -26, 102, -128, -83, -87, 60, 75, 49 | 62, -17, -106, 64, 35, -127, -27, -43, -100, -109, -62, -100, -12, 103, 3, -25, 91, -98, 50 | -41, -33, 110, 47, 99, -87, -7, -80, -62, 10, 122, -44, -16, 55, 8, 20, -104, -45, -24, -7, 51 | 21, -23, 9, 106, -97, 85, -69, 8, 92, 122, 44, -49, 95, -25, 45, 100, 75, -112, -123, 119, 52 | 9, 70, 85, -96, -67, -99, -98, -54, -96, 75, 84, -58, -102, 100, 33, 84, 91, -73, -74, -49, 53 | 2, -48, 71, 48, -110, -73, 123, -120, -97, -43, -108, 66, 52, -33, -2, 119, -106, 74, 25, 54 | -26, 0, 53, -92, -104, -60, -95, 9, -28, -38, -32, 40, -43, 48, -15, 115, -101, 94, 5, -69, 55 | 83, 71, 121, 1, 36, -112, 7, -47, -1, -13, 13, 4, -45, -86, 21, -47, -31, 64, -2, 115, 34, 56 | 79, -106, -1, -25, 24, 107, 0, -105, -50, 100, -23, -2, 114, 80, 55, -51, -21, 121, -101, 57 | -97, 74, -108, 82, -116, -15, -39, 27, -43, -93, -34, 21, 25, -43, -40, -84, 57, 88, -46, 58 | 76, 74, -120, 91, -16, -92, 35, 114, -11, -1, -90, -82, 3, -34, 34, -75, 53, 63, -108, -39, 59 | -84, -78, -126, -117, -43, -106, 29, 1, -117, 63, 29, 93, -101, 81, 94, 104, -87, -10, 115, 60 | }; 61 | static final String COMMENT1 = "/Users/dxia/.ssh/id_rsa"; 62 | 63 | static final byte[] KEY_BLOB2 = new byte[] { 64 | 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 1, 0, -101, -45, 65 | 102, -66, 72, -24, 64, 113, 40, -125, -113, 31, 65, 31, 75, 113, -64, 67, 71, -70, 62, 108, 66 | 93, -77, 60, -49, 89, -109, -24, 106, 36, -116, -25, -42, 116, 90, -45, 31, 60, 0, 20, -74, 67 | -18, 8, 114, -66, 65, 3, 28, -102, 22, -17, 31, -41, 91, -71, 109, -63, 93, -106, 24, -59, 68 | 19, -125, -100, 95, -79, 20, 3, 63, -95, -104, 13, -72, -106, -8, 40, 35, 21, -102, 55, -86, 69 | -32, 112, -106, 98, -6, -36, -109, -12, -76, 110, 33, 66, -53, 76, -37, -38, 112, -44, -29, 70 | -123, 74, 91, 84, -63, 11, 76, 107, 121, -40, 38, -25, -3, 99, -58, -119, -78, -3, 37, -50, 71 | 95, 37, 21, -85, 31, 38, -10, 29, -17, -89, 86, 111, -123, -29, 103, -16, -119, 118, 43, 72 | -62, -9, 85, -42, -59, -74, -71, -19, -51, 38, -112, -91, -11, 11, -56, -12, -118, -53, 37, 73 | 112, 101, 24, 92, 101, 5, 21, -57, 86, 81, -54, -124, -74, 49, 99, 101, 44, 29, 101, 38, 74 | -126, 118, -87, -4, 80, -94, -9, -87, 94, -120, 111, -25, 103, -125, -17, -45, -118, -39, 75 | -55, -14, 7, 40, 49, 75, 113, 103, 93, -78, 107, -8, -84, -20, 75, 1, -101, -59, 108, -57, 76 | -93, 110, -28, -82, 93, 119, 88, -50, 77, 91, 9, 109, 48, -119, 3, -99, -113, 65, 3, -74, 77 | -122, 109, -88, 105, -51, 50, 90, 99, -18, 98, 14, 28, 94, 41, 119, 68, -51, -116, 17, 78 | }; 79 | static final String COMMENT2 = "/Users/dxia/.ssh/id_rsa.example"; 80 | static final String PUBLIC_KEY2 = 81 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb02a+SOhAcSiDjx9BH0txwENHuj5sXbM8z1mT6GokjOfWdFrTH" 82 | + "zwAFLbuCHK+QQMcmhbvH9dbuW3BXZYYxRODnF+xFAM/oZgNuJb4KCMVmjeq4HCWYvrck/S0biFCy0zb2nDU44VK" 83 | + "W1TBC0xredgm5/1jxomy/SXOXyUVqx8m9h3vp1ZvheNn8Il2K8L3VdbFtrntzSaQpfULyPSKyyVwZRhcZQUVx1Z" 84 | + "RyoS2MWNlLB1lJoJ2qfxQovepXohv52eD79OK2cnyBygxS3FnXbJr+KzsSwGbxWzHo27krl13WM5NWwltMIkDnY" 85 | + "9BA7aGbahpzTJaY+5iDhxeKXdEzYwR david@example.com"; 86 | 87 | static final byte[] DATA = "Matt Damon: space pirate!".getBytes(); 88 | 89 | private static final byte[] SIGN_RESPONSE_HEADERS = new byte[] { 90 | 0, 0, 1, 20, 14, 0, 0, 1, 15, 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 1, 0, 91 | }; 92 | static final byte[] SIGN_RESPONSE_DATA = new byte[] { 93 | 105, 106, 27, 119, -107, -25, 94 | -88, 101, 23, -27, -28, 34, -121, 2, -90, -58, -14, -68, 74, -17, 20, 41, 15, 81, 100, 95 | -110, -112, 22, -114, 29, 89, -108, -27, 123, 81, -27, 3, -63, 45, -78, 47, -55, -116, 84, 96 | -15, -110, 66, -71, -45, 104, 20, -42, 127, 39, -53, 89, 63, -120, 40, -71, -2, -20, -75, 97 | -9, -108, -128, -56, 34, 34, -13, -78, -104, -18, -28, -120, -118, -102, 17, 95, 0, 86, 98 | 89, 5, -91, -34, -18, -1, 94, 83, 41, 31, -112, 104, 96, 7, 17, -47, -122, -77, 113, -95, 99 | -109, 55, 46, 120, -118, 117, -27, 43, -8, -83, 124, -107, 96, 56, 35, -70, -121, 27, -82, 100 | 89, 48, 33, 74, 58, 8, 118, -69, 54, -67, 123, -63, -67, -88, 7, -30, 57, -102, 114, -72, 101 | 18, 13, 25, 81, 67, 95, 61, -114, 81, 68, 23, 126, -7, -8, 57, -76, -62, 32, -118, -18, 102 | 28, 64, 17, 115, 125, 106, -62, 12, -94, -15, -56, 46, -80, -55, 109, 62, 4, 122, 42, 103 | -124, -104, -23, 10, 64, 66, -32, -126, -77, 1, -46, 15, -91, 89, -105, 52, -87, -124, 104 | -26, -127, -67, 65, -89, -122, 105, 93, 105, 9, 55, 89, -94, 100, -114, -98, 127, 78, 98, 105 | 41, 67, -104, -50, -31, -102, -113, -14, -36, 54, 9, 4, -76, -102, 110, 125, 96, -27, 82, 106 | 19, 24, -121, 78, 6, 120, -88, -113, -46, -34, -100, -60, 12, -18, 68, 106, -49, -56, 107 | -107, -81, 127, 108 | }; 109 | 110 | static { 111 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 112 | try { 113 | out.write(SIGN_RESPONSE_HEADERS); 114 | out.write(SIGN_RESPONSE_DATA); 115 | } catch (IOException e) { 116 | throw Throwables.propagate(e); 117 | } 118 | } 119 | } 120 | --------------------------------------------------------------------------------