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

[INJICERT-695] add Mock mdoc VCI plugin #89

Merged
merged 4 commits into from
Dec 18, 2024
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
22 changes: 21 additions & 1 deletion mock-certify-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-datetime-jvm</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>com.android.identity</groupId>
<artifactId>identity-credential</artifactId>
<version>20231002</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>co.nstant.in</groupId>
<artifactId>cbor</artifactId>
<version>0.9</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
Expand Down Expand Up @@ -377,4 +397,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.mosip.certify.mock.integration.service;

import foundation.identity.jsonld.JsonLDObject;
import io.mosip.certify.api.dto.VCRequestDto;
import io.mosip.certify.api.dto.VCResult;
import io.mosip.certify.api.exception.VCIExchangeException;
import io.mosip.certify.api.spi.VCIssuancePlugin;
import io.mosip.certify.api.util.ErrorConstants;
import io.mosip.certify.constants.VCFormats;
import io.mosip.certify.core.exception.CertifyException;
import io.mosip.certify.mock.integration.mocks.MdocGenerator;
import io.mosip.esignet.core.dto.OIDCTransaction;
import io.mosip.kernel.core.keymanager.spi.KeyStore;
import io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant;
import io.mosip.kernel.keymanagerservice.entity.KeyAlias;
import io.mosip.kernel.keymanagerservice.helper.KeymanagerDBHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import java.security.Key;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

@ConditionalOnProperty(value = "mosip.certify.integration.vci-plugin", havingValue = "MDocMockVCIssuancePlugin")
@Component
@Slf4j
public class MDocMockVCIssuancePlugin implements VCIssuancePlugin {
private static final String AES_CIPHER_FAILED = "aes_cipher_failed";
private static final String NO_UNIQUE_ALIAS = "no_unique_alias";
private static final String USERINFO_CACHE = "userinfo";

@Autowired
private CacheManager cacheManager;

@Autowired
private KeyStore keyStore;

@Autowired
private KeymanagerDBHelper dbHelper;

@Value("${mosip.certify.cache.security.secretkey.reference-id}")
private String cacheSecretKeyRefId;

@Value("${mosip.certify.cache.security.algorithm-name}")
private String aesECBTransformation;

@Value("${mosip.certify.cache.secure.individual-id}")
private boolean secureIndividualId;

@Value("${mosip.certify.cache.store.individual-id}")
private boolean storeIndividualId;

@Value("${mosip.certify.mock.vciplugin.mdoc.issuer-key-cert:empty}")
private String issuerKeyAndCertificate = null;
vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved

private static final String ACCESS_TOKEN_HASH = "accessTokenHash";

public static final String CERTIFY_SERVICE_APP_ID = "CERTIFY_SERVICE";

@Override
public VCResult<JsonLDObject> getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map<String, Object> identityDetails) throws VCIExchangeException {
log.error("not implemented the format {}", vcRequestDto);
throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED);
}

@Override
public VCResult<String> getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map<String, Object> identityDetails) throws VCIExchangeException {
String accessTokenHash = identityDetails.get(ACCESS_TOKEN_HASH).toString();
String documentNumber;
try {
documentNumber = getIndividualId(getUserInfoTransaction(accessTokenHash));
} catch (Exception e) {
log.error("Error getting documentNumber", e);
throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED);
}

if(vcRequestDto.getFormat().equals(VCFormats.MSO_MDOC)){
VCResult<String> vcResult = new VCResult<>();
String mdocVc;
try {
mdocVc = new MdocGenerator().generate(mockDataForMsoMdoc(documentNumber),holderId, issuerKeyAndCertificate);
} catch (Exception e) {
log.error("Exception on mdoc creation", e);
throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED);
}
vcResult.setCredential(mdocVc);
vcResult.setFormat(VCFormats.MSO_MDOC);
return vcResult;
}
log.error("not implemented the format {}", vcRequestDto);
throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED);
}

private Map<String, Object> mockDataForMsoMdoc(String documentNumber) {
Map<String, Object> data = new HashMap<>();
log.info("Setting up the data for mDoc");
data.put("family_name","Agatha");
data.put("given_name","Joseph");
data.put("birth_date", "1994-11-06");
data.put("issuing_country", "IN");
data.put("document_number", documentNumber);
data.put("driving_privileges",new HashMap<>(){{
put("vehicle_category_code","A");
}});
return data;
}

/**
* TODO: This function getIndividualId is duplicated with Other VCIPlugin class and can be moved to commons
*/
protected String getIndividualId(OIDCTransaction transaction) {
if(!storeIndividualId)
return null;
return secureIndividualId ? decryptIndividualId(transaction.getIndividualId()) : transaction.getIndividualId();
}

