Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Apple attestation #5165

Merged
merged 9 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
</dependency>
<!--Test dependencies-->
<dependency>
<groupId>org.testng</groupId>
Expand Down Expand Up @@ -149,6 +165,10 @@
com.google.api.client.http.*;version="${com.google.api.http.clients.osgi.version.range}",
com.google.api.client.json.*;version="${com.google.api.http.clients.osgi.version.range}",
com.google.api.client.util.*;version="${com.google.api.http.clients.osgi.version.range}",
com.fasterxml.jackson.core.*; version="${com.fasterxml.jackson.annotation.version.range}",
com.fasterxml.jackson.databind.*; version="${com.fasterxml.jackson.annotation.version.range}",
com.fasterxml.jackson.annotation.*; version="${com.fasterxml.jackson.annotation.version.range}",
com.fasterxml.jackson.dataformat.cbor.*; version="${com.fasterxml.jackson.annotation.version.range}",
</Import-Package>
<Export-Package>
!org.wso2.carbon.identity.client.attestation.mgt.internal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@


import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;

import java.security.cert.X509Certificate;

/**
* The `ClientAttestationMgtDataHolder` class serves as a data holder for managing
* client attestation-related data and services.
Expand All @@ -32,6 +35,9 @@ public class ClientAttestationMgtDataHolder {
private static ClientAttestationMgtDataHolder instance
= new ClientAttestationMgtDataHolder();

private X509Certificate appleAttestationRootCertificate;
private boolean appleAttestationRevocationCheckEnabled;

private ClientAttestationMgtDataHolder() {

}
Expand All @@ -50,4 +56,24 @@ public void setApplicationManagementService(ApplicationManagementService applica

this.applicationManagementService = applicationManagementService;
}

public X509Certificate getAppleAttestationRootCertificate() {

return appleAttestationRootCertificate;
}

public void setAppleAttestationRootCertificate(X509Certificate appleAttestationRootCertificate) {

this.appleAttestationRootCertificate = appleAttestationRootCertificate;
}

public boolean isAppleAttestationRevocationCheckEnabled() {

return appleAttestationRevocationCheckEnabled;
}

public void setAppleAttestationRevocationCheckEnabled(boolean appleAttestationRevocationCheckEnabled) {

this.appleAttestationRevocationCheckEnabled = appleAttestationRevocationCheckEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.wso2.carbon.identity.client.attestation.mgt.internal;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
Expand All @@ -31,6 +32,19 @@
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.client.attestation.mgt.services.ClientAttestationService;
import org.wso2.carbon.identity.client.attestation.mgt.services.ClientAttestationServiceImpl;
import org.wso2.carbon.identity.core.util.IdentityUtil;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;

import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.APPLE_ATTESTATION_REVOCATION_CHECK_ENABLED;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.APPLE_ATTESTATION_ROOT_CERTIFICATE_PATH;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.CERTIFICATE_EXPIRY_THRESHOLD;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.MILLI_SECOND_IN_DAY;

/**
* OSGi declarative services component which handled registration and un-registration of
Expand All @@ -43,28 +57,102 @@
)
public class ClientAttestationMgtServiceComponent {

private static final Log log = LogFactory.getLog(ClientAttestationMgtServiceComponent.class);
private static final Log LOG = LogFactory.getLog(ClientAttestationMgtServiceComponent.class);

@Activate
protected void activate(ComponentContext context) {

try {
context.getBundleContext().registerService(ClientAttestationService.class.getName(),
new ClientAttestationServiceImpl(), null);
if (log.isDebugEnabled()) {
log.debug("Client Attestation Service Component deployed.");
loadConfigs();

if (LOG.isDebugEnabled()) {
LOG.debug("Client Attestation Service Component deployed.");
}

} catch (Throwable throwable) {
log.error("Error while activating Input Validation Service Component.", throwable);
LOG.error("Error while activating Input Validation Service Component.", throwable);
}
}

/**
* Loads configurations for the Client Attestation Service.
*/
private void loadConfigs() {

// Set the Apple attestation root certificate and revocation check status
ClientAttestationMgtDataHolder.getInstance()
.setAppleAttestationRootCertificate(getAppleAttestationRootCertificate());
ClientAttestationMgtDataHolder.getInstance()
.setAppleAttestationRevocationCheckEnabled(loadAppleAttestationRevocationCheckEnabled());
}

