diff --git a/certify-core/pom.xml b/certify-core/pom.xml
index 402bbc3e..aa024db5 100644
--- a/certify-core/pom.xml
+++ b/certify-core/pom.xml
@@ -53,6 +53,41 @@
org.springframework.boot
spring-boot-starter-cache
+
+
+ org.apache.velocity
+ velocity
+ 1.7
+
+
+ org.apache.velocity.tools
+ velocity-tools-generic
+ 3.1
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.apache.velocity
+ velocity-engine-core
+
+
+
+
+ org.springframework.data
+ spring-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-test
+ test
+
+
jakarta.persistence
jakarta.persistence-api
diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java
index 509f4ac4..06f6ed25 100644
--- a/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java
+++ b/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java
@@ -15,5 +15,7 @@ public class Constants {
public static final String CLIENT_ID = "client_id";
public static final String CERTIFY_PARTNER_APP_ID = "CERTIFY_PARTNER";
public static final String CERTIFY_SERVICE_APP_ID = "CERTIFY_SERVICE";
+ public static final String CERTIFY_MOCK_RSA = "CERTIFY_MOCK_RSA";
public static final String ROOT_KEY = "ROOT";
+ public static final String EMPTY_REF_ID = "";
}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/SignatureAlg.java b/certify-core/src/main/java/io/mosip/certify/core/constants/SignatureAlg.java
new file mode 100644
index 00000000..b5903d77
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/constants/SignatureAlg.java
@@ -0,0 +1,15 @@
+package io.mosip.certify.core.constants;
+
+/**
+ * SignatureAlg is the constants file of supported VC sign algorithms.
+ * TODO(later): convert this into a structure such that it enables
+ * consumers to choose VC sign algos with
+ */
+public class SignatureAlg {
+ // LinkedDataSignature Algorithms
+ public static final String RSA_SIGNATURE_SUITE = "RsaSignature2018";
+
+ public static final String ED25519_SIGNATURE_SUITE = "Ed25519Signature2018";
+
+ // RS256, PS256, ES256 --> JWSAlgorithm.RS256.getName();
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM1Constants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM1Constants.java
new file mode 100644
index 00000000..197423b3
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM1Constants.java
@@ -0,0 +1,6 @@
+package io.mosip.certify.core.constants;
+
+public class VCDM1Constants {
+ public static final String ISSUANCE_DATE = "issuanceDate";
+ public static final String EXPIRATION_DATE = "expirationDate";
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM2Constants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM2Constants.java
new file mode 100644
index 00000000..b3c77286
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDM2Constants.java
@@ -0,0 +1,11 @@
+package io.mosip.certify.core.constants;
+
+/**
+ * VC Data Model 2.0 Constants are some constant fields for the (draft)
+ * VC DataModel 2.0.
+ * ref:
+ */
+public class VCDM2Constants {
+ public static final String VALID_UNITL = "validUntil";
+ public static final String VALID_FROM = "validFrom";
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/VCDMConstants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDMConstants.java
new file mode 100644
index 00000000..3a100ea3
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/constants/VCDMConstants.java
@@ -0,0 +1,8 @@
+package io.mosip.certify.core.constants;
+
+public class VCDMConstants {
+
+ // the below assertionMethods are common b/w VC 1.1 & VC 2.0
+ public static final String ASSERTION_METHOD = "assertionMethod";
+ public static final String PROOF = "proof";
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateData.java b/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateData.java
new file mode 100644
index 00000000..10e592f1
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateData.java
@@ -0,0 +1,34 @@
+package io.mosip.certify.core.entity;
+
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.*;
+import lombok.*;
+import jakarta.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+
+@Entity
+@NoArgsConstructor
+@IdClass(TemplateId.class)
+public class TemplateData {
+ @NotBlank(message = "Template is mandatory")
+ @Getter
+ @Setter
+ private String template;
+ @Id
+ @Getter
+ @Setter
+ private String context;
+ @Id
+ @Getter
+ @Setter
+ private String credentialType;
+
+ @NotBlank
+ @Column(name = "cr_dtimes")
+ private LocalDateTime createdTimes;
+
+ @Column(name = "upd_dtimes")
+ private LocalDateTime updatedTimes;
+
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateId.java b/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateId.java
new file mode 100644
index 00000000..ef472d04
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/entity/TemplateId.java
@@ -0,0 +1,29 @@
+package io.mosip.certify.core.entity;
+
+import lombok.*;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class TemplateId implements Serializable {
+ @Getter
+ @Setter
+ private String context;
+ @Getter
+ @Setter
+ private String credentialType;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TemplateId that)) return false;
+ return Objects.equals(context, that.context) && Objects.equals(credentialType, that.credentialType);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(context, credentialType);
+ }
+}
diff --git a/certify-core/src/main/java/io/mosip/certify/core/repository/TemplateRepository.java b/certify-core/src/main/java/io/mosip/certify/core/repository/TemplateRepository.java
new file mode 100644
index 00000000..bcef3d65
--- /dev/null
+++ b/certify-core/src/main/java/io/mosip/certify/core/repository/TemplateRepository.java
@@ -0,0 +1,15 @@
+package io.mosip.certify.core.repository;
+
+import io.mosip.certify.core.entity.TemplateData;
+import io.mosip.certify.core.entity.TemplateId;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface TemplateRepository extends JpaRepository {
+ Optional findByCredentialTypeAndContext(String credentialType, String context);
+ // NOTE: best practice? .save()
+}
+
diff --git a/certify-core/src/test/java/io/mosip/certify/core/validators/CredentialRequestValidatorFactoryTest.java b/certify-core/src/test/java/io/mosip/certify/core/validators/CredentialRequestValidatorFactoryTest.java
new file mode 100644
index 00000000..c5f6e53d
--- /dev/null
+++ b/certify-core/src/test/java/io/mosip/certify/core/validators/CredentialRequestValidatorFactoryTest.java
@@ -0,0 +1,51 @@
+package io.mosip.certify.core.validators;
+
+import io.mosip.certify.core.constants.VCFormats;
+import io.mosip.certify.core.dto.CredentialDefinition;
+import io.mosip.certify.core.dto.CredentialRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class CredentialRequestValidatorFactoryTest {
+
+ CredentialRequestValidatorFactory factory;
+
+ @Before
+ public void setUp() {
+ factory = new CredentialRequestValidatorFactory();
+ }
+
+ @Test
+ public void isValid_invalidFormat() {
+ CredentialRequest cr = new CredentialRequest();
+ cr.setFormat("fake-format");
+ assertFalse(factory.isValid(cr));
+ }
+
+ @Test
+ public void isValid_LDP_true() {
+ CredentialRequest cr = new CredentialRequest();
+ cr.setFormat(VCFormats.LDP_VC);
+ cr.setCredential_definition(new CredentialDefinition());
+ assertTrue(factory.isValid(cr));
+ }
+
+ @Test
+ public void isValid_mDoc_true() {
+ CredentialRequest cr = new CredentialRequest();
+ cr.setFormat(VCFormats.MSO_MDOC);
+ cr.setDoctype("mDoc-doctype-fake");
+ cr.setClaims(Map.of("isAge", "21"));
+ CredentialDefinition cd = new CredentialDefinition();
+ cd.setType(List.of("VerifiableCredential", "MockDrivingLicense"));
+ cd.setContext(List.of("https://example.context.page.sh"));
+ cr.setCredential_definition(new CredentialDefinition());
+ assertTrue(factory.isValid(cr));
+ }
+}
\ No newline at end of file
diff --git a/certify-core/src/test/resources/SchoolTemplate.vm b/certify-core/src/test/resources/SchoolTemplate.vm
new file mode 100644
index 00000000..957c9e42
--- /dev/null
+++ b/certify-core/src/test/resources/SchoolTemplate.vm
@@ -0,0 +1,38 @@
+#set($validFrom = $validFrom) ## Assume $validFrom might be null or undefined
+#set($validUntil = $validUntil) ## Assume $validFrom might be null or undefined
+
+{
+"@context": [
+"https://www.w3.org/ns/credentials/v2",
+"${context}"
+],
+"type": [
+"VerifiableCredential",
+"SchoolCertificate"
+],
+"validFrom": "${validFrom}",
+"issuer": "${issuer}",
+#if($validUntil)
+"validUntil": "${validUntil}",
+#end
+"credentialSubject": {
+ "foundingDate": "${dob}",
+ "city": "${city}",
+#if($amenities)
+ "amenities": #if($amenities)$amenities#else#set($amenities = '"[]"') $amenities#end,
+#end
+ "name": "${name}",
+ "principalName": "${principalName}",
+ "schoolType": "${schoolType}",
+ "country": "${country}",
+ "schoolRegistrationNumber": "${policyName}"
+},
+"renderMethod": [{
+"id": "https://vharsh.github.io/DID/insurance_svg_template.svg",
+"type": "SvgRenderingTemplate",
+"name": "Portrait Mode",
+"css3MediaQuery": "@media (orientation: portrait)",
+"digestMultibase": "zQmAPdhyxzznFCwYxAp2dRerWC85Wg6wFl9G270iEu5h6JqW"
+}]
+}
+
diff --git a/certify-integration-api/src/main/java/io/mosip/certify/api/exception/DataProviderExchangeException.java b/certify-integration-api/src/main/java/io/mosip/certify/api/exception/DataProviderExchangeException.java
new file mode 100644
index 00000000..7232c4f4
--- /dev/null
+++ b/certify-integration-api/src/main/java/io/mosip/certify/api/exception/DataProviderExchangeException.java
@@ -0,0 +1,31 @@
+package io.mosip.certify.api.exception;
+
+import io.mosip.certify.api.util.ErrorConstants;
+
+/**
+ * {@link DataProviderExchangeException} is thrown when the DataProvider
+ * plugin fails to return user data against a set of claims generated by
+ * an Authentication provider.
+ */
+public class DataProviderExchangeException extends Exception {
+ private String errorCode;
+
+ public DataProviderExchangeException() {
+ super(ErrorConstants.VCI_DATAFETCH_FAILED);
+ this.errorCode = ErrorConstants.VCI_DATAFETCH_FAILED;
+ }
+
+ public DataProviderExchangeException(String errorCode) {
+ super(errorCode);
+ this.errorCode = errorCode;
+ }
+
+ public DataProviderExchangeException(String errorCode, String errorMessage) {
+ super(errorCode + " -> " + errorMessage);
+ this.errorCode = errorCode;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/certify-integration-api/src/main/java/io/mosip/certify/api/spi/DataProviderPlugin.java b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/DataProviderPlugin.java
new file mode 100644
index 00000000..0d2e6c79
--- /dev/null
+++ b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/DataProviderPlugin.java
@@ -0,0 +1,14 @@
+package io.mosip.certify.api.spi;
+
+import io.mosip.certify.api.exception.DataProviderExchangeException;
+
+import java.util.Map;
+
+/**
+ * DataProviderPlugin is implemented by type#2 of identity plugin
+ * implementors to fetch data for Certify to template into a VC
+ * format of choice using {@link VCFormatter}.
+ */
+public interface DataProviderPlugin {
+ Map fetchData(Map identityDetails) throws DataProviderExchangeException;
+}
diff --git a/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCFormatter.java b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCFormatter.java
new file mode 100644
index 00000000..b5082bea
--- /dev/null
+++ b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCFormatter.java
@@ -0,0 +1,13 @@
+package io.mosip.certify.api.spi;
+
+
+import java.util.Map;
+/**
+ * VCDataModelFormatter is a templating engine which takes @param templateInput and returns a templated VC.
+ * Some implementations include
+ * - VC 2.0 data model templating engine
+ */
+public interface VCFormatter {
+ // TODO: Should it be changed to JSONObject?
+ String format(Map templateInput, Map defaultSettings);
+}
\ No newline at end of file
diff --git a/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCSigner.java b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCSigner.java
new file mode 100644
index 00000000..724ae57f
--- /dev/null
+++ b/certify-integration-api/src/main/java/io/mosip/certify/api/spi/VCSigner.java
@@ -0,0 +1,11 @@
+package io.mosip.certify.api.spi;
+
+import io.mosip.certify.api.dto.VCResult;
+import java.util.Map;
+
+/**
+ * VCSigner can sign any VC provided a vcHash & Signer inputs
+ */
+public interface VCSigner {
+ VCResult> perform(String templatedVC, Map params);
+}
diff --git a/certify-integration-api/src/main/java/io/mosip/certify/api/util/ErrorConstants.java b/certify-integration-api/src/main/java/io/mosip/certify/api/util/ErrorConstants.java
index 9e1538db..87ef9ffd 100644
--- a/certify-integration-api/src/main/java/io/mosip/certify/api/util/ErrorConstants.java
+++ b/certify-integration-api/src/main/java/io/mosip/certify/api/util/ErrorConstants.java
@@ -9,4 +9,5 @@ public class ErrorConstants {
public static final String NOT_IMPLEMENTED = "not_implemented";
public static final String VCI_EXCHANGE_FAILED = "vci_exchange_failed";
+ public static final String VCI_DATAFETCH_FAILED = "vci_datafetch_failed";
}
diff --git a/certify-service/configure_start.sh b/certify-service/configure_start.sh
index ef5e13fd..71783552 100644
--- a/certify-service/configure_start.sh
+++ b/certify-service/configure_start.sh
@@ -25,6 +25,9 @@ download_and_extract() {
if [ "$enable_certify_artifactory" = "true" ]; then
download_and_extract "${artifactory_url_env}/artifactory/libs-release-local/certify/certify-plugin.zip" "${loader_path_env}"
+ echo "Please patch plugin JAR now"
+ sleep 60
+ echo "Plugin JAR patching not posssible now"
fi
#installs the pkcs11 libraries.
@@ -67,4 +70,4 @@ if [ "$download_hsm_client" = "true" ]; then
fi
cd $work_dir
-exec "$@"
\ No newline at end of file
+exec "$@"
diff --git a/certify-service/pom.xml b/certify-service/pom.xml
index f535c5c6..ca90cd4f 100644
--- a/certify-service/pom.xml
+++ b/certify-service/pom.xml
@@ -57,6 +57,12 @@
+
+ net.javacrumbs.json-unit
+ json-unit-assertj
+ 3.4.1
+ test
+
info.weboftrust
ld-signatures-java
diff --git a/certify-service/src/main/java/io/mosip/certify/CertifyServiceApplication.java b/certify-service/src/main/java/io/mosip/certify/CertifyServiceApplication.java
index 134bc317..3a7e80e4 100644
--- a/certify-service/src/main/java/io/mosip/certify/CertifyServiceApplication.java
+++ b/certify-service/src/main/java/io/mosip/certify/CertifyServiceApplication.java
@@ -9,11 +9,13 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@EnableCaching
@SpringBootApplication(scanBasePackages = "io.mosip.certify,"+
+ "io.mosip.certify.core.*," +
"io.mosip.kernel.crypto," +
"io.mosip.kernel.keymanager.hsm," +
"io.mosip.kernel.cryptomanager," +
diff --git a/certify-service/src/main/java/io/mosip/certify/config/AppConfig.java b/certify-service/src/main/java/io/mosip/certify/config/AppConfig.java
index f097e5dc..265d30ae 100644
--- a/certify-service/src/main/java/io/mosip/certify/config/AppConfig.java
+++ b/certify-service/src/main/java/io/mosip/certify/config/AppConfig.java
@@ -30,8 +30,8 @@
import org.springframework.web.client.RestTemplate;
@Configuration
-@EnableJpaRepositories(basePackages = {"io.mosip.certify.core.repository", "io.mosip.kernel.keymanagerservice.repository"})
-@EntityScan(basePackages = {"io.mosip.certify.core.entity", "io.mosip.kernel.keymanagerservice.entity"})
+@EnableJpaRepositories(basePackages = {"io.mosip.kernel.keymanagerservice.repository", "io.mosip.certify.core.repository"})
+@EntityScan(basePackages = {"io.mosip.kernel.keymanagerservice.entity, io.mosip.certify.core.entity"})
@Slf4j
public class AppConfig implements ApplicationRunner {
@@ -84,7 +84,12 @@ public void run(ApplicationArguments args) throws Exception {
// Set the reference id to empty string, as keymanager is expecting the same for initialization
masterKeyRequest.setReferenceId(org.apache.commons.lang3.StringUtils.EMPTY);
keymanagerService.generateMasterKey(objectType, masterKeyRequest);
-
+ // TODO: Generate an EC & ED key via K8s Job(INJICERT-469)
+ KeyPairGenerateRequestDto rsaKeyRequest = new KeyPairGenerateRequestDto();
+ rsaKeyRequest.setApplicationId(Constants.CERTIFY_MOCK_RSA);
+ rsaKeyRequest.setReferenceId(Constants.EMPTY_REF_ID);
+ rsaKeyRequest.setForce(false);
+ keymanagerService.generateMasterKey("certificate", rsaKeyRequest);
if(!StringUtils.isEmpty(cacheSecretKeyRefId)) {
SymmetricKeyGenerateRequestDto symmetricKeyGenerateRequestDto = new SymmetricKeyGenerateRequestDto();
symmetricKeyGenerateRequestDto.setApplicationId(Constants.CERTIFY_SERVICE_APP_ID);
diff --git a/certify-service/src/main/java/io/mosip/certify/impl/LoggerAuditService.java b/certify-service/src/main/java/io/mosip/certify/plugin/impl/LoggerAuditService.java
similarity index 100%
rename from certify-service/src/main/java/io/mosip/certify/impl/LoggerAuditService.java
rename to certify-service/src/main/java/io/mosip/certify/plugin/impl/LoggerAuditService.java
diff --git a/certify-service/src/main/java/io/mosip/certify/services/CertifyIssuanceServiceImpl.java b/certify-service/src/main/java/io/mosip/certify/services/CertifyIssuanceServiceImpl.java
new file mode 100644
index 00000000..aac9a097
--- /dev/null
+++ b/certify-service/src/main/java/io/mosip/certify/services/CertifyIssuanceServiceImpl.java
@@ -0,0 +1,255 @@
+/*
+ * 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.services;
+
+import com.nimbusds.jose.JWSAlgorithm;
+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.DataProviderExchangeException;
+import io.mosip.certify.api.spi.*;
+import io.mosip.certify.api.util.Action;
+import io.mosip.certify.api.util.ActionStatus;
+import io.mosip.certify.core.constants.VCFormats;
+import io.mosip.certify.core.constants.SignatureAlg;
+import io.mosip.certify.core.dto.CredentialMetadata;
+import io.mosip.certify.core.dto.CredentialRequest;
+import io.mosip.certify.core.dto.CredentialResponse;
+import io.mosip.certify.core.dto.ParsedAccessToken;
+import io.mosip.certify.core.dto.VCIssuanceTransaction;
+import io.mosip.certify.core.constants.Constants;
+import io.mosip.certify.core.constants.ErrorConstants;
+import io.mosip.certify.core.exception.CertifyException;
+import io.mosip.certify.core.exception.InvalidRequestException;
+import io.mosip.certify.core.exception.NotAuthenticatedException;
+import io.mosip.certify.core.spi.VCIssuanceService;
+import io.mosip.certify.core.util.AuditHelper;
+import io.mosip.certify.core.util.SecurityHelperService;
+import io.mosip.certify.core.validators.CredentialRequestValidatorFactory;
+import io.mosip.certify.exception.InvalidNonceException;
+import io.mosip.certify.proof.ProofValidator;
+import io.mosip.certify.proof.ProofValidatorFactory;
+import io.mosip.certify.utils.CredentialUtils;
+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.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.stereotype.Service;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.*;
+
+@Slf4j
+@Service
+@ConditionalOnProperty(value = "mosip.certify.issuer", havingValue = "CertifyIssuer")
+public class CertifyIssuanceServiceImpl implements VCIssuanceService {
+
+ @Value("#{${mosip.certify.key-values}}")
+ private LinkedHashMap> issuerMetadata;
+
+ @Value("${mosip.certify.cnonce-expire-seconds:300}")
+ private int cNonceExpireSeconds;
+
+ @Autowired
+ private ParsedAccessToken parsedAccessToken;
+
+ @Autowired
+ private VCFormatter vcFormatter;
+
+ @Autowired
+ private VCSigner vcSigner;
+
+ @Autowired
+ private DataProviderPlugin dataModelService;
+
+ @Value("${mosip.certify.issuer.pub.key}")
+ private String hostedKey;
+
+ @Value("${mosip.certify.issuer.uri}")
+ private String issuerURI;
+
+ @Autowired
+ private ProofValidatorFactory proofValidatorFactory;
+
+ @Autowired
+ private VCICacheService vciCacheService;
+
+ @Autowired
+ private SecurityHelperService securityHelperService;
+
+ @Autowired
+ private AuditPlugin auditWrapper;
+
+ @Override
+ public CredentialResponse getCredential(CredentialRequest credentialRequest) {
+ // 1. Credential Request validation
+ boolean isValidCredentialRequest = new CredentialRequestValidatorFactory().isValid(credentialRequest);
+ if(!isValidCredentialRequest) {
+ throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST);
+ }
+
+ if(!parsedAccessToken.isActive())
+ throw new NotAuthenticatedException();
+ // 2. Scope Validation
+ String scopeClaim = (String) parsedAccessToken.getClaims().getOrDefault("scope", "");
+ CredentialMetadata credentialMetadata = null;
+ for(String scope : scopeClaim.split(Constants.SPACE)) {
+ Optional result = getScopeCredentialMapping(scope, credentialRequest.getFormat());
+ if(result.isPresent()) {
+ credentialMetadata = result.get(); //considering only first credential scope
+ break;
+ }
+ }
+
+ if(credentialMetadata == null) {
+ log.error("No credential mapping found for the provided scope {}", scopeClaim);
+ throw new CertifyException(ErrorConstants.INVALID_SCOPE);
+ }
+
+ // 3. Proof Validation
+ ProofValidator proofValidator = proofValidatorFactory.getProofValidator(credentialRequest.getProof().getProof_type());
+ if(!proofValidator.validate((String)parsedAccessToken.getClaims().get(Constants.CLIENT_ID), getValidClientNonce(),
+ credentialRequest.getProof())) {
+ throw new CertifyException(ErrorConstants.INVALID_PROOF);
+ }
+
+ // 4. Get VC from configured plugin implementation
+ VCResult> vcResult = getVerifiableCredential(credentialRequest, credentialMetadata,
+ proofValidator.getKeyMaterial(credentialRequest.getProof()));
+
+ auditWrapper.logAudit(Action.VC_ISSUANCE, ActionStatus.SUCCESS,
+ AuditHelper.buildAuditDto(parsedAccessToken.getAccessTokenHash(), "accessTokenHash"), null);
+ return getCredentialResponse(credentialRequest.getFormat(), vcResult);
+ }
+
+ @Override
+ public Map getCredentialIssuerMetadata(String version) {
+ if(issuerMetadata.containsKey(version))
+ return issuerMetadata.get(version);
+ throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_OPENID_VERSION);
+ }
+
+ private VCResult> getVerifiableCredential(CredentialRequest credentialRequest, CredentialMetadata credentialMetadata,
+ String holderId) {
+ parsedAccessToken.getClaims().put("accessTokenHash", parsedAccessToken.getAccessTokenHash());
+ VCRequestDto vcRequestDto = new VCRequestDto();
+ vcRequestDto.setFormat(credentialRequest.getFormat());
+
+
+ VCResult> vcResult = null;
+ switch (credentialRequest.getFormat()) {
+ case "ldp_vc" :
+ vcRequestDto.setContext(credentialRequest.getCredential_definition().getContext());
+ vcRequestDto.setType(credentialRequest.getCredential_definition().getType());
+ vcRequestDto.setCredentialSubject(credentialRequest.getCredential_definition().getCredentialSubject());
+ validateLdpVcFormatRequest(credentialRequest, credentialMetadata);
+ try {
+ // TODO(multitenancy): later decide which plugin out of n plugins is the correct one
+ Map identityData = dataModelService.fetchData(parsedAccessToken.getClaims());
+ Map templateParams = new HashMap<>();
+ templateParams.put("templateName", CredentialUtils.getTemplateName(vcRequestDto));
+ templateParams.put("issuerURI", issuerURI);
+ String templatedVC = vcFormatter.format(identityData, templateParams);
+ Map vcSignerParams = new HashMap<>();
+ // TODO: Collate this into simpler APIs where just key-type is specified
+ vcSignerParams.put(KeyManagerConstants.VC_SIGN_ALGO,
+ SignatureAlg.RSA_SIGNATURE_SUITE);
+ vcSignerParams.put(KeyManagerConstants.PUBLIC_KEY_URL, hostedKey);
+ vcSignerParams.put(KeyManagerConstants.KEY_APP_ID, Constants.CERTIFY_MOCK_RSA);
+ vcSignerParams.put(KeyManagerConstants.KEY_REF_ID, Constants.EMPTY_REF_ID);
+ // Change it to PS256 as per --> https://w3c.github.io/vc-jws-2020/#dfn-jsonwebsignature2020
+ vcSignerParams.put(KeyManagerConstants.KEYMGR_SIGN_ALGO, JWSAlgorithm.RS256.getName());
+ vcResult = vcSigner.perform(templatedVC, vcSignerParams);
+ } catch(DataProviderExchangeException e) {
+ throw new CertifyException(e.getErrorCode());
+ }
+ break;
+ default:
+ throw new CertifyException(ErrorConstants.UNSUPPORTED_VC_FORMAT);
+ }
+
+ if(vcResult != null && vcResult.getCredential() != null)
+ return vcResult;
+
+ log.error("Failed to generate VC : {}", vcResult);
+ auditWrapper.logAudit(Action.VC_ISSUANCE, ActionStatus.ERROR,
+ AuditHelper.buildAuditDto(parsedAccessToken.getAccessTokenHash(), "accessTokenHash"), null);
+ throw new CertifyException(ErrorConstants.VC_ISSUANCE_FAILED);
+ }
+
+ private CredentialResponse> getCredentialResponse(String format, VCResult> vcResult) {
+ switch (format) {
+ case "ldp_vc":
+ CredentialResponse ldpVcResponse = new CredentialResponse<>();
+ ldpVcResponse.setCredential((JsonLDObject)vcResult.getCredential());
+ return ldpVcResponse;
+ }
+ throw new CertifyException(ErrorConstants.UNSUPPORTED_VC_FORMAT);
+ }
+
+ private Optional getScopeCredentialMapping(String scope, String format) {
+ Map vciMetadata = getCredentialIssuerMetadata("latest");
+ LinkedHashMap supportedCredentials = (LinkedHashMap) vciMetadata.get("credential_configurations_supported");
+ Optional> result = supportedCredentials.entrySet().stream()
+ .filter(cm -> ((LinkedHashMap) cm.getValue()).get("scope").equals(scope)).findFirst();
+
+ if(result.isPresent()) {
+ LinkedHashMap metadata = (LinkedHashMap)result.get().getValue();
+ CredentialMetadata credentialMetadata = new CredentialMetadata();
+ credentialMetadata.setFormat((String) metadata.get("format"));
+ credentialMetadata.setScope((String) metadata.get("scope"));
+ credentialMetadata.setId(result.get().getKey());
+ if(format.equals(VCFormats.LDP_VC)){
+ LinkedHashMap credentialDefinition = (LinkedHashMap) metadata.get("credential_definition");
+ credentialMetadata.setTypes((List) credentialDefinition.get("type"));
+ }
+ return Optional.of(credentialMetadata);
+ }
+ return Optional.empty();
+ }
+
+ private void validateLdpVcFormatRequest(CredentialRequest credentialRequest,
+ CredentialMetadata credentialMetadata) {
+ if(!credentialRequest.getCredential_definition().getType().containsAll(credentialMetadata.getTypes()))
+ throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_VC_TYPE);
+
+ //TODO need to validate Credential_definition as JsonLD document, if invalid throw exception
+ }
+
+ private String getValidClientNonce() {
+ VCIssuanceTransaction transaction = vciCacheService.getVCITransaction(parsedAccessToken.getAccessTokenHash());
+ //If the transaction is null, it means that VCI service never created cNonce, its authorization server issued cNonce
+ String cNonce = (transaction == null) ?
+ (String) parsedAccessToken.getClaims().get(Constants.C_NONCE) :
+ transaction.getCNonce();
+ Object nonceExpireSeconds = parsedAccessToken.getClaims().getOrDefault(Constants.C_NONCE_EXPIRES_IN, 0);
+ int cNonceExpire = (transaction == null) ?
+ nonceExpireSeconds instanceof Long ? (int)(long)nonceExpireSeconds : (int)nonceExpireSeconds :
+ transaction.getCNonceExpireSeconds();
+ long issuedEpoch = (transaction == null) ?
+ ((Instant) parsedAccessToken.getClaims().getOrDefault(JwtClaimNames.IAT, Instant.MIN)).getEpochSecond():
+ transaction.getCNonceIssuedEpoch();
+
+ if( cNonce == null ||
+ cNonceExpire <= 0 ||
+ (issuedEpoch+cNonceExpire) < LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC) ) {
+ log.error("Client Nonce not found / expired in the access token, generate new cNonce");
+ transaction = createVCITransaction();
+ throw new InvalidNonceException(transaction.getCNonce(), transaction.getCNonceExpireSeconds());
+ }
+ return cNonce;
+ }
+
+ private VCIssuanceTransaction createVCITransaction() {
+ VCIssuanceTransaction transaction = new VCIssuanceTransaction();
+ transaction.setCNonce(securityHelperService.generateSecureRandomString(20));
+ transaction.setCNonceIssuedEpoch(LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC));
+ transaction.setCNonceExpireSeconds(cNonceExpireSeconds);
+ return vciCacheService.setVCITransaction(parsedAccessToken.getAccessTokenHash(), transaction);
+ }
+}
diff --git a/certify-service/src/main/java/io/mosip/certify/services/KeyManagerConstants.java b/certify-service/src/main/java/io/mosip/certify/services/KeyManagerConstants.java
new file mode 100644
index 00000000..46a83918
--- /dev/null
+++ b/certify-service/src/main/java/io/mosip/certify/services/KeyManagerConstants.java
@@ -0,0 +1,14 @@
+/*
+ * 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.services;
+
+public class KeyManagerConstants {
+ public static final String VC_SIGN_ALGO = "VCsignAlgo";
+ public static final String PUBLIC_KEY_URL = "publicKeyURL";
+ public static final String KEY_APP_ID = "keyAppId";
+ public static final String KEY_REF_ID = "keyRefId";
+ public static final String KEYMGR_SIGN_ALGO = "KeyMgrSignAlgo";
+}
diff --git a/certify-service/src/main/java/io/mosip/certify/services/KeymanagerLibSigner.java b/certify-service/src/main/java/io/mosip/certify/services/KeymanagerLibSigner.java
new file mode 100644
index 00000000..693336bb
--- /dev/null
+++ b/certify-service/src/main/java/io/mosip/certify/services/KeymanagerLibSigner.java
@@ -0,0 +1,118 @@
+/*
+ * 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.services;
+
+import foundation.identity.jsonld.JsonLDException;
+import foundation.identity.jsonld.JsonLDObject;
+import info.weboftrust.ldsignatures.LdProof;
+import info.weboftrust.ldsignatures.canonicalizer.URDNA2015Canonicalizer;
+import io.mosip.certify.api.dto.VCResult;
+import io.mosip.certify.api.spi.VCSigner;
+import io.mosip.certify.core.constants.*;
+import io.mosip.certify.core.exception.CertifyException;
+import io.mosip.kernel.signature.dto.JWSSignatureRequestDto;
+import io.mosip.kernel.signature.dto.JWTSignatureResponseDto;
+import io.mosip.kernel.signature.service.SignatureService;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONArray;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * KeymanagerLibSigner is a VCSigner which uses the Certify embedded
+ * keymanager to perform VC signing tasks for JSON LD VCs.
+ * These are the known external requirements:
+ * - the public key must be pre-hosted for the VC & should be available
+ * so long that VC should be verifiable
+ * - the VC should have a validFrom or issuanceDate in a specific UTC format
+ */
+@Slf4j
+@Service
+public class KeymanagerLibSigner implements VCSigner {
+
+ @Autowired
+ SignatureService signatureService;
+
+ @Override
+ public VCResult perform(String templatedVC, Map keyMgrInput) {
+ // Can the below lines be done at Templating side itself ?
+ VCResult VC = new VCResult<>();
+ JsonLDObject j = JsonLDObject.fromJson(templatedVC);
+ j.setDocumentLoader(null);
+ // NOTE: other aspects can be configured via keyMgrInput map
+ String validFrom;
+ String signatureAlgorithm = keyMgrInput.getOrDefault(KeyManagerConstants.VC_SIGN_ALGO,
+ SignatureAlg.RSA_SIGNATURE_SUITE);
+ String publicKeyURL = keyMgrInput.get(KeyManagerConstants.PUBLIC_KEY_URL);
+ String keyAppId = keyMgrInput.get(KeyManagerConstants.KEY_APP_ID);
+ String keyRefId = keyMgrInput.get(KeyManagerConstants.KEY_REF_ID);
+ String keyManagerSignAlgo = keyMgrInput.get(KeyManagerConstants.KEYMGR_SIGN_ALGO);
+ if (j.getJsonObject().containsKey(VCDM1Constants.ISSUANCE_DATE)) {
+ validFrom = j.getJsonObject().get(VCDM1Constants.ISSUANCE_DATE).toString();
+ } else if (j.getJsonObject().containsKey(VCDM2Constants.VALID_FROM)){
+ validFrom = j.getJsonObject().get(VCDM2Constants.VALID_FROM).toString();
+ } else {
+ validFrom = ZonedDateTime.now(ZoneOffset.UTC)
+ .format(DateTimeFormatter.ofPattern(Constants.UTC_DATETIME_PATTERN));
+ }
+ // TODO: VC Data Model spec doesn't specify a single date format or a
+ // timezone restriction, this will have to be supported timely.
+ Date createDate = Date
+ .from(LocalDateTime
+ .parse(validFrom,
+ DateTimeFormatter.ofPattern(Constants.UTC_DATETIME_PATTERN))
+ .atZone(ZoneId.systemDefault()).toInstant());
+ LdProof vcLdProof = LdProof.builder().defaultContexts(false).defaultTypes(false).type(signatureAlgorithm)
+ .created(createDate).proofPurpose(VCDMConstants.ASSERTION_METHOD)
+ .verificationMethod(URI.create(publicKeyURL))
+ .build();
+ // 1. Canonicalize
+ URDNA2015Canonicalizer canonicalizer = new URDNA2015Canonicalizer();
+ byte[] vcSignBytes = null;
+ try {
+ vcSignBytes = canonicalizer.canonicalize(vcLdProof, j);
+ } catch (IOException | GeneralSecurityException | JsonLDException e) {
+ log.error("Error during canonicalization", e.getMessage());
+ throw new CertifyException("Error during canonicalization");
+ }
+
+ // 2. VC Sign
+ String vcEncodedData = Base64.getUrlEncoder().encodeToString(vcSignBytes);
+ JWSSignatureRequestDto payload = new JWSSignatureRequestDto();
+ payload.setDataToSign(vcEncodedData);
+ payload.setApplicationId(keyAppId);
+ payload.setReferenceId(keyRefId); // alg, empty = RSA
+ payload.setIncludePayload(false);
+ payload.setIncludeCertificate(false);
+ payload.setIncludeCertHash(true);
+ payload.setValidateJson(false);
+ payload.setB64JWSHeaderParam(false);
+ payload.setCertificateUrl("");
+ payload.setSignAlgorithm(keyManagerSignAlgo); // RSSignature2018 --> RS256, PS256, ES256
+ // TODO: Should this be a well defined Certify Exception for better comms b/w Certify & Support team?
+ JWTSignatureResponseDto jwsSignedData = signatureService.jwsSign(payload);
+ String sign = jwsSignedData.getJwtSignedData();
+ LdProof ldProofWithJWS = LdProof.builder().base(vcLdProof).defaultContexts(false)
+ .jws(sign).build();
+ ldProofWithJWS.addToJsonLDObject(j);
+ VC.setCredential(j);
+ return VC;
+ // TODO: Check if this is really a VC
+ // MOSIP ref: https://github.com/mosip/id-authentication/blob/master/authentication/authentication-service/src/main/java/io/mosip/authentication/service/kyc/impl/VciServiceImpl.java#L281
+ }
+}
diff --git a/certify-service/src/main/java/io/mosip/certify/services/VCIssuanceServiceImpl.java b/certify-service/src/main/java/io/mosip/certify/services/VCIssuanceServiceImpl.java
index bda09424..e768e6dd 100644
--- a/certify-service/src/main/java/io/mosip/certify/services/VCIssuanceServiceImpl.java
+++ b/certify-service/src/main/java/io/mosip/certify/services/VCIssuanceServiceImpl.java
@@ -35,6 +35,7 @@
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.security.oauth2.jwt.JwtClaimNames;
import org.springframework.stereotype.Service;
import java.time.Instant;
@@ -47,6 +48,7 @@
@Slf4j
@Service
+@ConditionalOnProperty(value = "mosip.certify.issuer", havingValue = "PluginIssuer")
public class VCIssuanceServiceImpl implements VCIssuanceService {
private static final String TYPE_VERIFIABLE_CREDENTIAL = "VerifiableCredential";
@@ -117,9 +119,9 @@ public CredentialResponse getCredential(CredentialRequest credentialRequest) {
@Override
public Map getCredentialIssuerMetadata(String version) {
- if(issuerMetadata.containsKey(version))
- return issuerMetadata.get(version);
- throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_OPENID_VERSION);
+ if(issuerMetadata.containsKey(version))
+ return issuerMetadata.get(version);
+ throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_OPENID_VERSION);
}
private VCResult> getVerifiableCredential(CredentialRequest credentialRequest, CredentialMetadata credentialMetadata,
@@ -213,7 +215,7 @@ private Optional getScopeCredentialMapping(String scope, St
private void validateLdpVcFormatRequest(CredentialRequest credentialRequest,
CredentialMetadata credentialMetadata) {
if(!credentialRequest.getCredential_definition().getType().containsAll(credentialMetadata.getTypes()))
- throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_VC_TYPE);
+ throw new InvalidRequestException(ErrorConstants.UNSUPPORTED_VC_TYPE);
//TODO need to validate Credential_definition as JsonLD document, if invalid throw exception
}
diff --git a/certify-service/src/main/java/io/mosip/certify/services/templating/VelocityTemplatingEngineImpl.java b/certify-service/src/main/java/io/mosip/certify/services/templating/VelocityTemplatingEngineImpl.java
new file mode 100644
index 00000000..dca1614d
--- /dev/null
+++ b/certify-service/src/main/java/io/mosip/certify/services/templating/VelocityTemplatingEngineImpl.java
@@ -0,0 +1,98 @@
+/*
+ * 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.services.templating;
+
+import java.io.*;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import io.mosip.certify.api.spi.VCFormatter;
+import io.mosip.certify.core.constants.Constants;
+import io.mosip.certify.core.constants.VCDM2Constants;
+import io.mosip.certify.core.repository.TemplateRepository;
+import jakarta.annotation.PostConstruct;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.tools.generic.DateTool;
+import org.json.JSONArray;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class VelocityTemplatingEngineImpl implements VCFormatter {
+ VelocityEngine engine;
+ public static final String DELIMITER = ":";
+ Map templateCache;
+ @Autowired
+ TemplateRepository templateRepository;
+ @Value("${mosip.certify.vcformat.vc.expiry:true}")
+ boolean shouldHaveDates;
+
+ @PostConstruct
+ public void initialize() {
+ engine = new VelocityEngine();
+ // TODO: The DataSourceResourceLoader can be used instead if there's a
+ // single primary key column and the table has a last modified date.
+ templateCache = new HashMap<>();
+ templateRepository.findAll().stream().forEach((template -> templateCache.put(String.join(DELIMITER, template.getCredentialType(), template.getContext()), template.getTemplate())));
+ engine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
+ engine.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8");
+ engine.init();
+ }
+
+ // TODO: Add a public method for updating the Velocity template cache
+
+ /**
+ * performs the templating
+ * NOTE: the defaultSettings map should have the "templateName" key set to
+ * "${sort(CREDENTIALTYPE1,CREDENTIALTYPE2,CREDENTIALTYPE3...)}:${sort(VC_CONTEXT1,VC_CONTENXT2,VC_CONTEXT3...)}"
+ *
+ * @param templateInput is the input from the DataProvider plugin
+ * @param defaultSettings has some sensible defaults from Certify for
+ * internal work such as locating the appropriate template
+ * @return templated VC as a String
+ */
+ @Override
+ public String format(Map templateInput, Map defaultSettings) {
+ // TODO: Isn't template name becoming too complex with VC_CONTEXTS & CREDENTIAL_TYPES both?
+ String templateName = defaultSettings.get("templateName").toString();
+ String issuer = defaultSettings.get("issuerURI").toString();
+ String t = templateCache.get(templateName);
+ StringWriter writer = new StringWriter();
+ // 1. Prepare map
+ Map finalTemplate = new HashMap<>();
+ for (String key : templateInput.keySet()) {
+ Object value = templateInput.get(key);
+ if (value instanceof List) {
+ // TODO(problem area): handle field values with unescaped JSON
+ // reserved literals such as " or ,
+ // (Q) Should Object always be a JSONObject?
+ finalTemplate.put(key, new JSONArray((List