├── .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 | [](https://travis-ci.org/spotify/ssh-agent-proxy)
4 | [](https://codecov.io/gh/spotify/ssh-agent-proxy)
5 | [](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.spotify%22%20ssh-agent-proxy)
6 | [](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 |
--------------------------------------------------------------------------------