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;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
@ -35,6 +36,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
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.attestation.authenticator.AttestedCredentialData;
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.validator.WebAuthnRegistrationContextValidationResponse;
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 {
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
private KeycloakSession session;
private CertPathTrustworthinessValidator certPathtrustValidator;
public WebAuthnRegister(KeycloakSession session) {
this.session = session;
this.certPathtrustValidator = new NullCertPathTrustworthinessValidator();
}
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
this.session = session;
this.certPathtrustValidator = certPathtrustValidator;
}
@Override
@ -129,8 +149,7 @@ public class WebAuthnRegister implements RequiredActionProvider {
try {
WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, isUserVerificationRequired);
// NOTE: not yet verify Attestation Statement based on certificates
WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator();
WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = createWebAuthnRegistrationContextValidator();
WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
showInfoAfterWebAuthnApiCreate(response);
@ -140,7 +159,6 @@ public class WebAuthnRegister implements RequiredActionProvider {
WebAuthnCredentialModel credential = new WebAuthnCredentialModel();
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
credential.setAttestationStatement(response.getAttestationObject().getAttestationStatement());
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
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) {
if (signatureAlgorithmsList == null || signatureAlgorithmsList.isEmpty()) return "";
StringBuilder sb = new StringBuilder();

View file

@ -23,13 +23,31 @@ import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
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 static final String PROVIDER_ID = "webauthn-register";
@Override
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

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.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.attestation.statement.AttestationStatement;
import com.webauthn4j.util.exception.WebAuthnException;
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
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 String ATTESTATION_STATEMENT = "ATTESTATION_STATEMENT";
private static final String AAGUID = "AAGUID";
private static final String CREDENTIAL_ID = "CREDENTIAL_ID";
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;
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();
model.setType(WebAuthnCredentialModel.WEBAUTHN_CREDENTIAL_TYPE);
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.setConfig(credential);
// authenticator's counter
model.setValue(String.valueOf(webAuthnModel.getCount()));
@ -113,7 +104,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
// delete webauthn authenticator's credential itself
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, 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());
}
// delete webauthn authenticator's metadata
@ -194,9 +185,6 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
WebAuthnCredentialModel auth = new WebAuthnCredentialModel();
MultivaluedHashMap<String, String> attributes = credential.getConfig();
AttestationStatement attrStatement = attestationStatementConverter.convertToEntityAttribute(attributes.getFirst(ATTESTATION_STATEMENT));
auth.setAttestationStatement(attrStatement);
AAGUID aaguid = new AAGUID(attributes.getFirst(AAGUID));
byte[] credentialId = null;
@ -225,7 +213,6 @@ public class WebAuthnCredentialProvider implements CredentialProvider, Credentia
private void dumpCredentialModel(CredentialModel credential) {
logger.debugv(" Persisted Credential Info::");
MultivaluedHashMap<String, String> attributes = credential.getConfig();
logger.debugv(" ATTESTATION_STATEMENT = {0}", attributes.getFirst(ATTESTATION_STATEMENT));
logger.debugv(" AAGUID = {0}", attributes.getFirst(AAGUID));
logger.debugv(" CREDENTIAL_ID = {0}", attributes.getFirst(CREDENTIAL_ID));
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">
<option value="not specified"></option>
<option value="none">none</option>
<!-- not yet supported <option value="indirect">indirect</option> -->
<!-- not yet supported <option value="direct">direct</option> -->
<option value="indirect">indirect</option>
<option value="direct">direct</option>
</select>
</div>
</div>