KEYCLOAK-11372 Support for attestation statement verification (#6449)

This commit is contained in:
Takashi Norimatsu 2019-11-08 17:15:28 +09:00 committed by Marek Posolda
parent bf8184221a
commit 4574d37d8d
4 changed files with 65 additions and 26 deletions

View file

@ -16,6 +16,7 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
@ -35,6 +36,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.WebAuthnPolicy;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.JsonConverter;
import com.webauthn4j.data.WebAuthnRegistrationContext; import com.webauthn4j.data.WebAuthnRegistrationContext;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData; import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.statement.AttestationStatement; import com.webauthn4j.data.attestation.statement.AttestationStatement;
@ -46,14 +49,31 @@ import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.util.exception.WebAuthnException; import com.webauthn4j.util.exception.WebAuthnException;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse; import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidator; import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
import com.webauthn4j.validator.attestation.statement.androidkey.AndroidKeyAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.androidsafetynet.AndroidSafetyNetAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.none.NoneAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.packed.PackedAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.tpm.TPMAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.u2f.FIDOU2FAttestationStatementValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.NullCertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.ecdaa.DefaultECDAATrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
public class WebAuthnRegister implements RequiredActionProvider { public class WebAuthnRegister implements RequiredActionProvider {
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class); private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
private KeycloakSession session; private KeycloakSession session;
private CertPathTrustworthinessValidator certPathtrustValidator;
public WebAuthnRegister(KeycloakSession session) { public WebAuthnRegister(KeycloakSession session) {
this.session = session; this.session = session;
this.certPathtrustValidator = new NullCertPathTrustworthinessValidator();
}
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
this.session = session;
this.certPathtrustValidator = certPathtrustValidator;
} }
@Override @Override
@ -129,8 +149,7 @@ public class WebAuthnRegister implements RequiredActionProvider {
try { try {
WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, isUserVerificationRequired); WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, isUserVerificationRequired);
// NOTE: not yet verify Attestation Statement based on certificates WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = createWebAuthnRegistrationContextValidator();
WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator();
WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext); WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
showInfoAfterWebAuthnApiCreate(response); showInfoAfterWebAuthnApiCreate(response);
@ -140,7 +159,6 @@ public class WebAuthnRegister implements RequiredActionProvider {
WebAuthnCredentialModel credential = new WebAuthnCredentialModel(); WebAuthnCredentialModel credential = new WebAuthnCredentialModel();
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData()); credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
credential.setAttestationStatement(response.getAttestationObject().getAttestationStatement());
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount()); credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
this.session.userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credential); this.session.userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credential);
@ -168,6 +186,22 @@ public class WebAuthnRegister implements RequiredActionProvider {
} }
} }
private WebAuthnRegistrationContextValidator createWebAuthnRegistrationContextValidator() {
return new WebAuthnRegistrationContextValidator(
Arrays.asList(
new NoneAttestationStatementValidator(),
new PackedAttestationStatementValidator(),
new TPMAttestationStatementValidator(),
new AndroidKeyAttestationStatementValidator(),
new AndroidSafetyNetAttestationStatementValidator(),
new FIDOU2FAttestationStatementValidator()
), this.certPathtrustValidator,
new DefaultECDAATrustworthinessValidator(),
new DefaultSelfAttestationTrustworthinessValidator(),
new JsonConverter(),
new CborConverter());
}
private String stringifySignatureAlgorithms(List<String> signatureAlgorithmsList) { private String stringifySignatureAlgorithms(List<String> signatureAlgorithmsList) {
if (signatureAlgorithmsList == null || signatureAlgorithmsList.isEmpty()) return ""; if (signatureAlgorithmsList == null || signatureAlgorithmsList.isEmpty()) return "";
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View file

@ -23,13 +23,31 @@ import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.truststore.TruststoreProvider;
import com.webauthn4j.anchor.KeyStoreTrustAnchorsProvider;
import com.webauthn4j.anchor.TrustAnchorsResolverImpl;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.NullCertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.TrustAnchorCertPathTrustworthinessValidator;
public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory { public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory {
public static final String PROVIDER_ID = "webauthn-register"; public static final String PROVIDER_ID = "webauthn-register";
@Override @Override
public RequiredActionProvider create(KeycloakSession session) { public RequiredActionProvider create(KeycloakSession session) {
return new WebAuthnRegister(session); WebAuthnRegister webAuthnRegister = null;
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
webAuthnRegister = new WebAuthnRegister(session, new NullCertPathTrustworthinessValidator());
} else {
KeyStoreTrustAnchorsProvider trustAnchorsProvider = new KeyStoreTrustAnchorsProvider();
trustAnchorsProvider.setKeyStore(truststoreProvider.getTruststore());
TrustAnchorsResolverImpl resolverImpl = new TrustAnchorsResolverImpl(trustAnchorsProvider);
TrustAnchorCertPathTrustworthinessValidator trustValidator = new TrustAnchorCertPathTrustworthinessValidator(resolverImpl);
webAuthnRegister = new WebAuthnRegister(session, trustValidator);
}
return webAuthnRegister;
} }
@Override @Override

View file

@ -38,7 +38,6 @@ import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.data.attestation.authenticator.AAGUID; import com.webauthn4j.data.attestation.authenticator.AAGUID;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData; import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.COSEKey; import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.attestation.statement.AttestationStatement;
import com.webauthn4j.util.exception.WebAuthnException; import com.webauthn4j.util.exception.WebAuthnException;
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse; import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator; import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
@ -47,7 +46,6 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class); private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
private static final String ATTESTATION_STATEMENT = "ATTESTATION_STATEMENT";
private static final String AAGUID = "AAGUID"; private static final String AAGUID = "AAGUID";
private static final String CREDENTIAL_ID = "CREDENTIAL_ID"; private static final String CREDENTIAL_ID = "CREDENTIAL_ID";
private static final String CREDENTIAL_PUBLIC_KEY = "CREDENTIAL_PUBLIC_KEY"; private static final String CREDENTIAL_PUBLIC_KEY = "CREDENTIAL_PUBLIC_KEY";
@ -78,24 +76,17 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
if (!supportsCredentialType(input.getType())) return null; if (!supportsCredentialType(input.getType())) return null;
WebAuthnCredentialModel webAuthnModel = (WebAuthnCredentialModel) input; WebAuthnCredentialModel webAuthnModel = (WebAuthnCredentialModel) input;
MultivaluedHashMap<String, String> credential = new MultivaluedHashMap<>();
credential.add(AAGUID, webAuthnModel.getAttestedCredentialData().getAaguid().toString());
credential.add(CREDENTIAL_ID, Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId()));
credential.add(CREDENTIAL_PUBLIC_KEY, credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey()));
CredentialModel model = new CredentialModel(); CredentialModel model = new CredentialModel();
model.setType(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE); model.setType(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
model.setCreatedDate(Time.currentTimeMillis()); model.setCreatedDate(Time.currentTimeMillis());
MultivaluedHashMap<String, String> credential = new MultivaluedHashMap<>();
credential.add(ATTESTATION_STATEMENT, attestationStatementConverter.convertToDatabaseColumn(webAuthnModel.getAttestationStatement()));
credential.add(AAGUID, webAuthnModel.getAttestedCredentialData().getAaguid().toString());
credential.add(CREDENTIAL_ID, Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId()));
credential.add(CREDENTIAL_PUBLIC_KEY, credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey()));
model.setId(webAuthnModel.getAuthenticatorId()); model.setId(webAuthnModel.getAuthenticatorId());
model.setConfig(credential); model.setConfig(credential);
// authenticator's counter // authenticator's counter
model.setValue(String.valueOf(webAuthnModel.getCount())); model.setValue(String.valueOf(webAuthnModel.getCount()));
@ -113,7 +104,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
// delete webauthn authenticator's credential itself // delete webauthn authenticator's credential itself
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) { for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) {
logger.infov("Delete public key credential. username = {0}, credentialType = {1}", user.getUsername(), credentialType); logger.infov("Delete public key credential. username = {0}, credentialType = {1}", user.getUsername(), credentialType);
dumpCredentialModel(credential); if(logger.isDebugEnabled()) dumpCredentialModel(credential);
session.userCredentialManager().removeStoredCredential(realm, user, credential.getId()); session.userCredentialManager().removeStoredCredential(realm, user, credential.getId());
} }
// delete webauthn authenticator's metadata // delete webauthn authenticator's metadata
@ -194,9 +185,6 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
WebAuthnCredentialModel auth = new WebAuthnCredentialModel(); WebAuthnCredentialModel auth = new WebAuthnCredentialModel();
MultivaluedHashMap<String, String> attributes = credential.getConfig(); MultivaluedHashMap<String, String> attributes = credential.getConfig();
AttestationStatement attrStatement = attestationStatementConverter.convertToEntityAttribute(attributes.getFirst(ATTESTATION_STATEMENT));
auth.setAttestationStatement(attrStatement);
AAGUID aaguid = new AAGUID(attributes.getFirst(AAGUID)); AAGUID aaguid = new AAGUID(attributes.getFirst(AAGUID));
byte[] credentialId = null; byte[] credentialId = null;
@ -225,7 +213,6 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
private void dumpCredentialModel(CredentialModel credential) { private void dumpCredentialModel(CredentialModel credential) {
logger.debugv(" Persisted Credential Info::"); logger.debugv(" Persisted Credential Info::");
MultivaluedHashMap<String, String> attributes = credential.getConfig(); MultivaluedHashMap<String, String> attributes = credential.getConfig();
logger.debugv(" ATTESTATION_STATEMENT = {0}", attributes.getFirst(ATTESTATION_STATEMENT));
logger.debugv(" AAGUID = {0}", attributes.getFirst(AAGUID)); logger.debugv(" AAGUID = {0}", attributes.getFirst(AAGUID));
logger.debugv(" CREDENTIAL_ID = {0}", attributes.getFirst(CREDENTIAL_ID)); logger.debugv(" CREDENTIAL_ID = {0}", attributes.getFirst(CREDENTIAL_ID));
logger.debugv(" CREDENTIAL_PUBLIC_KEY = {0}", attributes.getFirst(CREDENTIAL_PUBLIC_KEY)); logger.debugv(" CREDENTIAL_PUBLIC_KEY = {0}", attributes.getFirst(CREDENTIAL_PUBLIC_KEY));

View file

@ -50,8 +50,8 @@
<select id="attpref" ng-model="realm.webAuthnPolicyAttestationConveyancePreference" class="form-control"> <select id="attpref" ng-model="realm.webAuthnPolicyAttestationConveyancePreference" class="form-control">
<option value="not specified"></option> <option value="not specified"></option>
<option value="none">none</option> <option value="none">none</option>
<!-- not yet supported <option value="indirect">indirect</option> --> <option value="indirect">indirect</option>
<!-- not yet supported <option value="direct">direct</option> --> <option value="direct">direct</option>
</select> </select>
</div> </div>
</div> </div>