KEYCLOAK-11372 Support for attestation statement verification (#6449)
This commit is contained in:
parent
bf8184221a
commit
4574d37d8d
4 changed files with 65 additions and 26 deletions
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue