diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java index 053b08f..1e8efab 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -1,6 +1,7 @@ package org.oneedtech.inspect.vc; import static java.lang.Boolean.TRUE; +import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN; import static org.oneedtech.inspect.core.probe.RunContext.Key.*; import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; @@ -19,14 +20,19 @@ import org.oneedtech.inspect.core.probe.GeneratedObject; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; +import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.schema.JsonSchemaCache; +import org.oneedtech.inspect.schema.SchemaKey; import org.oneedtech.inspect.util.json.ObjectMapperCache; import org.oneedtech.inspect.util.resource.Resource; import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.vc.VerifiableCredential.Type; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; +import org.oneedtech.inspect.vc.probe.CredentialParseProbe; +import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe; import org.oneedtech.inspect.vc.probe.ExpirationProbe; import org.oneedtech.inspect.vc.probe.ExternalProofProbe; @@ -34,9 +40,11 @@ import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; import org.oneedtech.inspect.vc.probe.IssuanceProbe; import org.oneedtech.inspect.vc.probe.RevocationListProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; +import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; /** * An inspector for EndorsementCredential objects. @@ -44,8 +52,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ public class EndorsementInspector extends VCInspector implements SubInspector { + protected final List> userProbes; + protected > EndorsementInspector(B builder) { super(builder); + this.userProbes = ImmutableList.copyOf(builder.probes); } @Override @@ -127,8 +138,98 @@ public class EndorsementInspector extends VCInspector implements SubInspector { } @Override - public Report run(R resource) { - throw new IllegalStateException("must use #run(resource, map)"); + public Report run(Resource resource) { + super.check(resource); + + if (getBehavior(RESET_CACHES_ON_RUN) == TRUE) { + JsonSchemaCache.reset(); + CachingDocumentLoader.reset(); + } + + ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); + JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); + + RunContext ctx = new RunContext.Builder() + .put(this) + .put(resource) + .put(JACKSON_OBJECTMAPPER, mapper) + .put(JSONPATH_EVALUATOR, jsonPath) + .put(GENERATED_OBJECT_BUILDER, new VerifiableCredential.Builder()) + .build(); + + List accumulator = new ArrayList<>(); + int probeCount = 0; + + try { + // detect type (png, svg, json, jwt) and extract json data + probeCount++; + accumulator.add(new CredentialParseProbe().run(resource, ctx)); + if (broken(accumulator, true)) + return abort(ctx, accumulator, probeCount); + + // we expect the above to place a generated object in the context + VerifiableCredential endorsement = ctx.getGeneratedObject(VerifiableCredential.ID); + + //context and type properties + VerifiableCredential.Type type = Type.EndorsementCredential; + for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { + probeCount++; + accumulator.add(probe.run(endorsement.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //canonical schema and inline schema + SchemaKey schema = endorsement.getSchemaKey().orElseThrow(); + for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { + probeCount++; + accumulator.add(probe.run(endorsement.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //credentialSubject + probeCount++; + accumulator.add(new CredentialSubjectProbe().run(endorsement.getJson(), ctx)); + + //signatures, proofs + probeCount++; + if(endorsement.getProofType() == EXTERNAL){ + //The credential originally contained in a JWT, validate the jwt and external proof. + accumulator.add(new ExternalProofProbe().run(endorsement, ctx)); + } else { + accumulator.add(new EmbeddedProofProbe().run(endorsement, ctx)); + } + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //check refresh service if we are not already refreshed + probeCount++; + if(resource.getContext().get(REFRESHED) != TRUE) { + Optional newID = checkRefreshService(endorsement, ctx); + if(newID.isPresent()) { + return this.run( + new UriResource(new URI(newID.get())) + .setContext(new ResourceContext(REFRESHED, TRUE))); + } + } + + //revocation, expiration and issuance + for(Probe probe : List.of(new RevocationListProbe(), + new ExpirationProbe(), new IssuanceProbe())) { + probeCount++; + accumulator.add(probe.run(endorsement, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //finally, run any user-added probes + for(Probe probe : userProbes) { + probeCount++; + accumulator.add(probe.run(endorsement, ctx)); + } + + } catch (Exception e) { + accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); + } + + return new Report(ctx, new ReportItems(accumulator), probeCount); } public static class Builder extends VCInspector.Builder { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java index 1444c50..7d6990c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java @@ -52,4 +52,6 @@ public class CredentialParseProbe extends Probe { } } + public static final String ID = CredentialParseProbe.class.getSimpleName(); + } diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Endorsement30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Endorsement30Tests.java new file mode 100644 index 0000000..de3ed98 --- /dev/null +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Endorsement30Tests.java @@ -0,0 +1,48 @@ +package org.oneedtech.inspect.vc; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.oneedtech.inspect.test.Assertions.assertFatalCount; +import static org.oneedtech.inspect.test.Assertions.assertHasProbeID; +import static org.oneedtech.inspect.test.Assertions.assertInvalid; +import static org.oneedtech.inspect.test.Assertions.assertValid; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.oneedtech.inspect.core.Inspector.Behavior; +import org.oneedtech.inspect.core.report.Report; +import org.oneedtech.inspect.test.PrintHelper; +import org.oneedtech.inspect.vc.probe.CredentialParseProbe; + +public class Endorsement30Tests { + private static EndorsementInspector validator; + private static boolean verbose = false; + + @BeforeAll + static void setup() { + validator = new EndorsementInspector.Builder() + .set(Behavior.TEST_INCLUDE_SUCCESS, true) + .set(Behavior.VALIDATOR_FAIL_FAST, false) + .build(); + } + + @Test + void testEndorsementWithoutErrors() { + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.ENDORSEMENT_VALID.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertValid(report); + }); + } + + @Test + void testEndorsementWithErrors() { + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.ENDORSEMENT_ERR_SCHEMA_STATUS_REFRESH.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertFatalCount(report, 1); + // Parse probe fails because refresh points to invalid URL so nothing to parse + assertHasProbeID(report, CredentialParseProbe.ID, true); + }); + } +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index f95ce38..2efd81c 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -23,6 +23,8 @@ public class Samples { public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false); public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false); public final static Sample SIMPLE_JSON_ERR_CONTEXT = new Sample("ob30/simple-err-context.json", false); + public final static Sample ENDORSEMENT_ERR_SCHEMA_STATUS_REFRESH = new Sample("ob30/endorsement-err-schema-status-refresh.json", false); + public final static Sample ENDORSEMENT_VALID = new Sample("ob30/endorsement-valid.json", false); } public static final class PNG { public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true); diff --git a/inspector-vc/src/test/resources/ob30/endorsement-err-schema-status-refresh.json b/inspector-vc/src/test/resources/ob30/endorsement-err-schema-status-refresh.json new file mode 100644 index 0000000..3f5b184 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/endorsement-err-schema-status-refresh.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://1edtech.edu/endorsementcredential/3732", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "issuer": { + "id": "https://state.gov/issuers/565049", + "type": "Profile", + "name": "State Department of Education" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "expirationDate": "2030-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": "EndorsementSubject", + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://state.gov/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://state.gov/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://state.gov/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-12-15T16:53:56Z", + "verificationMethod": "https://state.gov/issuers/565049#z6MkmQ49FhpMN7V11B7Qzc6WC6Q3ymVW4xmHUsHBR2MWMMo1", + "proofPurpose": "assertionMethod", + "proofValue": "z36uHeAeKagDGaXMSqQX1eyKg4rFsWHzYLHxXfypkysPsvTtwN3Z8VZQtn7VQovX2GjkEWgGaW7hQ3UkNKbR84nUn" + } + ] + } \ No newline at end of file diff --git a/inspector-vc/src/test/resources/ob30/endorsement-valid.json b/inspector-vc/src/test/resources/ob30/endorsement-valid.json new file mode 100644 index 0000000..7c813dc --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/endorsement-valid.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://1edtech.edu/endorsementcredential/3732", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "issuer": { + "id": "https://state.gov/issuers/565049", + "type": "Profile", + "name": "State Department of Education" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "expirationDate": "2030-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": "EndorsementSubject", + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "proof": { + "type": "Ed25519Signature2020", + "created": "2022-12-20T20:56:10Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:z6MkgG59UvkBM1Lfv2D9JBu6yHn7aKurbwQgs1h5NCYJEk35", + "proofValue": "zLJF9oRBcpVQ9HUt8BRfPevvEGwL74bSyBKDpGzjNpb3KtMQ8JhQDPq3C4sqojneNz74YFWkjLSntC93iZGVscta" + } +} \ No newline at end of file