From 82094d6d6772f6a1997fc3388bd1ab463027372d Mon Sep 17 00:00:00 2001 From: Casey Piper Date: Tue, 19 Sep 2017 10:06:45 -0700 Subject: [PATCH 1/2] Begin adding packed format --- .../gaedemo/objects/AttestationStatement.java | 2 + .../objects/PackedAttestationStatement.java | 167 ++++++++++++++++++ .../gaedemo/servlets/BeginGetAssertion.java | 3 +- .../gaedemo/servlets/BeginMakeCredential.java | 3 +- .../servlets/FinishMakeCredential.java | 3 +- 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/webauthn/gaedemo/objects/PackedAttestationStatement.java diff --git a/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatement.java b/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatement.java index 99a72a1..7cf2d08 100644 --- a/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatement.java +++ b/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatement.java @@ -37,6 +37,8 @@ public static AttestationStatement decode(String fmt, DataItem attStmt) { return null; } return stmt; + } else if (fmt.equals("packed")) { + return PackedAttestationStatement.decode(attStmt); } return null; diff --git a/src/main/java/com/google/webauthn/gaedemo/objects/PackedAttestationStatement.java b/src/main/java/com/google/webauthn/gaedemo/objects/PackedAttestationStatement.java new file mode 100644 index 0000000..e22deb8 --- /dev/null +++ b/src/main/java/com/google/webauthn/gaedemo/objects/PackedAttestationStatement.java @@ -0,0 +1,167 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.webauthn.gaedemo.objects; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.UnicodeString; +import com.googlecode.objectify.annotation.Subclass; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Subclass +public class PackedAttestationStatement extends AttestationStatement { + public byte[] sig; + public byte[] attestnCert; + public List caCert; + public Algorithm alg; + public byte[] ecdaaKeyId; + + /** + * @param sig + * @param attestnCert + * @param caCert + */ + public PackedAttestationStatement(byte[] sig, byte[] attestnCert, List caCert, + String alg) { + super(); + this.sig = sig; + this.attestnCert = attestnCert; + this.caCert = caCert; + this.alg = Algorithm.decode(alg); + this.ecdaaKeyId = null; + } + + /** + * @param sig + * @param attestnCert + * @param caCert + */ + public PackedAttestationStatement(byte[] sig, byte[] ecdaaKeyId, String alg) { + super(); + this.sig = sig; + this.ecdaaKeyId = ecdaaKeyId; + this.alg = Algorithm.decode(alg); + this.caCert = null; + this.attestnCert = null; + } + + public PackedAttestationStatement() { + this.sig = null; + this.attestnCert = null; + this.caCert = null; + this.alg = null; + this.ecdaaKeyId = null; + } + + /** + * @param attStmt + * @return Decoded FidoU2fAttestationStatement + */ + public static PackedAttestationStatement decode(DataItem attStmt) { + PackedAttestationStatement result = new PackedAttestationStatement(); + Map given = null; + + if (attStmt instanceof ByteString) { + byte[] temp = ((ByteString) attStmt).getBytes(); + List dataItems = null; + try { + dataItems = CborDecoder.decode(temp); + } catch (Exception e) { + } + given = (Map) dataItems.get(0); + } else { + given = (Map) attStmt; + } + + for (DataItem data : given.getKeys()) { + if (data instanceof UnicodeString) { + if (((UnicodeString) data).getString().equals("x5c")) { + Array array = (Array) given.get(data); + List list = array.getDataItems(); + if (list.size() > 0) { + result.attestnCert = ((ByteString) list.get(0)).getBytes(); + } + result.caCert = new ArrayList(); + for (int i = 1; i < list.size(); i++) { + result.caCert.add(((ByteString) list.get(i)).getBytes()); + } + } else if (((UnicodeString) data).getString().equals("sig")) { + result.sig = ((ByteString) (given.get(data))).getBytes(); + } else if (((UnicodeString) data).getString().equals("alg")) { + result.alg = Algorithm.decode(((UnicodeString) (given.get(data))).getString()); + } else if (((UnicodeString) data).getString().equals("ecdaaKeyId")) { + result.ecdaaKeyId = ((ByteString) (given.get(data))).getBytes(); + } + } + } + return result; + } + + @Override + DataItem encode() throws CborException { + Map result = new Map(); + if (attestnCert != null) { + Array x5c = new Array(); + x5c.add(new ByteString(attestnCert)); + for (byte[] cert : this.caCert) { + x5c.add(new ByteString(cert)); + } + result.put(new UnicodeString("x5c"), x5c); + } + if (ecdaaKeyId != null) { + result.put(new UnicodeString("ecdaaKeyId"), new ByteString(ecdaaKeyId)); + } + result.put(new UnicodeString("sig"), new ByteString(sig)); + result.put(new UnicodeString("alg"), new UnicodeString(alg.toString())); + + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PackedAttestationStatement) { + PackedAttestationStatement other = (PackedAttestationStatement) obj; + if (attestnCert == other.attestnCert || Arrays.equals(attestnCert, other.attestnCert)) { + if (Arrays.equals(sig, other.sig)) { + if (caCert == other.caCert || caCert.size() == other.caCert.size()) { + if (caCert != null) { + for (int i = 0; i < caCert.size(); i++) { + if (!Arrays.equals(caCert.get(i), other.caCert.get(i))) { + return false; + } + } + } + if (other.alg != alg) { + return false; + } + return true; + } + } + } + } + return false; + } + + @Override + public String getName() { + return "Packed Attestation"; + } +} diff --git a/src/main/java/com/google/webauthn/gaedemo/servlets/BeginGetAssertion.java b/src/main/java/com/google/webauthn/gaedemo/servlets/BeginGetAssertion.java index 4b3c4fd..78013ff 100644 --- a/src/main/java/com/google/webauthn/gaedemo/servlets/BeginGetAssertion.java +++ b/src/main/java/com/google/webauthn/gaedemo/servlets/BeginGetAssertion.java @@ -45,7 +45,8 @@ protected void doGet(HttpServletRequest q, HttpServletResponse p) protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String currentUser = userService.getCurrentUser().getUserId(); - String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); + String rpId = request.getHeader("Host").split(":")[0]; + //String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); PublicKeyCredentialRequestOptions assertion = new PublicKeyCredentialRequestOptions(rpId); SessionData session = new SessionData(assertion.challenge, rpId); session.save(currentUser); diff --git a/src/main/java/com/google/webauthn/gaedemo/servlets/BeginMakeCredential.java b/src/main/java/com/google/webauthn/gaedemo/servlets/BeginMakeCredential.java index f5f0e96..2ddbece 100644 --- a/src/main/java/com/google/webauthn/gaedemo/servlets/BeginMakeCredential.java +++ b/src/main/java/com/google/webauthn/gaedemo/servlets/BeginMakeCredential.java @@ -46,7 +46,8 @@ protected void doGet(HttpServletRequest a, HttpServletResponse b) protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user = userService.getCurrentUser(); - String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); + //String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); + String rpId = request.getHeader("Host").split(":")[0]; String rpName = getServletContext().getInitParameter("name"); rpName = (rpName == null ? "" : rpName); diff --git a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java index cd70c1c..3b307e3 100644 --- a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java +++ b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java @@ -87,7 +87,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) PublicKeyCredential cred = new PublicKeyCredential(credentialId, type, BaseEncoding.base64Url().decode(credentialId), attestation); - String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); + //String rpId = (request.isSecure() ? "https://" : "http://") + request.getHeader("Host"); + String rpId = request.getHeader("Host").split(":")[0]; switch (cred.getAttestationType()) { case FIDOU2F: U2fServer.registerCredential(cred, currentUser, session, rpId); From c705beea809498ef8ffe07ba83a30abb814ed33a Mon Sep 17 00:00:00 2001 From: Casey Piper Date: Tue, 19 Sep 2017 11:03:09 -0700 Subject: [PATCH 2/2] Add packed attestation support without verification --- .../objects/AttestationStatementEnum.java | 2 +- .../gaedemo/objects/PublicKeyCredential.java | 2 + .../webauthn/gaedemo/server/PackedServer.java | 208 ++++++++++++++++++ .../gaedemo/servlets/FinishGetAssertion.java | 4 + .../servlets/FinishMakeCredential.java | 4 + 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/google/webauthn/gaedemo/server/PackedServer.java diff --git a/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatementEnum.java b/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatementEnum.java index c255150..1f79837 100644 --- a/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatementEnum.java +++ b/src/main/java/com/google/webauthn/gaedemo/objects/AttestationStatementEnum.java @@ -15,5 +15,5 @@ package com.google.webauthn.gaedemo.objects; public enum AttestationStatementEnum { - FIDOU2F, ANDROIDSAFETYNET; + FIDOU2F, ANDROIDSAFETYNET, PACKED; } diff --git a/src/main/java/com/google/webauthn/gaedemo/objects/PublicKeyCredential.java b/src/main/java/com/google/webauthn/gaedemo/objects/PublicKeyCredential.java index 7037b3e..246ed12 100644 --- a/src/main/java/com/google/webauthn/gaedemo/objects/PublicKeyCredential.java +++ b/src/main/java/com/google/webauthn/gaedemo/objects/PublicKeyCredential.java @@ -69,6 +69,8 @@ public AttestationStatementEnum getAttestationType() { return AttestationStatementEnum.ANDROIDSAFETYNET; } else if (attStmt instanceof FidoU2fAttestationStatement) { return AttestationStatementEnum.FIDOU2F; + } else if (attStmt instanceof PackedAttestationStatement) { + return AttestationStatementEnum.PACKED; } } catch (ClassCastException e) { return null; diff --git a/src/main/java/com/google/webauthn/gaedemo/server/PackedServer.java b/src/main/java/com/google/webauthn/gaedemo/server/PackedServer.java new file mode 100644 index 0000000..a680b6c --- /dev/null +++ b/src/main/java/com/google/webauthn/gaedemo/server/PackedServer.java @@ -0,0 +1,208 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.webauthn.gaedemo.server; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.nio.ByteBuffer; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import javax.servlet.ServletException; + +import com.google.common.primitives.Bytes; +import com.google.gson.Gson; +import com.google.webauthn.gaedemo.crypto.Crypto; +import com.google.webauthn.gaedemo.exceptions.ResponseException; +import com.google.webauthn.gaedemo.exceptions.WebAuthnException; +import com.google.webauthn.gaedemo.objects.AuthenticatorAssertionResponse; +import com.google.webauthn.gaedemo.objects.AuthenticatorAttestationResponse; +import com.google.webauthn.gaedemo.objects.EccKey; +import com.google.webauthn.gaedemo.objects.FidoU2fAttestationStatement; +import com.google.webauthn.gaedemo.objects.PackedAttestationStatement; +import com.google.webauthn.gaedemo.objects.PublicKeyCredential; +import com.google.webauthn.gaedemo.storage.Credential; + + +public class PackedServer extends Server { + + private static final Logger Log = Logger.getLogger(PackedServer.class.getName()); + + /** + * @param cred + * @param currentUser + * @param sessionId + * @throws ServletException + */ + public static void verifyAssertion(PublicKeyCredential cred, String currentUser, String sessionId, + Credential savedCredential) throws ServletException { + AuthenticatorAssertionResponse assertionResponse = + (AuthenticatorAssertionResponse) cred.getResponse(); + + Log.info("-- Verifying signature --"); + if (!(savedCredential.getCredential() + .getResponse() instanceof AuthenticatorAttestationResponse)) { + throw new ServletException("Stored attestation missing"); + } + AuthenticatorAttestationResponse storedAttData = + (AuthenticatorAttestationResponse) savedCredential.getCredential().getResponse(); + + if (!(storedAttData.decodedObject.getAuthenticatorData().getAttData() + .getPublicKey() instanceof EccKey)) { + throw new ServletException("U2f-capable key not provided"); + } + + EccKey publicKey = + (EccKey) storedAttData.decodedObject.getAuthenticatorData().getAttData().getPublicKey(); +// try { +// /* +// * U2F authentication signatures are signed over the concatenation of +// * +// * 32 byte application parameter hash +// * +// * 1 byte user presence +// * +// * 4 byte big-endian representation of the counter +// * +// * 32 byte challenge parameter (ie SHA256 hash of clientData) +// */ +// String clientDataJson = assertionResponse.getClientDataString(); +// byte[] clientDataHash = Crypto.sha256Digest(clientDataJson.getBytes()); +// +// byte[] signedBytes = Bytes.concat( +// storedAttData.getAttestationObject().getAuthenticatorData().getRpIdHash(), +// new byte[] { +// (assertionResponse.getAuthenticatorData().isUP() == true ? (byte) 1 : (byte) 0)}, +// ByteBuffer.allocate(4).putInt(assertionResponse.getAuthenticatorData().getSignCount()) +// .array(), +// clientDataHash); +// if (!Crypto.verifySignature(Crypto.decodePublicKey(publicKey.getX(), publicKey.getY()), +// signedBytes, assertionResponse.getSignature())) { +// throw new ServletException("Signature invalid"); +// } +// } catch (WebAuthnException e) { +// throw new ServletException("Failure while verifying signature"); +// } + +// if (assertionResponse.getAuthenticatorData().getSignCount() <= savedCredential.getSignCount()) { +// throw new ServletException("Sign count invalid"); +// } + + savedCredential.updateSignCount(assertionResponse.getAuthenticatorData().getSignCount()); + + Log.info("Signature verified"); + } + + /** + * @param cred + * @param currentUser + * @param session + * @param origin + * @throws ServletException + */ + public static void registerCredential(PublicKeyCredential cred, String currentUser, + String session, String origin) throws ServletException { + + if (!(cred.getResponse() instanceof AuthenticatorAttestationResponse)) { + throw new ServletException("Invalid response structure"); + } + + AuthenticatorAttestationResponse attResponse = + (AuthenticatorAttestationResponse) cred.getResponse(); + + List savedCreds = Credential.load(currentUser); + for (Credential c : savedCreds) { + if (c.getCredential().id.equals(cred.id)) { + throw new ServletException("Credential already registerd for this user"); + } + } + + try { + verifySessionAndChallenge(attResponse, currentUser, session); + } catch (ResponseException e1) { + throw new ServletException("Unable to verify session and challenge data", e1); + } + + if (!attResponse.getClientData().getOrigin().equals(origin)) { + throw new ServletException("Couldn't verify client data"); + } + + Gson gson = new Gson(); + String clientDataJson = attResponse.getClientDataString(); + System.out.println(clientDataJson); + byte[] clientDataHash = Crypto.sha256Digest(clientDataJson.getBytes()); + + byte[] rpIdHash = Crypto.sha256Digest(origin.getBytes()); + if (!Arrays.equals(attResponse.getAttestationObject().getAuthenticatorData().getRpIdHash(), + rpIdHash)) { + throw new ServletException("RPID hash incorrect"); + } + + if (!(attResponse.decodedObject.getAuthenticatorData().getAttData() + .getPublicKey() instanceof EccKey)) { + throw new ServletException("U2f-capable key not provided"); + } + + PackedAttestationStatement attStmt = + (PackedAttestationStatement) attResponse.decodedObject.getAttestationStatement(); + + EccKey publicKey = + (EccKey) attResponse.decodedObject.getAuthenticatorData().getAttData().getPublicKey(); +// +// try { +// /* +// * U2F registration signatures are signed over the concatenation of +// * +// * 1 byte RFU (0) +// * +// * 32 byte application parameter hash +// * +// * 32 byte challenge parameter +// * +// * key handle +// * +// * 65 byte user public key represented as {0x4, X, Y} +// */ +// byte[] signedBytes = Bytes.concat(new byte[] {0}, rpIdHash, +// clientDataHash, cred.rawId, new byte[] {0x04}, +// publicKey.getX(), publicKey.getY()); +// +// // TODO Make attStmt.attestnCert an X509Certificate right off the bat. +// DataInputStream inputStream = new DataInputStream( +// new ByteArrayInputStream(attStmt.attestnCert)); +// X509Certificate attestationCertificate = (X509Certificate) +// CertificateFactory.getInstance("X.509"). +// generateCertificate(inputStream); +// if (!Crypto.verifySignature(attestationCertificate, signedBytes, +// attStmt.sig)) { +// throw new ServletException("Signature invalid"); +// } +// } catch (CertificateException e) { +// throw new ServletException("Error when parsing attestationCertificate"); +// } catch (WebAuthnException e) { +// throw new ServletException("Failure while verifying signature", e); +// } + + // TODO Check trust anchors + // TODO Check if self-attestation(/is allowed) + // TODO Check X.509 certs + + } + +} diff --git a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishGetAssertion.java b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishGetAssertion.java index 7d476fb..65fec1b 100644 --- a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishGetAssertion.java +++ b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishGetAssertion.java @@ -25,6 +25,7 @@ import com.google.webauthn.gaedemo.objects.AuthenticatorAssertionResponse; import com.google.webauthn.gaedemo.objects.PublicKeyCredential; import com.google.webauthn.gaedemo.server.AndroidSafetyNetServer; +import com.google.webauthn.gaedemo.server.PackedServer; import com.google.webauthn.gaedemo.server.PublicKeyCredentialResponse; import com.google.webauthn.gaedemo.server.Server; import com.google.webauthn.gaedemo.server.U2fServer; @@ -108,6 +109,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) case ANDROIDSAFETYNET: AndroidSafetyNetServer.verifyAssertion(cred, currentUser, session, savedCredential); break; + case PACKED: + PackedServer.verifyAssertion(cred, currentUser, session, savedCredential); + break; } response.setContentType("application/json"); diff --git a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java index 3b307e3..1f80662 100644 --- a/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java +++ b/src/main/java/com/google/webauthn/gaedemo/servlets/FinishMakeCredential.java @@ -25,6 +25,7 @@ import com.google.webauthn.gaedemo.objects.AuthenticatorAttestationResponse; import com.google.webauthn.gaedemo.objects.PublicKeyCredential; import com.google.webauthn.gaedemo.server.AndroidSafetyNetServer; +import com.google.webauthn.gaedemo.server.PackedServer; import com.google.webauthn.gaedemo.server.PublicKeyCredentialResponse; import com.google.webauthn.gaedemo.server.U2fServer; import com.google.webauthn.gaedemo.storage.Credential; @@ -96,6 +97,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) case ANDROIDSAFETYNET: AndroidSafetyNetServer.registerCredential(cred, currentUser, session, rpId); break; + case PACKED: + PackedServer.registerCredential(cred, currentUser, session, rpId); + break; } Credential credential = new Credential(cred);