diff --git a/mock-certify-plugin/pom.xml b/mock-certify-plugin/pom.xml index 187ce98..fc6e540 100644 --- a/mock-certify-plugin/pom.xml +++ b/mock-certify-plugin/pom.xml @@ -129,6 +129,26 @@ slf4j-api 2.0.12 + + org.jetbrains.kotlinx + kotlinx-datetime-jvm + 0.6.0 + + + com.android.identity + identity-credential + 20231002 + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + co.nstant.in + cbor + 0.9 + net.javacrumbs.json-unit json-unit-assertj @@ -377,4 +397,4 @@ - \ No newline at end of file + diff --git a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java new file mode 100644 index 0000000..d5542f9 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java @@ -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 getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { + log.error("not implemented the format {}", vcRequestDto); + throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); + } + + @Override + public VCResult getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map 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 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 mockDataForMsoMdoc(String documentNumber) { + Map 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> keyAliasMap = dbHelper.getKeyAliases(keyAppId, keyRefId, LocalDateTime.now(ZoneOffset.UTC)); + List 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); + } +} diff --git a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java index 16bbc94..e35ed4b 100644 --- a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java +++ b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java @@ -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; @@ -107,7 +108,7 @@ public VCResult getVerifiableCredentialWithLinkedDataProof(VCReque VCResult 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); diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java b/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java new file mode 100644 index 0000000..86e518b --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java @@ -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"; +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java new file mode 100644 index 0000000..2501290 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java @@ -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 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 drivingPrivileges = (Map) 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> generatedIssuerNameSpaces = MdocUtil.generateIssuerNameSpaces(nameSpacedData, new Random(SEED), 16); + Map 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> 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> nameSpaces; + private final DataItem issuerAuth; + + public IssuerSigned(Map> nameSpaces, DataItem issuerAuth) { + this.nameSpaces = nameSpaces; + this.issuerAuth = issuerAuth; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("nameSpaces", CBORConverter.toDataItem(nameSpaces)); + map.put("issuerAuth", issuerAuth); + return map; + } +} + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java new file mode 100644 index 0000000..418c50c --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java @@ -0,0 +1,65 @@ +package io.mosip.certify.util; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.SimpleValue; +import co.nstant.in.cbor.model.UnicodeString; +import co.nstant.in.cbor.model.UnsignedInteger; + +import java.util.List; + +public class CBORConverter { + + public static DataItem toDataItem(Object value) { + if (value instanceof DataItem) { + return (DataItem) value; + } else if (value instanceof String) { + return new UnicodeString((String) value); + } else if (value instanceof Integer) { + return new UnsignedInteger(((Integer) value).longValue()); + } else if (value instanceof Long) { + return new UnsignedInteger((Long) value); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? SimpleValue.TRUE : SimpleValue.FALSE; + } else if (value instanceof java.util.Map) { + Map cborMap = new Map(); + java.util.Map map = (java.util.Map) value; + for (java.util.Map.Entry entry : map.entrySet()) { + cborMap.put(new UnicodeString((String) entry.getKey()), toDataItem(entry.getValue())); + } + return cborMap; + } else if (value instanceof List) { + Array cborArray = new Array(); + List list = (List) value; + for (Object item : list) { + cborArray.add(toDataItem(item)); + } + return cborArray; + } else if (value instanceof Object[]) { + Array cborArray = new Array(); + Object[] array = (Object[]) value; + for (Object item : array) { + cborArray.add(toDataItem(item)); + } + return cborArray; + } else if (value instanceof byte[]) { + try { + List dataItems = CborDecoder.decode((byte[]) value); + if (!dataItems.isEmpty()) { + return dataItems.get(0); + } + } catch (Exception e) { + throw new IllegalArgumentException("Failed to decode ByteArray", e); + } + } else { + throw new IllegalArgumentException("Unsupported value: " + value + " " + value.getClass().getSimpleName()); + } + + return null; + } +} + + + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java new file mode 100644 index 0000000..d7e8a33 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java @@ -0,0 +1,28 @@ +package io.mosip.certify.util; + +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; + +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.util.Base64; + + +public class +JwkToKeyConverter { + + public PublicKey convertToPublicKey(String encodedData) throws Exception { + String jwkJsonString = new String(Base64.getUrlDecoder().decode(encodedData), StandardCharsets.UTF_8); + JWK jwk = JWK.parse(jwkJsonString); + + if (jwk instanceof RSAKey) { + return ((RSAKey) jwk).toPublicKey(); + } else if (jwk instanceof ECKey) { + return ((ECKey) jwk).toPublicKey(); + } + + throw new IllegalArgumentException("Unsupported key type"); + } + +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java new file mode 100644 index 0000000..c95e10f --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java @@ -0,0 +1,7 @@ +package io.mosip.certify.util; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +public record KeyPairAndCertificate(KeyPair keyPair, X509Certificate certificate) { +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java new file mode 100644 index 0000000..398b625 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java @@ -0,0 +1,49 @@ +package io.mosip.certify.util; + + +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +@Slf4j +public class KeyPairAndCertificateExtractor { + public KeyPairAndCertificate extract(String keyCert) { + String[] splitKeyCert = keyCert.split("\\|\\|"); + try { + X509Certificate certificate = convertStringToX509Certificate((splitKeyCert[1])); + return (new KeyPairAndCertificate(getKeyPair(splitKeyCert[0], certificate), certificate)); + } catch (Exception e) { + log.error("Failed to extract key certificate", e); + } + + return null; + } + + private X509Certificate convertStringToX509Certificate(String certString) throws CertificateException { + byte[] certBytes = Base64.getDecoder().decode(certString); + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certBytes)); + } + + private KeyPair getKeyPair(String base64PrivateKey, X509Certificate certificate) throws NoSuchAlgorithmException, InvalidKeySpecException { + byte[] privateKeyBytes = Base64.getDecoder().decode(base64PrivateKey); + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + + PublicKey publicKey = certificate.getPublicKey(); + + return new KeyPair(publicKey, privateKey); + } +} + +