/**
* Loads the status of Apple attestation revocation check from the configuration.
*
* @return True if revocation check is enabled, false otherwise.
*/
private boolean loadAppleAttestationRevocationCheckEnabled() {

return Boolean.parseBoolean(IdentityUtil.getProperty(APPLE_ATTESTATION_REVOCATION_CHECK_ENABLED));
}

/**
* Retrieves the Apple attestation root certificate from the configured file path.
*
* @return The Apple attestation root certificate, or null if not found.
*/
private X509Certificate getAppleAttestationRootCertificate() {

try {
String appleAttestationRootCertificatePath =
IdentityUtil.getProperty(APPLE_ATTESTATION_ROOT_CERTIFICATE_PATH);

if (StringUtils.isNotBlank(appleAttestationRootCertificatePath)) {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
FileInputStream fileInputStream = new FileInputStream(appleAttestationRootCertificatePath);
X509Certificate appleAttestationRootCertificate =
(X509Certificate) certificateFactory.generateCertificate(fileInputStream);

// Warn if the certificate is expiring soon
if (isCertificateExpiringSoon(appleAttestationRootCertificate)) {
LOG.warn("Provided apple attestation root certificate is going to expire soon. " +
"Please add the latest certificate.");
}
return appleAttestationRootCertificate;
} else {
LOG.warn("Apple attestation root certificate path is not configured.");
}
} catch (CertificateException | FileNotFoundException e) {
LOG.warn("Apple attestation root certificate not found.", e);
}
return null;
}

/**
* Checks if the given X.509 certificate is expiring within 30 days.
*
* @param certificate The X.509 certificate to check.
* @return True if the certificate is expiring soon, false otherwise.
*/
private boolean isCertificateExpiringSoon(X509Certificate certificate) {

Date currentDate = new Date();
Date expirationDate = certificate.getNotAfter();

// Calculate the difference in days
long differenceInDays = (expirationDate.getTime() - currentDate.getTime()) / MILLI_SECOND_IN_DAY;

// Check if the certificate is expiring within 3 months.
return differenceInDays <= CERTIFICATE_EXPIRY_THRESHOLD;
}

