diff --git a/.gitignore b/.gitignore index f271844..9a6b505 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ target/ .setting/ .mvn/ .project/ +*.DS_Store diff --git a/mock-certify-plugin/pom.xml b/mock-certify-plugin/pom.xml index 2cb66e4..8bcd249 100644 --- a/mock-certify-plugin/pom.xml +++ b/mock-certify-plugin/pom.xml @@ -53,23 +53,27 @@ 0.8.11 3.6.3 1.3.0-beta.1 + 2.0.0 - + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + 2.10.1 + org.projectlombok lombok 1.18.30 provided - io.mosip.certify certify-core 0.10.0-SNAPSHOT provided - io.mosip.esignet esignet-core @@ -131,6 +135,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 + @@ -156,6 +180,10 @@ danubetech-maven-public https://repo.danubetech.com/repository/maven-public/ + + google + https://maven.google.com/ + @@ -356,6 +384,70 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + src/test/java + target/generated-test-sources/test-annotations + + + + + + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + \ No newline at end of file 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 22b7aef..7163e79 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,7 +24,6 @@ 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.core.dto.ParsedAccessToken; import io.mosip.certify.core.exception.CertifyException; import io.mosip.esignet.core.dto.OIDCTransaction; import org.springframework.beans.factory.annotation.Autowired; @@ -93,7 +92,13 @@ public class MockVCIssuancePlugin implements VCIssuancePlugin { @Value("#{${mosip.certify.mock.vciplugin.vc-credential-contexts:{'https://www.w3.org/2018/credentials/v1','https://schema.org/'}}}") private List vcCredentialContexts; - private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; + @Value("${mosip.certify.mock.vciplugin.issuer.key-cert:empty}") + private String issuerKeyAndCertificate = null; + + @Value("${mosip.certify.mock.vciplugin.ca.key-cert:empty}") + private String caKeyAndCertificate = null; + + private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; public static final String UTC_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; @@ -203,7 +208,7 @@ private String decryptIndividualId(String encryptedIndividualId) { 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)); + 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); @@ -235,10 +240,52 @@ private static String getUTCDateTime() { @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("mso_mdoc")){ + VCResult vcResult = new VCResult<>(); + String mdocVc = null; + try { + mdocVc = new io.mosip.certify.mock.integration.mocks.MdocGenerator().generate(mockDataForMsoMdoc(documentNumber),holderId, caKeyAndCertificate,issuerKeyAndCertificate); + } catch (Exception e) { + log.error("Exception on mdoc creation", e); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + vcResult.setCredential(mdocVc); + vcResult.setFormat("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"); + //TODO: Populate datetime in real time + data.put("issue_date", "2024-01-12"); + data.put("expiry_date", "2025-01-12"); + data.put("family_name","Agatha"); + data.put("given_name","Joseph"); + data.put("birth_date", "1994-11-06"); + data.put("issuing_country", "Island"); + data.put("document_number", documentNumber); + data.put("driving_privileges",new HashMap<>(){{ + put("vehicle_category_code","A"); + put("issue_date","2023-01-01"); + put("expiry_date","2043-01-01"); + }}); + return data; + } + public OIDCTransaction getUserInfoTransaction(String accessTokenHash) { - return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class); + return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class); } -} +} \ No newline at end of file diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.kt b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.kt new file mode 100644 index 0000000..35a3c5f --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.kt @@ -0,0 +1,119 @@ +package io.mosip.certify.mock.integration.mocks + +import co.nstant.`in`.cbor.CborBuilder +import co.nstant.`in`.cbor.CborEncoder +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 io.mosip.certify.util.IssuerKeyPairAndCertificate +import java.util.* + + +class MdocGenerator { + companion object { + const val NAMESPACE: String = "org.iso.18013.5.1" + const val DOCTYPE: String = "$NAMESPACE.mDL" + const val DIGEST_ALGORITHM = "SHA-256" + const val ECDSA_ALGORITHM = "SHA256withECDSA" + const val SEED = 42L + } + + fun generate( + data: MutableMap, + holderId: String, + caKeyAndCertificate: String, + issuerKeyAndCertificate: String + ): String? { + val issuerKeyPairAndCertificate: IssuerKeyPairAndCertificate? = readKeypairAndCertificates( + caKeyAndCertificate,issuerKeyAndCertificate + ) + if(issuerKeyPairAndCertificate == null) { + throw RuntimeException("Unable to load Crypto details") + } + val devicePublicKey = JwkToKeyConverter().convertToPublicKey(holderId.replace("did:jwk:", "")) + val issuerKeypair = issuerKeyPairAndCertificate.issuerKeypair() + + val nameSpacedDataBuilder: NameSpacedData.Builder = NameSpacedData.Builder() + data.keys.forEach { key -> + nameSpacedDataBuilder.putEntryString(NAMESPACE, key, data[key].toString()) + } + val nameSpacedData: NameSpacedData = + nameSpacedDataBuilder + .build() + val generatedIssuerNameSpaces: MutableMap> = + MdocUtil.generateIssuerNameSpaces(nameSpacedData, Random(SEED), 16) + val calculateDigestsForNameSpace = + MdocUtil.calculateDigestsForNameSpace(NAMESPACE, generatedIssuerNameSpaces, DIGEST_ALGORITHM) + + val mobileSecurityObjectGenerator = MobileSecurityObjectGenerator(DIGEST_ALGORITHM, NAMESPACE, devicePublicKey) + mobileSecurityObjectGenerator.addDigestIdsForNamespace(NAMESPACE, calculateDigestsForNameSpace) + val expirationTime: Long = kotlinx.datetime.Instant.Companion.DISTANT_FUTURE.toEpochMilliseconds() + mobileSecurityObjectGenerator.setValidityInfo( + Timestamp.now(), + Timestamp.now(), + Timestamp.ofEpochMilli(expirationTime), + null + ) + val mso: ByteArray = mobileSecurityObjectGenerator.generate() + + val coseSign1Sign: DataItem = Util.coseSign1Sign( + issuerKeypair.private, + ECDSA_ALGORITHM, + mso.copyOf(), + null, + listOf(issuerKeyPairAndCertificate.caCertificate(), issuerKeyPairAndCertificate.issuerCertificate()) + ) + + return construct(generatedIssuerNameSpaces, coseSign1Sign) + } + + @Throws(Exception::class) + private fun readKeypairAndCertificates(caKeyAndCertificate: String,issuerKeyAndCertificate: String): IssuerKeyPairAndCertificate? { + val pkcS12Reader = PKCS12Reader() + val caDetails: KeyPairAndCertificate = pkcS12Reader.extract(caKeyAndCertificate) + val issuerDetails: KeyPairAndCertificate = pkcS12Reader.extract(issuerKeyAndCertificate) + if (issuerDetails != null && caDetails != null) { + return IssuerKeyPairAndCertificate( + issuerDetails.keyPair, + issuerDetails.certificate, + caDetails.certificate + ) + } + return null + } + + private fun construct(nameSpaces: MutableMap>, issuerAuth: DataItem): String? { + val mDoc = MDoc(DOCTYPE, IssuerSigned(nameSpaces, issuerAuth)) + val cbor = mDoc.toCBOR() + return Base64.getUrlEncoder().encodeToString(cbor) + } +} + +data class MDoc(val docType: String, val issuerSigned: IssuerSigned) { + fun toCBOR(): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + CborEncoder(byteArrayOutputStream).encode( + CborBuilder().addMap() + .put("docType", docType) + .put(CBORConverter.toDataItem("issuerSigned"), CBORConverter.toDataItem(issuerSigned.toMap())) + .end() + .build() + ) + return byteArrayOutputStream.toByteArray() + + } +} + +data class IssuerSigned(val nameSpaces: MutableMap>, val issuerAuth: DataItem) { + fun toMap(): Map { + return buildMap { + put("nameSpaces", CBORConverter.toDataItem(nameSpaces)) + put("issuerAuth", issuerAuth) + } + } +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.kt b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.kt new file mode 100644 index 0000000..6971aa7 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.kt @@ -0,0 +1,66 @@ +package io.mosip.certify.util + +import co.nstant.`in`.cbor.CborDecoder +import co.nstant.`in`.cbor.model.* +import co.nstant.`in`.cbor.model.Map +import java.io.ByteArrayInputStream +import kotlin.Any +import kotlin.Array +import kotlin.Boolean +import kotlin.ByteArray +import kotlin.IllegalArgumentException +import kotlin.Int +import kotlin.Long +import kotlin.String + + +class CBORConverter() { + + companion object { + fun toDataItem(value: Any): DataItem { + return when (value) { + is DataItem -> value + is String -> UnicodeString(value) + is Int -> UnsignedInteger(value.toLong()) + is Long -> UnsignedInteger(value) + is Boolean -> { + if (value) SimpleValue.TRUE else SimpleValue.FALSE + } + + is kotlin.collections.Map<*, *> -> { + val cborMap = Map() + value.forEach { (key, value) -> + cborMap.put(UnicodeString(key as String), toDataItem(value!!)) + } + cborMap + } + + is List<*> -> { + val cborArray = Array() + value.forEach { item -> + cborArray.add(toDataItem(item!!)) + } + cborArray + } + + is Array<*> -> { + val cborArray = Array() + value.forEach { item -> + cborArray.add(toDataItem(item!!)) + } + cborArray + } + + is ByteArray -> { + val byteArrayInputStream = ByteArrayInputStream(value) + val dataItems = CborDecoder(byteArrayInputStream).decode() + return dataItems.firstOrNull()!! + } + + else -> throw IllegalArgumentException("Unsupported value: $value ${value.javaClass.simpleName}") + } + } + } +} + + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/IssuerKeyPairAndCertificate.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/IssuerKeyPairAndCertificate.java new file mode 100644 index 0000000..41ddbd1 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/IssuerKeyPairAndCertificate.java @@ -0,0 +1,8 @@ +package io.mosip.certify.util; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +public record IssuerKeyPairAndCertificate(KeyPair issuerKeypair, X509Certificate issuerCertificate, + X509Certificate caCertificate) { +} 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..9af04a6 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java @@ -0,0 +1,27 @@ +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/PKCS12Reader.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/PKCS12Reader.java new file mode 100644 index 0000000..34f04a3 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/PKCS12Reader.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 PKCS12Reader { + 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); + } +} + + diff --git a/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java index b4c91fe..0614e33 100644 --- a/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java +++ b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java @@ -4,6 +4,7 @@ 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.core.dto.ParsedAccessToken; import io.mosip.esignet.core.dto.OIDCTransaction; import io.mosip.kernel.signature.dto.JWTSignatureResponseDto; import io.mosip.kernel.signature.service.SignatureService;