Skip to content

Commit

Permalink
Merge pull request #331 from holashchand/issue-1023
Browse files Browse the repository at this point in the history
[BUG]: fixed verify api in registry and added verify any credential api
  • Loading branch information
challabeehyv authored Jun 11, 2024
2 parents f180784 + 790351c commit c00b5df
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 54 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ services:
- signature_v2_get_url=http://credential:3000/credentials/{id}
- signature_v2_delete_url=http://credential:3000/credentials/{id}
- signature_v2_verify_url=http://credential:3000/credentials/{id}/verify
- signature_v2_verify_any_url=http://credential:3000/credentials/verify
- signature_v2_revocation_list_url=http://credential:3000/credentials/revocation-list?issuerId={issuerDid}&page={page}&limit={limit}
- signature_v2_schema_health_check_url=http://credential-schema:3333/health
- signature_v2_schema_create_url=http://credential-schema:3333/credential-schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Template;
import com.google.gson.Gson;
import dev.sunbirdrc.pojos.ComponentHealthInfo;
import dev.sunbirdrc.registry.dao.NotFoundException;
import dev.sunbirdrc.registry.exception.SignatureException;
Expand All @@ -27,13 +28,11 @@
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;

import java.io.IOException;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

Expand All @@ -53,6 +52,8 @@ public class SignatureV2ServiceImpl implements SignatureService, ICertificateSer
@Value("${signature.v2.deleteCredentialByIdURL}")
private String deleteCredentialByIdURL;
@Value("${signature.v2.verifyCredentialURL}")
private String verifyCredentialByIdURL;
@Value("${signature.v2.verifyAnyCredentialURL}")
private String verifyCredentialURL;
@Value("${signature.v2.getRevocationListURL}")
private String getRevocationListURL;
Expand All @@ -73,6 +74,8 @@ public class SignatureV2ServiceImpl implements SignatureService, ICertificateSer
private CredentialSchemaService credentialSchemaService;
@Autowired
private DIDService didService;
@Autowired
private Gson gson;

@Override
public Object sign(Map<String, Object> propertyValue) throws SignatureException.UnreachableException, SignatureException.CreationException {
Expand All @@ -89,18 +92,39 @@ public Object sign(Map<String, Object> propertyValue) throws SignatureException.

@Override
public boolean verify(Object propertyValue) throws SignatureException.UnreachableException, SignatureException.VerificationException {
String credentialId = (String) ((Map<String, Object>) propertyValue).get("credentialId");
ObjectNode credential = (ObjectNode) ((Map<String, Object>) propertyValue).get("signedCredentials");
if(credentialId == null || credentialId.isEmpty()) {
credentialId = credential.get("credentialId").asText();
}
JsonNode resultNode = null;
ObjectNode properties = objectMapper.convertValue(propertyValue, ObjectNode.class);
JsonNode signedCredential = properties.get("signedCredentials");
try {
resultNode = this.verifyCredential(credentialId);
JsonNode resultNode = null;
if(signedCredential.isTextual()) {
resultNode = this.verifyCredentialById(signedCredential.asText());
} else if(signedCredential.isObject()) {
resultNode = verifyCredential(signedCredential, null);
}
if(resultNode == null) {
throw new RuntimeException("Invalid result while verifying");
}
AtomicReference<Boolean> verified = new AtomicReference<>(true);
if(resultNode.has("status")) {
verified.set(resultNode.get("status").asText().equals("ISSUED"));
}
String expectedValue = "OK";
if(resultNode.has("errors")) {
throw new SignatureException.VerificationException(resultNode.asText());
}
if(resultNode.has("checks")) {
for (JsonNode check : resultNode.get("checks")) {
check.fields().forEachRemaining(field -> {
if(!field.getValue().asText().equalsIgnoreCase(expectedValue)) {
verified.set(false);
}
});
}
}
return verified.get();
} catch (IOException e) {
throw new SignatureException.VerificationException(e.getMessage());
}
return resultNode.get("verified").asBoolean();
}

@Override
Expand Down Expand Up @@ -219,8 +243,27 @@ public ArrayNode revocationList(String issuerDid, Integer page, Integer limit) t
return JsonNodeFactory.instance.arrayNode();
}