@Deactivate
protected void deactivate(ComponentContext context) {

if (log.isDebugEnabled()) {
log.debug("Input Validation service component deactivated.");
if (LOG.isDebugEnabled()) {
LOG.debug("Input Validation service component deactivated.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

package org.wso2.carbon.identity.client.attestation.mgt.services;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.nimbusds.jose.JWEObject;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
Expand All @@ -30,10 +33,18 @@
import org.wso2.carbon.identity.client.attestation.mgt.model.ClientAttestationContext;
import org.wso2.carbon.identity.client.attestation.mgt.utils.Constants;
import org.wso2.carbon.identity.client.attestation.mgt.validators.AndroidAttestationValidator;
import org.wso2.carbon.identity.client.attestation.mgt.validators.AppleAttestationValidator;
import org.wso2.carbon.identity.client.attestation.mgt.validators.ClientAttestationValidator;

import java.io.IOException;
import java.text.ParseException;
import java.util.Base64;
import java.util.Map;

import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.APPLE_APP_ATTEST;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.ATT_STMT;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.AUTH_DATA;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.FMT;
import static org.wso2.carbon.identity.client.attestation.mgt.utils.Constants.OAUTH2;

/**
Expand Down Expand Up @@ -111,28 +122,65 @@ public ClientAttestationContext validateAttestation(String attestationObject,
serviceProvider.getClientAttestationMetaData());
androidAttestationValidator.validateAttestation(attestationObject, clientAttestationContext);
return clientAttestationContext;
} else if (isAppleAttestation(attestationObject)) {
clientAttestationContext.setAttestationEnabled(true);
clientAttestationContext.setClientType(Constants.ClientTypes.IOS);

ClientAttestationValidator appleAttestationValidator =
new AppleAttestationValidator(applicationResourceId, tenantDomain,
serviceProvider.getClientAttestationMetaData());
appleAttestationValidator.validateAttestation(attestationObject, clientAttestationContext);
return clientAttestationContext;
} else {
handleInvalidAttestationObject(clientAttestationContext);
return clientAttestationContext;
}
}

private void handleInvalidAttestationObject(ClientAttestationContext clientAttestationContext) {
/**
* Checks if the provided attestation object is an Apple Attestation.
* This method developed using following documentation
* <a href="https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server">
* Validating Apps That Connect to Your Servers
* </a>
* @param attestationObject The attestation object to be checked.
* @return true if it is an Apple Attestation, false otherwise.
*/
private boolean isAppleAttestation(String attestationObject) {

// Create a CBOR factory and an ObjectMapper for CBOR serialization.
Thumimku marked this conversation as resolved.
Show resolved Hide resolved
CBORFactory factory = new CBORFactory();
ObjectMapper cborMapper = new ObjectMapper(factory);

if (LOG.isDebugEnabled()) {
LOG.debug("Requested attestation object is not in valid format.");
try {
// Decode the Base64-encoded attestation object.
byte[] cborData = Base64.getDecoder().decode(attestationObject);
// Parse the CBOR data into a Map.
Map<String, Object> cborMap = cborMapper.readValue(cborData, new TypeReference<Map<String, Object>>() { });

// Check for the presence of specific keys and the "fmt" value.
if (cborMap.containsKey(AUTH_DATA)
&& cborMap.containsKey(ATT_STMT)
&& cborMap.containsKey(FMT)
&& StringUtils.equals(cborMap.get(FMT).toString(), APPLE_APP_ATTEST)) {
return true;
}
} catch (IOException | IllegalArgumentException e) {
// An exception occurred, indicating it's not an Apple Attestation.
return false;
}
setErrorToContext("Requested attestation object is not in valid format.",
clientAttestationContext);
// It didn't meet the criteria for an Apple Attestation.
return false;
}

private void handleClientAttestationException
(ClientAttestationMgtException e, ClientAttestationContext clientAttestationContext) {

private void handleInvalidAttestationObject(ClientAttestationContext clientAttestationContext) {

if (LOG.isDebugEnabled()) {
LOG.debug("Error while evaluating client attestation.", e);
LOG.debug("Requested attestation object is not in valid format.");
}
setErrorToContext(e.getMessage(), clientAttestationContext);
setErrorToContext("Requested attestation object is not in valid format.",
clientAttestationContext);
}

private void setErrorToContext(String message, ClientAttestationContext clientAttestationContext) {
Expand All @@ -144,6 +192,15 @@ private void setErrorToContext(String message, ClientAttestationContext clientAt
clientAttestationContext.setValidationFailureMessage(message);
}

/**
* Checks if the provided attestation object is an Android Attestation request.
* This method developed using following documentation
* <a href="https://developer.android.com/google/play/integrity/classic#token-format">
* Google Play Integrity Token Format
* </a>
* @param attestationObject The attestation object to be checked, typically in JWE format.
* @return true if it is an Android Attestation request, false otherwise.
*/
private boolean isAndroidAttestation(String attestationObject) {

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum ClientTypes {
// Represents an Android client application.
ANDROID,
// Represents an iOS client application.
iOS
IOS
}
public static final String ATTESTATION_HEADER = "x-client-attestation";
public static final String CLIENT_ATTESTATION_CONTEXT = "client.attestation.context";
Expand All @@ -41,8 +41,27 @@ public enum ClientTypes {
public static final String RESPONSE_MODE = "response_mode";
public static final String OAUTH2 = "oauth2";
public static final String UTC = "UTC";
public static final String PLAY_RECOGNIZED = "PLAY_RECOGNIZED";
public static final String CLIENT_ATTESTATION_ALLOWED_WINDOW_IN_MILL_SECOND
= "ClientAttestation.AllowedWindowMillis";
public static final String APPLE_ATTESTATION_ROOT_CERTIFICATE_PATH
= "ClientAttestation.AppleAttestationRootCertificatePath";
public static final String APPLE_ATTESTATION_REVOCATION_CHECK_ENABLED
= "ClientAttestation.AppleAttestationRevocationCheckEnabled";

// Constants related to Android Attestation
public static final String PLAY_RECOGNIZED = "PLAY_RECOGNIZED";

// Constants related to Apple Attestation
public static final String AUTH_DATA = "authData";
public static final String ATT_STMT = "attStmt";
public static final String FMT = "fmt";
public static final String X5C = "x5c";
public static final String APPLE_APP_ATTEST = "apple-appattest";
public static final String SHA_256 = "SHA-256";
public static final String X_509_CERTIFICATE_TYPE = "X.509";
public static final String PKIX = "PKIX";
public static final int CERTIFICATE_EXPIRY_THRESHOLD = 90;
// Milli seconds in days 24 * 60 * 60 * 1000
public static final int MILLI_SECOND_IN_DAY = 86400000;

}
Loading
Loading