-
Notifications
You must be signed in to change notification settings - Fork 33
/
Sep10Service.java
321 lines (287 loc) · 12.7 KB
/
Sep10Service.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
package org.stellar.anchor.sep10;
import org.stellar.anchor.api.exception.SepException;
import org.stellar.anchor.api.exception.SepValidationException;
import org.stellar.anchor.api.sep.sep10.ChallengeRequest;
import org.stellar.anchor.api.sep.sep10.ChallengeResponse;
import org.stellar.anchor.api.sep.sep10.ValidationRequest;
import org.stellar.anchor.api.sep.sep10.ValidationResponse;
import org.stellar.anchor.config.AppConfig;
import org.stellar.anchor.config.Sep10Config;
import org.stellar.anchor.horizon.Horizon;
import org.stellar.anchor.util.Sep1Helper;
import org.stellar.anchor.util.Sep1Helper.TomlContent;
import org.stellar.sdk.*;
import org.stellar.sdk.requests.ErrorResponse;
import org.stellar.sdk.responses.AccountResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.stream.Collectors;
import static org.stellar.anchor.util.Log.*;
/** The Sep-10 protocol service. */
public class Sep10Service {
final AppConfig appConfig;
final Sep10Config sep10Config;
final Horizon horizon;
final JwtService jwtService;
final String serverAccountId;
public Sep10Service(
AppConfig appConfig, Sep10Config sep10Config, Horizon horizon, JwtService jwtService) {
infoF("Creating Sep10Service");
infoConfig("appConfig:", appConfig, AppConfig.class);
this.appConfig = appConfig;
infoConfig("sep10Config:", sep10Config, Sep10Config.class);
this.sep10Config = sep10Config;
this.horizon = horizon;
this.jwtService = jwtService;
this.serverAccountId = KeyPair.fromSecretSeed(sep10Config.getSigningSeed()).getAccountId();
}
public ChallengeResponse createChallenge(ChallengeRequest challengeRequest) throws SepException {
info("Creating challenge");
//
// Validations
//
if (challengeRequest.getHomeDomain() == null) {
infoF("home_domain is not specified. Use {}", sep10Config.getHomeDomain());
challengeRequest.setHomeDomain(sep10Config.getHomeDomain());
} else if (!sep10Config.getHomeDomain().equalsIgnoreCase(challengeRequest.getHomeDomain())) {
infoF("Bad home_domain: {}", challengeRequest.getHomeDomain());
throw new SepValidationException(
String.format("home_domain [%s] is not supported.", challengeRequest.getHomeDomain()));
}
if (sep10Config.isClientAttributionRequired()) {
if (challengeRequest.getClientDomain() == null) {
info("client_domain is required but not provided");
throw new SepValidationException("client_domain is required");
}
List<String> denyList = sep10Config.getClientAttributionDenyList();
if (denyList != null
&& denyList.size() > 0
&& denyList.contains(challengeRequest.getClientDomain())) {
infoF(
"client_domain({}) provided is in the configured deny list",
challengeRequest.getClientDomain());
throw new SepValidationException("unable to process.");
}
List<String> allowList = sep10Config.getClientAttributionAllowList();
if (allowList != null
&& allowList.size() > 0
&& !allowList.contains(challengeRequest.getClientDomain())) {
infoF(
"client_domain provided ({}) is not in configured allow list",
challengeRequest.getClientDomain());
throw new SepValidationException("unable to process");
}
}
// Validate account
try {
KeyPair.fromAccountId(challengeRequest.getAccount());
} catch (Exception ex) {
infoF("client wallet account ({}) is invalid", challengeRequest.getAccount());
throw new SepValidationException("Invalid account.");
}
// Validate memo. It should be 64-bit positive integer if not null.
try {
if (challengeRequest.getMemo() != null) {
int memoInt = Integer.parseInt(challengeRequest.getMemo());
if (memoInt <= 0) {
infoF("Invalid memo value: {}", challengeRequest.getMemo());
throw new SepValidationException(
String.format("Invalid memo value: %s", challengeRequest.getMemo()));
}
}
} catch (NumberFormatException e) {
infoF("invalid memo format: {}. Only MEMO_INT is supported", challengeRequest.getMemo());
throw new SepValidationException(
String.format("Invalid memo format: %s", challengeRequest.getMemo()));
}
//
// Create the challenge
//
try {
String clientSigningKey = null;
if (!Objects.toString(challengeRequest.getClientDomain(), "").isEmpty()) {
infoF("Fetching SIGNING_KEY from client_domain: {}", challengeRequest.getClientDomain());
clientSigningKey = getClientAccountId(challengeRequest.getClientDomain());
debugF("SIGNING_KEY from client_domain fetched: {}", clientSigningKey);
}
KeyPair signer = KeyPair.fromSecretSeed(sep10Config.getSigningSeed());
long now = System.currentTimeMillis() / 1000L;
Transaction txn =
Sep10Challenge.newChallenge(
signer,
new Network(appConfig.getStellarNetworkPassphrase()),
challengeRequest.getAccount(),
challengeRequest.getHomeDomain(),
getDomainFromURI(appConfig.getHostUrl()),
new TimeBounds(now, now + sep10Config.getAuthTimeout()),
(challengeRequest.getClientDomain() == null)
? ""
: challengeRequest.getClientDomain(),
(clientSigningKey == null) ? "" : clientSigningKey);
// Convert the challenge to response
traceB("SEP-10 challenge txn:", txn);
ChallengeResponse challengeResponse =
ChallengeResponse.of(txn.toEnvelopeXdrBase64(), appConfig.getStellarNetworkPassphrase());
traceB("challengeResponse:", challengeResponse);
return challengeResponse;
} catch (URISyntaxException e) {
warnF("Invalid HOST_URL: {}", appConfig.getHostUrl());
throw new SepException(
String.format("Invalid HOST_URL [%s} is used.", appConfig.getHostUrl()));
} catch (InvalidSep10ChallengeException ex) {
warnEx(ex);
throw new SepException("Failed to create the sep-10 challenge.", ex);
}
}
public ValidationResponse validateChallenge(ValidationRequest validationRequest)
throws IOException, InvalidSep10ChallengeException, URISyntaxException,
SepValidationException {
if (validationRequest == null || validationRequest.getTransaction() == null) {
throw new SepValidationException("{transaction} is required.");
}
String clientDomain = validateChallenge(validationRequest.getTransaction());
return ValidationResponse.of(
generateSep10Jwt(validationRequest.getTransaction(), clientDomain));
}
public String validateChallenge(String challengeXdr)
throws IOException, InvalidSep10ChallengeException, URISyntaxException {
debug("Parse challenge string.");
Sep10Challenge.ChallengeTransaction challenge =
Sep10Challenge.readChallengeTransaction(
challengeXdr,
serverAccountId,
new Network(appConfig.getStellarNetworkPassphrase()),
sep10Config.getHomeDomain(),
getDomainFromURI(appConfig.getHostUrl()));
debugF(
"Challenge parsed. account={}, home_domain={}",
shorter(challenge.getClientAccountId()),
challenge.getMatchedHomeDomain());
traceB("challenge:", challenge);
String clientDomain = null;
Operation operation =
Arrays.stream(challenge.getTransaction().getOperations())
.filter(
op ->
(op instanceof ManageDataOperation
&& ((ManageDataOperation) op).getName().equals("client_domain")))
.findFirst()
.orElse(null);
traceB("Challenge operation:", operation);
if (operation != null) {
clientDomain = new String(((ManageDataOperation) operation).getValue());
}
debugF("client_domain: {}", clientDomain);
// Check the client's account
AccountResponse account;
try {
infoF("Checking if {} exists in the Stellar network", challenge.getClientAccountId());
account = horizon.getServer().accounts().account(challenge.getClientAccountId());
traceF("challenge account: {}", account);
} catch (ErrorResponse ex) {
infoF("Account {} does not exist in the Stellar Network");
// account not found
// The client account does not exist, using the client's master key to verify.
Set<String> signers = new HashSet<>();
signers.add(challenge.getClientAccountId());
infoF(
"Verifying challenge threshold. server_account={}, signers={}",
shorter(serverAccountId),
signers.size());
if ((clientDomain != null && challenge.getTransaction().getSignatures().size() != 3)
|| (clientDomain == null && challenge.getTransaction().getSignatures().size() != 2)) {
infoF(
"Invalid SEP 10 challenge exception, there is more than one client signer on challenge transaction for an account that doesn't exist");
throw new InvalidSep10ChallengeException(
"There is more than one client signer on challenge transaction for an account that doesn't exist");
}
debug("Calling Sep10Challenge.verifyChallengeTransactionSigners");
Sep10Challenge.verifyChallengeTransactionSigners(
challengeXdr,
serverAccountId,
new Network(appConfig.getStellarNetworkPassphrase()),
sep10Config.getHomeDomain(),
getDomainFromURI(appConfig.getHostUrl()),
signers);
return clientDomain;
}
// Find the signers of the client account.
Set<Sep10Challenge.Signer> signers =
Arrays.stream(account.getSigners())
.filter(as -> as.getType().equals("ed25519_public_key"))
.map(as -> new Sep10Challenge.Signer(as.getKey(), as.getWeight()))
.collect(Collectors.toSet());
// the signatures must be greater than the medium threshold of the account.
int threshold = account.getThresholds().getMedThreshold();
infoF(
"Verifying challenge threshold. server_account={}, threshold={}, signers={}",
shorter(serverAccountId),
threshold,
signers.size());
Sep10Challenge.verifyChallengeTransactionThreshold(
challengeXdr,
serverAccountId,
new Network(appConfig.getStellarNetworkPassphrase()),
sep10Config.getHomeDomain(),
getDomainFromURI(appConfig.getHostUrl()),
threshold,
signers);
return clientDomain;
}
String getClientAccountId(String clientDomain) throws SepException {
String clientSigningKey = "";
String url = "https://" + clientDomain + "/.well-known/stellar.toml";
try {
debugF("Fetching {}", url);
TomlContent toml = Sep1Helper.readToml(url);
clientSigningKey = toml.getString("SIGNING_KEY");
if (clientSigningKey == null) {
infoF("SIGNING_KEY not present in 'client_domain' TOML.");
throw new SepException("SIGNING_KEY not present in 'client_domain' TOML");
}
// client key validation
debugF("Validating client_domain signing key: {}", clientSigningKey);
KeyPair.fromAccountId(clientSigningKey);
return clientSigningKey;
} catch (IllegalArgumentException | FormatException ex) {
infoF("SIGNING_KEY {} is not a valid Stellar account Id.", clientSigningKey);
throw new SepException(
String.format("SIGNING_KEY %s is not a valid Stellar account Id.", clientSigningKey));
} catch (IOException ioex) {
infoF("Unable to read from {}", url);
throw new SepException(String.format("Unable to read from %s", url), ioex);
}
}
String generateSep10Jwt(String challengeXdr, String clientDomain)
throws InvalidSep10ChallengeException, IOException, URISyntaxException {
infoF("Creating SEP-10 challenge.");
Sep10Challenge.ChallengeTransaction challenge =
Sep10Challenge.readChallengeTransaction(
challengeXdr,
serverAccountId,
new Network(appConfig.getStellarNetworkPassphrase()),
sep10Config.getHomeDomain(),
getDomainFromURI(appConfig.getHostUrl()));
debugB("challenge:", challenge);
long issuedAt = challenge.getTransaction().getTimeBounds().getMinTime();
JwtToken jwtToken =
JwtToken.of(
appConfig.getHostUrl() + "/auth",
challenge.getClientAccountId(),
issuedAt,
issuedAt + sep10Config.getJwtTimeout(),
challenge.getTransaction().hashHex(),
clientDomain);
debugB("jwtToken:", jwtToken);
return jwtService.encode(jwtToken);
}
String getDomainFromURI(String strUri) throws URISyntaxException {
URI uri = new URI(strUri);
if (uri.getPort() < 0) {
return uri.getHost();
}
return uri.getHost() + ":" + uri.getPort();
}
}