public JsonNode verifyCredential(String credentialId) throws IOException {
ResponseEntity<String> response = retryRestTemplate.getForEntity(verifyCredentialURL, credentialId);
public JsonNode verifyCredential(Object vc, Object options) {
Map vcMap = objectMapper.convertValue(vc, Map.class);
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("verifiableCredential", vcMap);
requestMap.put("options", options);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(gson.toJson(requestMap), headers);
try {
ResponseEntity<String> response = retryRestTemplate.postForEntity(verifyCredentialURL, request);
if (response.getStatusCode().is2xxSuccessful()) {
return JSONUtil.convertStringJsonNode(response.getBody());
}
} catch (RestClientException | IOException e) {
logger.error("Exception while verifying a VC: {}, {}", vc, ExceptionUtils.getStackTrace(e));
}
return null;
}

public JsonNode verifyCredentialById(String credentialId) throws IOException {
ResponseEntity<String> response = retryRestTemplate.getForEntity(verifyCredentialByIdURL, credentialId);
if (response.getStatusCode().is2xxSuccessful()) {
return JSONUtil.convertStringJsonNode(response.getBody());
}
Expand Down
1 change: 1 addition & 0 deletions java/registry/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ signature:
getCredentialByIdURL: ${signature_v2_get_url:http://localhost:3000/credentials/{id}}
deleteCredentialByIdURL: ${signature_v2_delete_url:http://localhost:3000/credentials/{id}}
verifyCredentialURL: ${signature_v2_verify_url:http://localhost:3000/credentials/{id}/verify}
verifyAnyCredentialURL: ${signature_v2_verify_any_url:http://localhost:3000/credentials/verify}
getRevocationListURL: ${signature_v2_revocation_list_url:http://localhost:3000/credentials/revocation-list?issuerId={issuerDid}&page={page}&limit={limit}}
schema:
author: ${signature_v2_schema_author:Registry}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.IOException;
import java.util.Collections;
Expand Down Expand Up @@ -113,27 +114,38 @@ public void testSign_Exception() throws Exception {
public void testVerify_Success() throws SignatureException.VerificationException, SignatureException.UnreachableException, IOException {
// Prepare test data
ObjectNode credential = JsonNodeFactory.instance.objectNode();
credential.put("credentialId", "12345");
credential.put("signedCredentials", "12345");

ObjectNode result = JsonNodeFactory.instance.objectNode();
result.put("verified", "true");

doReturn(result).when(signatureServiceMock).verifyCredential(any());
assertTrue(signatureServiceMock.verify(Collections.singletonMap("credentialId", "12345")));

result.put("verified", "false");
assertFalse(signatureServiceMock.verify(Collections.singletonMap("credentialId", "12345")));
result.put("status", "ISSUED");
result.set("checks", JsonNodeFactory.instance.arrayNode()
.add(JsonNodeFactory.instance.objectNode()
.put("revoked", "ok")
.put("expired", "ok")
));
ReflectionTestUtils.setField(signatureServiceMock, "objectMapper", new ObjectMapper());
doReturn(result).when(signatureServiceMock).verifyCredentialById(any());
assertTrue(signatureServiceMock.verify(Collections.singletonMap("signedCredentials", "12345")));

result.put("status", "REVOKED");
assertFalse(signatureServiceMock.verify(Collections.singletonMap("signedCredentials", "12345")));
}

@Test
public void testVerify_Exception() throws Exception {
// Prepare test data
ObjectNode credential = JsonNodeFactory.instance.objectNode();
credential.put("credentialId", "12345");
credential.put("signedCredentials", "12345");

doThrow(new IOException()).when(signatureServiceMock).verifyCredential(any());
ObjectNode result = JsonNodeFactory.instance.objectNode();
result.put("status", "ISSUED");
result.set("errors", JsonNodeFactory.instance.arrayNode()
.add(JsonNodeFactory.instance.textNode("Exception while fetching the did")
));
ReflectionTestUtils.setField(signatureServiceMock, "objectMapper", new ObjectMapper());
doReturn(result).when(signatureServiceMock).verifyCredentialById(any());
try {
signatureServiceMock.verify(Collections.singletonMap("credentialId", "12345"));
signatureServiceMock.verify(Collections.singletonMap("signedCredentials", "12345"));
fail("Exception should be thrown");
} catch (Exception e) {
assertTrue(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Credential } from 'src/app.interface';
import { GetCredentialsByTagsResponseDTO } from './dto/getCredentialsByTags.dto';
import { GetCredentialByIdResponseDTO } from './dto/getCredentialById.dto';
import { RevocationListDTO } from './dto/revocaiton-list.dto';
import { VerifyCredentialDTO } from './dto/verify-credential.dto';

@Controller('credentials')
export class CredentialsController {
Expand Down Expand Up @@ -115,8 +116,13 @@ export class CredentialsController {
}

@Get(':id/verify')
verifyCredential(@Param('id') credId: string) {
return this.credentialsService.verifyCredential(credId);
verifyCredentialById(@Param('id') credId: string) {
return this.credentialsService.verifyCredentialById(credId);
}

@Post('/verify')
verifyCredential(@Body() verifyRequest: VerifyCredentialDTO) {
return this.credentialsService.verifyCredential(verifyRequest.verifiableCredential);
}

@Get('revocation-list')
Expand Down
51 changes: 27 additions & 24 deletions services/credentials-service/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,35 +146,15 @@ export class CredentialsService {
}
}

async verifyCredential(credId: string) {
// getting the credential from the db
const stored =
(await this.prisma.verifiableCredentials.findUnique({
where: {
id: credId,
},
select: {
signed: true,
status: true,
},
}));
const { signed: credToVerify, status } = (stored || {}) as { signed: Verifiable<W3CCredential>; status: VCStatus };

this.logger.debug('Fetched credntial from db to verify');

// invalid request in case credential is not found
if (!credToVerify) {
this.logger.error('Credential not found');
throw new NotFoundException({ errors: ['Credential not found'] });
}
async verifyCredential(credToVerify: Verifiable<W3CCredential>, status?: VCStatus) {
try {
// calling identity service to verify the issuer DID
const issuerId = (credToVerify.issuer?.id || credToVerify.issuer) as string;
const did: DIDDocument = await this.identityUtilsService.resolveDID(
issuerId
);
const credVerificationMethod = (credToVerify?.proof || {})[Object.keys(credToVerify?.proof || {})
.find(d => d.indexOf("verificationMethod") > -1)]
.find(d => d.indexOf("verificationMethod") > -1)]

// VERIFYING THE JWS
const vm = did.verificationMethod?.find(d => (d.id === credVerificationMethod || d.id === credVerificationMethod?.id));
Expand Down Expand Up @@ -206,8 +186,7 @@ export class CredentialsService {
status: status,
checks: [
{
active: 'OK',
revoked: status === VCStatus.REVOKED ? 'NOK' : 'OK', // NOK represents revoked
...(status && {revoked: status === VCStatus.REVOKED ? 'NOK' : 'OK'}), // NOK represents revoked
expired:
new Date(credToVerify.expirationDate).getTime() < Date.now()
? 'NOK'
Expand All @@ -224,6 +203,30 @@ export class CredentialsService {
}
}

async verifyCredentialById(credId: string) {
// getting the credential from the db
const stored =
(await this.prisma.verifiableCredentials.findUnique({
where: {
id: credId,
},
select: {
signed: true,
status: true,
},
}));
const { signed: credToVerify, status } = (stored || {}) as { signed: Verifiable<W3CCredential>; status: VCStatus };

this.logger.debug('Fetched credntial from db to verify');

// invalid request in case credential is not found
if (!credToVerify) {
this.logger.error('Credential not found');
throw new NotFoundException({ errors: ['Credential not found'] });
}
return this.verifyCredential(credToVerify, status);
}

async getSuite(verificationMethod: VerificationMethod, signatureType: string) {
const supportedSignatures = {
"Ed25519Signature2020": ["Ed25519VerificationKey2020", "JsonWebKey2020", "Ed25519VerificationKey2018"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ export class IssuedVerifiableCredential {
'@context': ReadonlyArray<string>;
id: string;
type: ReadonlyArray<string>;
issuer: {
id: string;
};
issuer: string | { id: string };
issuanceDate: string;
expirationDate: string;
credentialSubject: JSON;
Expand Down

0 comments on commit c00b5df

Please sign in to comment.