Skip to content

Commit 00bbd39

Browse files
author
Henrik Stråth
committed
ClientData class now performs all client data related operations
1 parent 6d88853 commit 00bbd39

File tree

7 files changed

+109
-144
lines changed

7 files changed

+109
-144
lines changed

u2flib-server-core/src/main/java/com/yubico/u2f/ClientDataUtils.java

-101
This file was deleted.

u2flib-server-core/src/main/java/com/yubico/u2f/U2F.java

+15-19
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@
1010
package com.yubico.u2f;
1111

1212
import com.google.common.base.Optional;
13+
import com.yubico.u2f.data.messages.*;
1314
import com.yubico.u2f.data.messages.key.RawAuthenticateResponse;
1415
import com.yubico.u2f.data.messages.key.RawRegisterResponse;
1516
import com.yubico.u2f.data.Device;
1617
import com.yubico.u2f.crypto.ChallengeGenerator;
1718
import com.yubico.u2f.crypto.Crypto;
1819
import com.yubico.u2f.crypto.BouncyCastleCrypto;
1920
import com.yubico.u2f.crypto.RandomChallengeGenerator;
20-
import com.yubico.u2f.data.messages.StartedAuthentication;
21-
import com.yubico.u2f.data.messages.StartedRegistration;
22-
import com.yubico.u2f.data.messages.AuthenticateResponse;
23-
import com.yubico.u2f.data.messages.RegisterResponse;
2421
import org.apache.commons.codec.binary.Base64;
2522