private String decryptIndividualId(String encryptedIndividualId) {
try {
Cipher cipher = Cipher.getInstance(aesECBTransformation);
byte[] decodedBytes = Base64.getUrlDecoder().decode(encryptedIndividualId);
cipher.init(Cipher.DECRYPT_MODE, getSecretKeyFromHSM());
return new String(cipher.doFinal(decodedBytes, 0, decodedBytes.length));
} catch(Exception e) {
log.error("Error Cipher Operations of provided secret data.", e);
throw new CertifyException(AES_CIPHER_FAILED);
}
}

private OIDCTransaction getUserInfoTransaction(String accessTokenHash) {
return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class);
}

private Key getSecretKeyFromHSM() {
String keyAlias = getKeyAlias(CERTIFY_SERVICE_APP_ID, cacheSecretKeyRefId);
if (Objects.nonNull(keyAlias)) {
return keyStore.getSymmetricKey(keyAlias);
}
throw new CertifyException(NO_UNIQUE_ALIAS);
}

private String getKeyAlias(String keyAppId, String keyRefId) {
Map<String, List<KeyAlias>> keyAliasMap = dbHelper.getKeyAliases(keyAppId, keyRefId, LocalDateTime.now(ZoneOffset.UTC));
List<KeyAlias> currentKeyAliases = keyAliasMap.get(KeymanagerConstant.CURRENTKEYALIAS);
if (!currentKeyAliases.isEmpty() && currentKeyAliases.size() == 1) {
return currentKeyAliases.getFirst().getAlias();
}
log.error("CurrentKeyAlias is not unique. KeyAlias count: {}", currentKeyAliases.size());
throw new CertifyException(NO_UNIQUE_ALIAS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.mosip.certify.api.exception.VCIExchangeException;
import io.mosip.certify.api.spi.VCIssuancePlugin;
import io.mosip.certify.api.util.ErrorConstants;
import io.mosip.certify.constants.VCFormats;
import io.mosip.certify.core.exception.CertifyException;
import io.mosip.certify.util.UUIDGenerator;
import io.mosip.esignet.core.dto.OIDCTransaction;
Expand Down Expand Up @@ -107,7 +108,7 @@ public VCResult<JsonLDObject> getVerifiableCredentialWithLinkedDataProof(VCReque
VCResult<JsonLDObject> vcResult = new VCResult<>();
vcJsonLdObject = buildJsonLDWithLDProof(identityDetails.get(ACCESS_TOKEN_HASH).toString());
vcResult.setCredential(vcJsonLdObject);
vcResult.setFormat("ldp_vc");
vcResult.setFormat(VCFormats.LDP_VC);
return vcResult;
} catch (Exception e) {
log.error("Failed to build mock VC", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package io.mosip.certify.constants;

public class VCFormats {
public static final String MSO_MDOC = "mso_mdoc";
public static final String LDP_VC = "ldp_vc";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package io.mosip.certify.mock.integration.mocks;

import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import com.android.identity.credential.NameSpacedData;
import com.android.identity.internal.Util;
import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator;
import com.android.identity.mdoc.util.MdocUtil;
import com.android.identity.util.Timestamp;
import io.mosip.certify.util.*;

import java.io.ByteArrayOutputStream;
import java.security.KeyPair;
import java.security.PublicKey;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;

public class MdocGenerator {

public static final String NAMESPACE = "org.iso.18013.5.1";
public static final String DOCTYPE = NAMESPACE + ".mDL";
public static final String DIGEST_ALGORITHM = "SHA-256";
public static final String ECDSA_ALGORITHM = "SHA256withECDSA";
public static final long SEED = 42L;
public static final DateTimeFormatter FULL_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

/**
* @param data - content of the mdoc
* @param holderId - documentNumber of the mDL
* @param issuerKeyAndCertificate - Document signet related details
* @return
* @throws Exception
*
* As of now, only issuer certificate (DS) is available and its used to sign the mdoc. But as per spec,
* DS certificate is signed by the issuing authority’s root CA certificate basically IA creates a certificate chain keeping the
* root = root CA certificate
* leaf = DS certificate
* And only the DS certificate is attached to the credential.
* Root certificate is not available as of now and is a limitation.
*/
public String generate(Map<String, Object> data, String holderId, String issuerKeyAndCertificate) throws Exception {
KeyPairAndCertificateExtractor keyPairAndCertificateExtractor = new KeyPairAndCertificateExtractor();
KeyPairAndCertificate issuerDetails = keyPairAndCertificateExtractor.extract(issuerKeyAndCertificate);

if (issuerDetails.keyPair() == null) {
throw new RuntimeException("Unable to load Crypto details");
}

JwkToKeyConverter jwkToKeyConverter = new JwkToKeyConverter();
PublicKey devicePublicKey = jwkToKeyConverter.convertToPublicKey(holderId.replace("did:jwk:", ""));
KeyPair issuerKeypair = issuerDetails.keyPair();

LocalDate issueDate = LocalDate.now();
String formattedIssueDate = issueDate.format(FULL_DATE_FORMATTER);
LocalDate expiryDate = issueDate.plusYears(5);
String formattedExpiryDate = expiryDate.format(FULL_DATE_FORMATTER);

NameSpacedData.Builder nameSpacedDataBuilder = new NameSpacedData.Builder();
nameSpacedDataBuilder.putEntryString(NAMESPACE, "issue_date", formattedIssueDate);
nameSpacedDataBuilder.putEntryString(NAMESPACE, "expiry_date", formattedExpiryDate);

Map<String, String> drivingPrivileges = (Map<String, String>) data.get("driving_privileges");
drivingPrivileges.put("issue_date", formattedIssueDate);
drivingPrivileges.put("expiry_date", formattedExpiryDate);

data.keySet().forEach(key -> nameSpacedDataBuilder.putEntryString(NAMESPACE, key, data.get(key).toString()));

NameSpacedData nameSpacedData = nameSpacedDataBuilder.build();
Map<String, List<byte[]>> generatedIssuerNameSpaces = MdocUtil.generateIssuerNameSpaces(nameSpacedData, new Random(SEED), 16);
Map<Long, byte[]> calculateDigestsForNameSpace = MdocUtil.calculateDigestsForNameSpace(NAMESPACE, generatedIssuerNameSpaces, DIGEST_ALGORITHM);

MobileSecurityObjectGenerator mobileSecurityObjectGenerator = new MobileSecurityObjectGenerator(DIGEST_ALGORITHM, DOCTYPE, devicePublicKey);
mobileSecurityObjectGenerator.addDigestIdsForNamespace(NAMESPACE, calculateDigestsForNameSpace);

Timestamp currentTimestamp = Timestamp.now();
Timestamp validUntil = Timestamp.ofEpochMilli(addYearsToDate(currentTimestamp.toEpochMilli(), 2));
mobileSecurityObjectGenerator.setValidityInfo(currentTimestamp, currentTimestamp, validUntil, null);

byte[] mso = mobileSecurityObjectGenerator.generate();

DataItem coseSign1Sign = Util.coseSign1Sign(
issuerKeypair.getPrivate(),
ECDSA_ALGORITHM,
Util.cborEncode(Util.cborBuildTaggedByteString(mso)),
null,
Collections.singletonList(issuerDetails.certificate())
);

return construct(generatedIssuerNameSpaces, coseSign1Sign);
}

private String construct(Map<String, List<byte[]>> nameSpaces, DataItem issuerAuth) throws CborException {
MDoc mDoc = new MDoc(DOCTYPE, new IssuerSigned(nameSpaces, issuerAuth));
byte[] cbor = mDoc.toCBOR();
return Base64.getUrlEncoder().encodeToString(cbor);
}

private long addYearsToDate(long dateInEpochMillis, int years) {
Instant instant = Instant.ofEpochMilli(dateInEpochMillis);
Instant futureInstant = instant.plus(years * 365L, ChronoUnit.DAYS);
return futureInstant.toEpochMilli();
}
}


class MDoc {
private final String docType;
private final IssuerSigned issuerSigned;

public MDoc(String docType, IssuerSigned issuerSigned) {
this.docType = docType;
this.issuerSigned = issuerSigned;
}

public byte[] toCBOR() throws CborException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
CborEncoder cborEncoder = new CborEncoder(byteArrayOutputStream);
cborEncoder.encode(
new CborBuilder().addMap()
.put("docType", docType)
.put(CBORConverter.toDataItem("issuerSigned"), CBORConverter.toDataItem(issuerSigned.toMap()))
.end()
.build()
);
return byteArrayOutputStream.toByteArray();
}
}

class IssuerSigned {
private final Map<String, List<byte[]>> nameSpaces;
private final DataItem issuerAuth;

public IssuerSigned(Map<String, List<byte[]>> nameSpaces, DataItem issuerAuth) {
this.nameSpaces = nameSpaces;
this.issuerAuth = issuerAuth;
}

public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("nameSpaces", CBORConverter.toDataItem(nameSpaces));
map.put("issuerAuth", issuerAuth);
return map;
}
}

Loading
Loading