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;
|
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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue