persist());
-
- })
- .map(newUser -> {
- // make a login cookie
- this.webAuthnSecurity.rememberUser(newUser.userName, ctx);
- return Response.ok().build();
- })
- // handle login failure
- .onFailure().recoverWithItem(x -> {
- // make a proper error response
- return Response.status(Response.Status.BAD_REQUEST).build();
- });
-
- });
- }
-
-}
diff --git a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCertificate.java b/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCertificate.java
deleted file mode 100644
index 2d5ae828c..000000000
--- a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCertificate.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.quarkus.ts.security.webauthn.model;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.ManyToOne;
-
-import io.quarkus.hibernate.reactive.panache.PanacheEntity;
-
-@Entity
-public class WebAuthnCertificate extends PanacheEntity {
- @ManyToOne
- public WebAuthnCredential webAuthnCredential;
-
- /**
- * The list of X509 certificates encoded as base64url.
- */
- public String base64X509Certificate;
-}
diff --git a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCredential.java b/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCredential.java
index 28ac6369a..4e2361e2e 100644
--- a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCredential.java
+++ b/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/model/WebAuthnCredential.java
@@ -1,73 +1,38 @@
package io.quarkus.ts.security.webauthn.model;
-import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import jakarta.persistence.Entity;
-import jakarta.persistence.OneToMany;
+import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
-import jakarta.persistence.Table;
-import jakarta.persistence.UniqueConstraint;
-import io.quarkus.hibernate.reactive.panache.PanacheEntity;
+import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
+import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
+import io.quarkus.security.webauthn.WebAuthnCredentialRecord.RequiredPersistedData;
import io.smallrye.mutiny.Uni;
-import io.vertx.ext.auth.webauthn.Authenticator;
-import io.vertx.ext.auth.webauthn.PublicKeyCredential;
-@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "userName", "credID" }))
@Entity
-public class WebAuthnCredential extends PanacheEntity {
- /**
- * The username linked to this authenticator
- */
- public String userName;
-
- /**
- * The type of key (must be "public-key")
- */
- public String type = "public-key";
-
+public class WebAuthnCredential extends PanacheEntityBase {
/**
* The non user identifiable id for the authenticator
*/
+ @Id
public String credID;
/**
* The public key associated with this authenticator
*/
- public String publicKey;
+ public byte[] publicKey;
+
+ public long publicKeyAlgorithm;
/**
* The signature counter of the authenticator to prevent replay attacks
*/
public long counter;
- public String aaguid;
-
- /**
- * The Authenticator attestation certificates object, a JSON like:
- *
- * {@code
- * {
- * "alg": "string",
- * "x5c": [
- * "base64"
- * ]
- * }
- * }
- */
- /**
- * The algorithm used for the public credential
- */
- public PublicKeyCredential alg;
-
- /**
- * The list of X509 certificates encoded as base64url.
- */
- @OneToMany(mappedBy = "webAuthnCredential")
- public List webAuthnx509Certificates = new ArrayList<>();
-
- public String fmt;
+ public UUID aaguid;
// owning side
@OneToOne
@@ -76,43 +41,29 @@ public class WebAuthnCredential extends PanacheEntity {
public WebAuthnCredential() {
}
- public WebAuthnCredential(Authenticator authenticator, User user) {
- aaguid = authenticator.getAaguid();
- if (authenticator.getAttestationCertificates() != null)
- alg = authenticator.getAttestationCertificates().getAlg();
- counter = authenticator.getCounter();
- credID = authenticator.getCredID();
- fmt = authenticator.getFmt();
- publicKey = authenticator.getPublicKey();
- type = authenticator.getType();
- userName = authenticator.getUserName();
- if (authenticator.getAttestationCertificates() != null
- && authenticator.getAttestationCertificates().getX5c() != null) {
- for (String x509VCertificate : authenticator.getAttestationCertificates().getX5c()) {
- WebAuthnCertificate cert = new WebAuthnCertificate();
- cert.base64X509Certificate = x509VCertificate;
- cert.webAuthnCredential = this;
- this.webAuthnx509Certificates.add(cert);
- }
- }
+ public WebAuthnCredential(WebAuthnCredentialRecord credentialRecord, User user) {
+ RequiredPersistedData requiredPersistedData = credentialRecord.getRequiredPersistedData();
+ aaguid = requiredPersistedData.aaguid();
+ counter = requiredPersistedData.counter();
+ credID = requiredPersistedData.credentialId();
+ publicKey = requiredPersistedData.publicKey();
+ publicKeyAlgorithm = requiredPersistedData.publicKeyAlgorithm();
this.user = user;
user.webAuthnCredential = this;
}
- public static Uni createWebAuthnCredential(Authenticator authenticator, User user) {
- WebAuthnCredential credential = new WebAuthnCredential(authenticator, user);
- credential.persistAndFlush();
- user.webAuthnCredential = credential;
- user.persistAndFlush();
- return Uni.createFrom().item(credential);
+ public WebAuthnCredentialRecord toWebAuthnCredentialRecord() {
+ return WebAuthnCredentialRecord
+ .fromRequiredPersistedData(
+ new RequiredPersistedData(user.userName, credID, aaguid, publicKey, publicKeyAlgorithm, counter));
}
public static Uni> findByUserName(String userName) {
- return list("userName", userName);
+ return list("user.userName", userName);
}
- public static Uni> findByCredID(String credID) {
- return list("credID", credID);
+ public static Uni findByCredentialId(String credID) {
+ return findById(credID);
}
public Uni fetch(T association) {
diff --git a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/security/MyWebAuthnSetup.java b/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/security/MyWebAuthnSetup.java
index bb3eea4e9..d0ba6f07b 100644
--- a/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/security/MyWebAuthnSetup.java
+++ b/security/webauthn/src/main/java/io/quarkus/ts/security/webauthn/security/MyWebAuthnSetup.java
@@ -1,6 +1,5 @@
package io.quarkus.ts.security.webauthn.security;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -9,79 +8,46 @@
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
+import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnUserProvider;
import io.quarkus.ts.security.webauthn.model.User;
import io.quarkus.ts.security.webauthn.model.WebAuthnCredential;
import io.smallrye.mutiny.Uni;
-import io.vertx.ext.auth.webauthn.AttestationCertificates;
-import io.vertx.ext.auth.webauthn.Authenticator;
@ApplicationScoped
public class MyWebAuthnSetup implements WebAuthnUserProvider {
@WithTransaction
@Override
- public Uni> findWebAuthnCredentialsByUserName(String userName) {
+ public Uni> findByUserName(String userName) {
return WebAuthnCredential.findByUserName(userName)
- .flatMap(MyWebAuthnSetup::toAuthenticators);
+ .map(list -> list.stream().map(WebAuthnCredential::toWebAuthnCredentialRecord).toList());
}
@WithTransaction
@Override
- public Uni> findWebAuthnCredentialsByCredID(String credID) {
- return WebAuthnCredential.findByCredID(credID)
- .flatMap(MyWebAuthnSetup::toAuthenticators);
+ public Uni findByCredentialId(String credentialId) {
+ return WebAuthnCredential.findByCredentialId(credentialId)
+ .onItem().ifNull().failWith(() -> new RuntimeException("No such credentials"))
+ .map(WebAuthnCredential::toWebAuthnCredentialRecord);
}
@WithTransaction
@Override
- public Uni updateOrStoreWebAuthnCredentials(Authenticator authenticator) {
- return User.findByUserName(authenticator.getUserName())
- .flatMap(user -> {
- // new user
- if (user == null) {
- User newUser = new User();
- newUser.userName = authenticator.getUserName();
- WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser);
- return credential.persist()
- .flatMap(c -> newUser.persist())
- .onItem().ignore().andContinueWithNull();
- } else {
-
- // existing user
- user.webAuthnCredential.counter = authenticator.getCounter();
- return Uni.createFrom().nullItem();
- }
- });
- }
-
- private static Uni> toAuthenticators(List dbs) {
- // can't call combine/uni on empty list
- if (dbs.isEmpty())
- return Uni.createFrom().item(Collections.emptyList());
- List> ret = new ArrayList<>(dbs.size());
- for (WebAuthnCredential db : dbs) {
- ret.add(toAuthenticator(db));
- }
- return Uni.combine().all().unis(ret).with(f -> (List) f);
+ public Uni store(WebAuthnCredentialRecord credentialRecord) {
+ User newUser = new User();
+ newUser.userName = credentialRecord.getUserName();
+ WebAuthnCredential credential = new WebAuthnCredential(credentialRecord, newUser);
+ return credential.persist()
+ .flatMap(c -> newUser.persist())
+ .onItem().ignore().andContinueWithNull();
}
- private static Uni toAuthenticator(WebAuthnCredential credential) {
- return credential.fetch(credential.webAuthnx509Certificates)
- .map(x5c -> {
- Authenticator ret = new Authenticator();
- ret.setAaguid(credential.aaguid);
- AttestationCertificates attestationCertificates = new AttestationCertificates();
- attestationCertificates.setAlg(credential.alg);
- ret.setAttestationCertificates(attestationCertificates);
- ret.setCounter(credential.counter);
- ret.setCredID(credential.credID);
- ret.setFmt(credential.fmt);
- ret.setPublicKey(credential.publicKey);
- ret.setType(credential.type);
- ret.setUserName(credential.userName);
- return ret;
- });
+ @WithTransaction
+ @Override
+ public Uni update(String credentialId, long counter) {
+ return WebAuthnCredential.findByCredentialId(credentialId)
+ .onItem().ignore().andContinueWithNull();
}
@Override
diff --git a/security/webauthn/src/main/resources/application.properties b/security/webauthn/src/main/resources/application.properties
index b89856e15..075d2efe5 100644
--- a/security/webauthn/src/main/resources/application.properties
+++ b/security/webauthn/src/main/resources/application.properties
@@ -1,3 +1,2 @@
quarkus.hibernate-orm.database.generation=drop-and-create
-
-
+quarkus.webauthn.enable-registration-endpoint=true
diff --git a/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/AbstractWebAuthnTest.java b/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/AbstractWebAuthnTest.java
index abf694231..ec794eeca 100644
--- a/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/AbstractWebAuthnTest.java
+++ b/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/AbstractWebAuthnTest.java
@@ -3,9 +3,13 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
+import java.net.MalformedURLException;
+import java.net.URL;
+
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -23,10 +27,13 @@
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public abstract class AbstractWebAuthnTest {
+ private URL url;
+
protected abstract RestService getApp();
+ private static final String REGISTER_CHALLENGE_OPTIONS_URL = "/q/webauthn/register-options-challenge";
private static final String REGISTER_URL = "/q/webauthn/register";
- private static final String REGISTER_CALLBACK_URL = "/q/webauthn/callback";
+ private static final String LOGIN_CHALLENGE_OPTIONS_URL = "/q/webauthn/login-options-challenge";
private static final String LOGIN_URL = "/q/webauthn/login";
private static final String LOGOUT_URL = "/q/webauthn/logout";
@@ -47,7 +54,11 @@ enum User {
@BeforeAll
public static void setup() {
cookieFilter = new CookieFilter();
+ }
+ @BeforeEach
+ public void setupUrl() throws MalformedURLException {
+ url = new URL(getApp().getURI(Protocol.HTTP).toString());
}
@Test
@@ -89,10 +100,10 @@ public void checkPublicAPI() {
@Test
@Order(5)
public void testRegisterWebAuthnUser() {
- MyWebAuthnHardware myWebAuthnHardware = new MyWebAuthnHardware();
- String challenge = getChallenge(USERNAME, cookieFilter);
+ MyWebAuthnHardware myWebAuthnHardware = new MyWebAuthnHardware(url);
+ String challenge = getRegistrationChallenge(USERNAME, cookieFilter);
JsonObject registrationJson = myWebAuthnHardware.makeRegistrationJson(challenge);
- invokeCallback(registrationJson, cookieFilter);
+ invokeRegisteration(USERNAME, registrationJson, cookieFilter);
verifyLoggedIn(cookieFilter, USERNAME, User.USER);
invokeUserLogout();
@@ -101,23 +112,35 @@ public void testRegisterWebAuthnUser() {
@Test
@Order(6)
public void testRegisterSameUserName() {
- MyWebAuthnHardware myWebAuthnHardware = new MyWebAuthnHardware();
- String challenge = getChallenge(USERNAME, cookieFilter);
+ MyWebAuthnHardware myWebAuthnHardware = new MyWebAuthnHardware(url);
+ String challenge = getRegistrationChallenge(USERNAME, cookieFilter);
JsonObject registrationJson = myWebAuthnHardware.makeRegistrationJson(challenge);
- invokeCallback(registrationJson, cookieFilter);
- verifyLoggedIn(cookieFilter, USERNAME, User.USER);
+ ExtractableResponse response = RestAssured
+ .given()
+ .queryParam("userName", USERNAME)
+ .body(registrationJson.encode())
+ .filter(cookieFilter)
+ .contentType(ContentType.JSON)
+ .log().ifValidationFails()
+ .post(REGISTER_URL)
+ .then()
+ .statusCode(400)
+ .log().ifValidationFails()
+ .cookie("_quarkus_webauthn_challenge", Matchers.is(""))
+ .extract();
+ Assertions.assertNull(response.cookie("quarkus-credential"));
+ verifyLoggedOut(cookieFilter);
}
@Test
@Order(7)
public void testFailLoginWithFakeRegisterUser() {
- invokeUserLogout();
String newUserName = "Kipchoge";
ExtractableResponse response = given().filter(cookieFilter)
.contentType(ContentType.JSON)
.body("{\"name\": \"" + newUserName + "\"}")
- .post(REGISTER_URL)
+ .post(REGISTER_CHALLENGE_OPTIONS_URL)
.then()
.statusCode(is(200)).extract();
@@ -134,26 +157,33 @@ public void testFailLoginWithFakeRegisterUser() {
.statusCode(404);
}
- public static void invokeCallback(JsonObject registration, Filter cookieFilter) {
+ public static void invokeRegisteration(String userName, JsonObject registration, Filter cookieFilter) {
RestAssured
- .given().body(registration.encode()).filter(cookieFilter).contentType(ContentType.JSON).log()
- .ifValidationFails().post(REGISTER_CALLBACK_URL, new Object[0]).then().statusCode(204).log()
- .ifValidationFails().cookie("_quarkus_webauthn_challenge", Matchers.is(""))
- .cookie("_quarkus_webauthn_username", Matchers.is("")).cookie("quarkus-credential", Matchers.notNullValue());
+ .given()
+ .queryParam("userName", userName)
+ .body(registration.encode())
+ .filter(cookieFilter)
+ .contentType(ContentType.JSON)
+ .log().ifValidationFails()
+ .post(REGISTER_URL)
+ .then()
+ .statusCode(204)
+ .log().ifValidationFails()
+ .cookie("_quarkus_webauthn_challenge", Matchers.is(""))
+ .cookie("quarkus-credential", Matchers.notNullValue());
}
- public static String getChallenge(String userName, Filter cookieFilter) {
+ public static String getRegistrationChallenge(String userName, Filter cookieFilter) {
JsonObject registerJson = new JsonObject().put("name", userName);
ExtractableResponse response = given()
.body(registerJson.encode())
.contentType(ContentType.JSON)
.filter(cookieFilter)
- .post(REGISTER_URL)
+ .post(REGISTER_CHALLENGE_OPTIONS_URL)
.then()
.statusCode(200)
- .cookie("_quarkus_webauthn_challenge", Matchers.notNullValue())
- .cookie("_quarkus_webauthn_username", Matchers.notNullValue()).extract();
+ .cookie("_quarkus_webauthn_challenge", Matchers.notNullValue()).extract();
JsonObject responseJson = new JsonObject(response.asString());
String challenge = responseJson.getString("challenge");
Assertions.assertNotNull(challenge);
@@ -233,6 +263,7 @@ public void invokeUserLogout() {
.follow(false)
.get(LOGOUT_URL)
.then()
+ .log().ifValidationFails()
.statusCode(302)
.cookie("quarkus-credential", Matchers.is(""));
}
diff --git a/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/MyWebAuthnHardware.java b/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/MyWebAuthnHardware.java
index bae47f47b..930e8fc47 100644
--- a/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/MyWebAuthnHardware.java
+++ b/security/webauthn/src/test/java/io/quarkus/ts/security/webauthn/MyWebAuthnHardware.java
@@ -2,56 +2,75 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.SignatureException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
+import java.util.Base64.Encoder;
import java.util.Random;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
+import com.webauthn4j.data.attestation.authenticator.AuthenticatorData;
-import io.quarkus.test.bootstrap.Protocol;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.impl.Codec;
public class MyWebAuthnHardware {
+
private KeyPair keyPair;
private String id;
private byte[] credID;
+ private int counter = 1;
+ private URL origin;
- public MyWebAuthnHardware() {
- generateKeyPair();
- }
-
- private void generateKeyPair() {
+ public MyWebAuthnHardware(URL origin) {
+ KeyPairGenerator generator;
try {
- KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
+ generator = KeyPairGenerator.getInstance("EC");
generator.initialize(new ECGenParameterSpec("secp256r1"));
- this.keyPair = generator.generateKeyPair();
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
-
- this.credID = new byte[32];
- new Random().nextBytes(this.credID);
- this.id = Base64.getUrlEncoder().withoutPadding().encodeToString(this.credID);
+ keyPair = generator.generateKeyPair();
+ // This can be a random number, I think
+ Random random = new Random();
+ credID = new byte[32];
+ random.nextBytes(credID);
+ id = Base64.getUrlEncoder().withoutPadding().encodeToString(credID);
+ this.origin = origin;
}
+ /**
+ * Creates a registration JSON object for the given challenge
+ *
+ * @param challenge the server-sent challenge
+ * @return a registration JSON object
+ */
public JsonObject makeRegistrationJson(String challenge) {
- JsonObject clientData = (new JsonObject()).put("type", "webauthn.create").put("challenge", challenge)
- .put("origin", MySqlWebAuthnIT.app.getURI(Protocol.HTTP).getHost()).put("crossOrigin", false);
+ JsonObject clientData = new JsonObject()
+ .put("type", "webauthn.create")
+ .put("challenge", challenge)
+ .put("origin", origin.toString())
+ .put("crossOrigin", false);
String clientDataEncoded = Base64.getUrlEncoder().encodeToString(clientData.encode().getBytes(StandardCharsets.UTF_8));
- byte[] authBytes = this.makeAuthBytes();
+
+ byte[] authBytes = makeAuthBytes(true);
+ /*
+ * {"fmt": "none", "attStmt": {}, "authData": h'DATAAAAA'}
+ */
CBORFactory cborFactory = new CBORFactory();
ByteArrayOutputStream byteWriter = new ByteArrayOutputStream();
-
try {
JsonGenerator generator = cborFactory.createGenerator(byteWriter);
generator.writeStartObject();
@@ -61,62 +80,124 @@ public JsonObject makeRegistrationJson(String challenge) {
generator.writeBinaryField("authData", authBytes);
generator.writeEndObject();
generator.close();
- } catch (IOException var8) {
- throw new RuntimeException(var8);
+ } catch (IOException t) {
+ throw new RuntimeException(t);
}
-
String attestationObjectEncoded = Base64.getUrlEncoder().encodeToString(byteWriter.toByteArray());
- return (new JsonObject())
- .put("id", this.id).put("rawId", this.id).put("response", (new JsonObject())
- .put("attestationObject", attestationObjectEncoded).put("clientDataJSON", clientDataEncoded))
+
+ return new JsonObject()
+ .put("id", id)
+ .put("rawId", id)
+ .put("response", new JsonObject()
+ .put("attestationObject", attestationObjectEncoded)
+ .put("clientDataJSON", clientDataEncoded))
.put("type", "public-key");
}
- private byte[] makeAuthBytes() {
- int counter = 1;
- Buffer buffer = Buffer.buffer();
- String rpDomain = MySqlWebAuthnIT.app.getURI(Protocol.HTTP).getHost();
+ /**
+ * Creates a login JSON object for the given challenge
+ *
+ * @param challenge the server-sent challenge
+ * @return a login JSON object
+ */
+ public JsonObject makeLoginJson(String challenge) {
+ JsonObject clientData = new JsonObject()
+ .put("type", "webauthn.get")
+ .put("challenge", challenge)
+ .put("origin", origin.toString())
+ .put("crossOrigin", false);
+ byte[] clientDataBytes = clientData.encode().getBytes(StandardCharsets.UTF_8);
+ String clientDataEncoded = Base64.getUrlEncoder().encodeToString(clientDataBytes);
+ byte[] authBytes = makeAuthBytes(false);
+ String authenticatorData = Base64.getUrlEncoder().encodeToString(authBytes);
+
+ // sign the authbytes + hash(client data json)
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException var19) {
- throw new RuntimeException(var19);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ byte[] clientDataHash = md.digest(clientDataBytes);
+ byte[] signedBytes = new byte[authBytes.length + clientDataHash.length];
+ System.arraycopy(authBytes, 0, signedBytes, 0, authBytes.length);
+ System.arraycopy(clientDataHash, 0, signedBytes, authBytes.length, clientDataHash.length);
+ byte[] signatureBytes;
+ try {
+ Signature signature = Signature.getInstance("SHA256withECDSA");
+ signature.initSign(this.keyPair.getPrivate());
+ signature.update(signedBytes);
+ signatureBytes = signature.sign();
+ } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {
+ throw new RuntimeException(e);
}
+ String signatureEncoded = Base64.getUrlEncoder().encodeToString(signatureBytes);
+
+ return new JsonObject()
+ .put("id", id)
+ .put("rawId", id)
+ .put("response", new JsonObject()
+ .put("authenticatorData", authenticatorData)
+ .put("clientDataJSON", clientDataEncoded)
+ .put("signature", signatureEncoded))
+ .put("type", "public-key");
+ }
+
+ private byte[] makeAuthBytes(boolean attest) {
+ Buffer buffer = Buffer.buffer();
+ String rpDomain = "localhost";
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
byte[] rpIdHash = md.digest(rpDomain.getBytes(StandardCharsets.UTF_8));
buffer.appendBytes(rpIdHash);
- byte flags = 65;
+
+ byte flags = AuthenticatorData.BIT_AT | AuthenticatorData.BIT_UP | AuthenticatorData.BIT_UV;
buffer.appendByte(flags);
+
long signCounter = counter++;
buffer.appendUnsignedInt(signCounter);
- String aaguidString = "00000000-0000-0000-0000-000000000000";
- String aaguidStringShort = aaguidString.replace("-", "");
- byte[] aaguid = Codec.base16Decode(aaguidStringShort);
- buffer.appendBytes(aaguid);
- buffer.appendUnsignedShort(this.credID.length);
- buffer.appendBytes(this.credID);
- ECPublicKey publicKey = (ECPublicKey) this.keyPair.getPublic();
- Base64.Encoder urlEncoder = Base64.getUrlEncoder();
- String x = urlEncoder.encodeToString(publicKey.getW().getAffineX().toByteArray());
- String y = urlEncoder.encodeToString(publicKey.getW().getAffineY().toByteArray());
- CBORFactory cborFactory = new CBORFactory();
- ByteArrayOutputStream byteWriter = new ByteArrayOutputStream();
- try {
- JsonGenerator generator = cborFactory.createGenerator(byteWriter);
- generator.writeStartObject();
- generator.writeNumberField("1", 2);
- generator.writeNumberField("3", -7);
- generator.writeNumberField("-1", 1);
- generator.writeStringField("-2", x);
- generator.writeStringField("-3", y);
- generator.writeEndObject();
- generator.close();
- } catch (IOException var18) {
- throw new RuntimeException(var18);
+ if (attest) {
+ // Attested Data is present
+ String aaguidString = "00000000-0000-0000-0000-000000000000";
+ String aaguidStringShort = aaguidString.replace("-", "");
+ byte[] aaguid = Codec.base16Decode(aaguidStringShort);
+ buffer.appendBytes(aaguid);
+
+ buffer.appendUnsignedShort(credID.length);
+ buffer.appendBytes(credID);
+
+ ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
+ // NOTE: this used to be Base64 URL, but webauthn4j refuses it and wants Base64. I can't find in the spec where it's specified.
+ Encoder urlEncoder = Base64.getEncoder();
+ String x = urlEncoder.encodeToString(publicKey.getW().getAffineX().toByteArray());
+ String y = urlEncoder.encodeToString(publicKey.getW().getAffineY().toByteArray());
+
+ CBORFactory cborFactory = new CBORFactory();
+ ByteArrayOutputStream byteWriter = new ByteArrayOutputStream();
+ try {
+ JsonGenerator generator = cborFactory.createGenerator(byteWriter);
+ generator.writeStartObject();
+ // see CWK and https://tools.ietf.org/html/rfc8152#section-7.1
+ generator.writeNumberField("1", 2); // kty: "EC"
+ generator.writeNumberField("3", -7); // alg: "ES256"
+ generator.writeNumberField("-1", 1); // crv: "P-256"
+ // https://tools.ietf.org/html/rfc8152#section-13.1.1
+ generator.writeStringField("-2", x); // x, base64url
+ generator.writeStringField("-3", y); // y, base64url
+ generator.writeEndObject();
+ generator.close();
+ } catch (IOException t) {
+ throw new RuntimeException(t);
+ }
+ buffer.appendBytes(byteWriter.toByteArray());
}
- buffer.appendBytes(byteWriter.toByteArray());
return buffer.getBytes();
}