Skip to content

Commit

Permalink
[INJICERT-695] add Mock mdoc VCI plugin (#89)
Browse files Browse the repository at this point in the history
* [INJICERT-695] add Mock mdoc VCI plugin

Signed-off-by: KiruthikaJeyashankar <[email protected]>

* [INJICERT-695] extract constant for VCFormat

Signed-off-by: KiruthikaJeyashankar <[email protected]>

* [INJICERT-695] modify property name of mdoc issuer key certificate
mosip.certify.mock.vciplugin.issuer.key-cert changed to mosip.certify.mock.vciplugin.mdoc.issuer-key-cert

Signed-off-by: KiruthikaJeyashankar <[email protected]>

* [INJICERT-695] refactor - rename class

Signed-off-by: KiruthikaJeyashankar <[email protected]>

---------

Signed-off-by: KiruthikaJeyashankar <[email protected]>
Signed-off-by: Vishwa <[email protected]>
  • Loading branch information
KiruthikaJeyashankar authored Dec 18, 2024
1 parent e50fa83 commit 3a51e4e
Show file tree
Hide file tree
Showing 9 changed files with 489 additions and 2 deletions.
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;

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

0 comments on commit 3a51e4e

Please sign in to comment.