2623
import java.util.Set;
@@ -31,7 +28,7 @@ public class U2F {
3128
private static final ChallengeGenerator challengeGenerator = new RandomChallengeGenerator();
3229
public static final Crypto crypto = new BouncyCastleCrypto();
3330
public static final String AUTHENTICATE_TYP = "navigator.id.getAssertion";
34-
public static final String REGISTER_TYP = "navigator.id.finishEnrollment";
31+
public static final String REGISTER_TYPE = "navigator.id.finishEnrollment";
3532

3633
/**
3734
* Initiates the registration of a device.
@@ -60,14 +57,11 @@ public static Device finishRegistration(StartedRegistration startedRegistration,
6057
}
6158

6259
public static Device finishRegistration(StartedRegistration startedRegistration, RegisterResponse tokenResponse, Set<String> facets) throws U2fException {
63-
byte[] clientData = ClientDataUtils.checkClientData(
64-
tokenResponse.getClientData().toString(),
65-
REGISTER_TYP,
66-
startedRegistration.getChallenge(),
67-
Optional.fromNullable(facets)
68-
);
60+
ClientData clientData = tokenResponse.getClientData();
61+
clientData.checkContent(REGISTER_TYPE, startedRegistration.getChallenge(), Optional.fromNullable(facets));
62+
6963
RawRegisterResponse rawRegisterResponse = RawRegisterResponse.fromBase64(tokenResponse.getRegistrationData());
70-
rawRegisterResponse.checkSignature(startedRegistration.getAppId(), clientData);
64+
rawRegisterResponse.checkSignature(startedRegistration.getAppId(), clientData.getRawClientData());
7165
return rawRegisterResponse.createDevice();
7266
}
7367

@@ -102,14 +96,16 @@ public static int finishAuthentication(StartedAuthentication startedAuthenticati
10296
}
10397

10498
public static int finishAuthentication(StartedAuthentication startedAuthentication, AuthenticateResponse response, Device device, Set<String> facets) throws U2fException {
105-
byte[] clientData = ClientDataUtils.checkClientData(
106-
response.getClientData().toString(),
107-
AUTHENTICATE_TYP,
108-
startedAuthentication.getChallenge(),
109-
Optional.fromNullable(facets)
110-
);
99+
ClientData clientData = response.getClientData();
100+
clientData.checkContent(AUTHENTICATE_TYP, startedAuthentication.getChallenge(), Optional.fromNullable(facets));
101+
102+
111103
RawAuthenticateResponse rawAuthenticateResponse = RawAuthenticateResponse.fromBase64(response.getSignatureData());
112-
rawAuthenticateResponse.checkSignature(startedAuthentication.getAppId(), clientData, device.getPublicKey());
104+
rawAuthenticateResponse.checkSignature(
105+
startedAuthentication.getAppId(),
106+
clientData.getRawClientData(),
107+
device.getPublicKey()
108+
);
113109
rawAuthenticateResponse.checkUserPresence();
114110
return device.checkAndIncrementCounter(rawAuthenticateResponse.getCounter());
115111
}

u2flib-server-core/src/main/java/com/yubico/u2f/data/messages/AuthenticateResponse.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import com.google.common.base.Objects;
1313
import com.google.gson.Gson;
14+
import com.yubico.u2f.U2fException;
1415
import com.yubico.u2f.data.DataObject;
1516

1617
import static com.google.common.base.Preconditions.checkNotNull;
@@ -32,7 +33,7 @@ public AuthenticateResponse(String clientData, String signatureData, String chal
3233
this.challenge = checkNotNull(challenge);
3334
}
3435

35-
public ClientData getClientData() {
36+
public ClientData getClientData() throws U2fException {
3637
return new ClientData(clientData);
3738
}
3839

Original file line numberDiff line numberDiff line change
@@ -1,26 +1,99 @@
11
package com.yubico.u2f.data.messages;
22

3+
import com.google.common.base.Optional;
4+
import com.google.common.collect.ImmutableSet;
5+
import com.google.gson.JsonElement;
6+
import com.google.gson.JsonObject;
7+
import com.google.gson.JsonParser;
38
import com.yubico.u2f.U2fException;
4-
import com.yubico.u2f.ClientDataUtils;
59
import org.apache.commons.codec.binary.Base64;
610

11+
import java.net.URI;
12+
import java.net.URISyntaxException;
13+
import java.util.Set;
14+
715
import static com.google.common.base.Preconditions.checkNotNull;
816

917
public class ClientData {
1018

11-
private final String clientData;
19+
private static final String TYPE_PARAM = "typ";
20+
private static final String CHALLENGE_PARAM = "challenge";
21+
private static final String ORIGIN_PARAM = "origin";
22+
23+
private final String type;
24+
private final String challenge;
25+
private final String origin;
26+
private final byte[] rawClientData;
27+
28+
public byte[] getRawClientData() {
29+
return rawClientData;
30+
}
31+
32+
public ClientData(String rawClientData) throws U2fException {
33+
this(rawClientData.getBytes());
34+
}
35+
36+
public ClientData(byte[] clientData) throws U2fException {
1237

13-
public ClientData(String clientData) {
14-
this.clientData = checkNotNull(clientData);
38+
this.rawClientData = Base64.decodeBase64(clientData);
39+
JsonElement clientDataAsElement = new JsonParser().parse(new String(rawClientData));
40+
if (!clientDataAsElement.isJsonObject()) {
41+
throw new U2fException("ClientData has wrong format");
42+
}
43+
JsonObject jsonObject = clientDataAsElement.getAsJsonObject();
44+
if (!jsonObject.has(TYPE_PARAM)) {
45+
throw new U2fException("Bad clientData: missing 'typ' param");
46+
}
47+
this.type = jsonObject.get(TYPE_PARAM).getAsString();
48+
if (!jsonObject.has(CHALLENGE_PARAM)) {
49+
throw new U2fException("Bad clientData: missing 'challenge' param");
50+
}
51+
this.challenge = jsonObject.get(CHALLENGE_PARAM).getAsString();
52+
this.origin = checkNotNull(jsonObject.get(ORIGIN_PARAM).getAsString());
1553
}
1654

1755
@Override
1856
public String toString() {
19-
return clientData;
57+
return new String(rawClientData);
58+
}
59+
60+
public String getChallenge() {
61+
return challenge;
62+
}
63+
64+
public void checkContent(String type, String challenge, Optional<Set<String>> facets) throws U2fException {
65+
if (!type.equals(this.type)) {
66+
throw new U2fException("Bad clientData: bad type " + this.type);
67+
}
68+
if (!challenge.equals(this.challenge)) {
69+
throw new U2fException("Wrong challenge signed in clientData");
70+
}
71+
if(facets.isPresent()) {
72+
verifyOrigin(origin, canonicalizeOrigins(facets.get()));
73+
}
74+
}
75+
76+
private static void verifyOrigin(String origin, Set<String> allowedOrigins) throws U2fException {
77+
if (!allowedOrigins.contains(canonicalizeOrigin(origin))) {
78+
throw new U2fException(origin +
79+
" is not a recognized home origin for this backend");
80+
}
81+
}
82+
83+
public static Set<String> canonicalizeOrigins(Set<String> origins) {
84+
ImmutableSet.Builder<String> result = ImmutableSet.builder();
85+
for (String origin : origins) {
86+
result.add(canonicalizeOrigin(origin));
87+
}
88+
return result.build();
2089
}
2190

22-
public String getChallenge() throws U2fException {
23-
return ClientDataUtils.toJsonObject(Base64.decodeBase64(clientData.getBytes()))
24-
.get(ClientDataUtils.CHALLENGE_PARAM).getAsString();
91+
public static String canonicalizeOrigin(String url) {
92+
try {
93+
URI uri = new URI(url);
94+
return uri.getScheme() + "://" + uri.getAuthority();
95+
} catch (URISyntaxException e) {
96+
throw new AssertionError("specified bad origin", e);
97+
}
2598
}
2699
}

u2flib-server-core/src/main/java/com/yubico/u2f/data/messages/RegisterResponse.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package com.yubico.u2f.data.messages;
1111

1212
import com.google.common.base.Objects;
13+
import com.yubico.u2f.U2fException;
1314
import com.yubico.u2f.data.DataObject;
1415

1516
import static com.google.common.base.Preconditions.checkNotNull;
@@ -30,7 +31,7 @@ public String getRegistrationData() {
3031
return registrationData;
3132
}
3233

33-
public ClientData getClientData() {
34+
public ClientData getClientData() throws U2fException {
3435
return new ClientData(clientData);
3536
}
3637

u2flib-server-core/src/main/java/com/yubico/u2f/data/messages/key/RawAuthenticateResponse.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ public void checkSignature(String appId, byte[] clientData, byte[] publicKey) th
5757
);
5858
}
5959

60-
public static byte[] packBytesToSign(byte[] appIdHash, byte userPresence,
61-
int counter, byte[] challengeHash) {
60+
public static byte[] packBytesToSign(byte[] appIdHash, byte userPresence, int counter, byte[] challengeHash) {
6261
return ByteSink.create()
6362
.put(appIdHash)
6463
.put(userPresence)

u2flib-server-core/src/test/java/com/yubico/u2f/server/impl/U2FTest.java

+8-12
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@
1212
import com.google.common.collect.ImmutableSet;
1313
import com.yubico.u2f.TestVectors;
1414
import com.yubico.u2f.U2fException;
15-
import com.yubico.u2f.ClientDataUtils;
1615
import com.yubico.u2f.U2F;
1716
import com.yubico.u2f.data.Device;
18-
import com.yubico.u2f.data.messages.StartedAuthentication;
19-
import com.yubico.u2f.data.messages.StartedRegistration;
20-
import com.yubico.u2f.data.messages.AuthenticateResponse;
21-
import com.yubico.u2f.data.messages.RegisterResponse;
17+
import com.yubico.u2f.data.messages.*;
2218
import org.junit.Before;
2319
import org.junit.Test;
2420

@@ -44,13 +40,13 @@ public void setup() throws Exception {
4440

4541
@Test
4642
public void testSanitizeOrigin() {
47-
assertEquals("http://example.com", ClientDataUtils.canonicalizeOrigin("http://example.com"));
48-
assertEquals("http://example.com", ClientDataUtils.canonicalizeOrigin("http://example.com/"));
49-
assertEquals("http://example.com", ClientDataUtils.canonicalizeOrigin("http://example.com/foo"));
50-
assertEquals("http://example.com", ClientDataUtils.canonicalizeOrigin("http://example.com/foo?bar=b"));
51-
assertEquals("http://example.com", ClientDataUtils.canonicalizeOrigin("http://example.com/foo#fragment"));
52-
assertEquals("https://example.com", ClientDataUtils.canonicalizeOrigin("https://example.com"));
53-
assertEquals("https://example.com", ClientDataUtils.canonicalizeOrigin("https://example.com/foo"));
43+
assertEquals("http://example.com", ClientData.canonicalizeOrigin("http://example.com"));
44+
assertEquals("http://example.com", ClientData.canonicalizeOrigin("http://example.com/"));
45+
assertEquals("http://example.com", ClientData.canonicalizeOrigin("http://example.com/foo"));
46+
assertEquals("http://example.com", ClientData.canonicalizeOrigin("http://example.com/foo?bar=b"));
47+
assertEquals("http://example.com", ClientData.canonicalizeOrigin("http://example.com/foo#fragment"));
48+
assertEquals("https://example.com", ClientData.canonicalizeOrigin("https://example.com"));
49+
assertEquals("https://example.com", ClientData.canonicalizeOrigin("https://example.com/foo"));
5450
}
5551

5652
@Test

0 commit comments

Comments
 (0)