-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[INJICERT-695] add Mock mdoc VCI plugin (#89)
* [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
1 parent
e50fa83
commit 3a51e4e
Showing
9 changed files
with
489 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
...gin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
150 changes: 150 additions & 0 deletions
150
mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
|
Oops, something went wrong.