diff --git a/README.md b/README.md index c7f6eec2..5a24098f 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,23 @@ client.postStatement( )).block(); ``` +### Posting a Signed Statement + +Example: + +```java +client.postStatement( + r -> r.signedStatement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) + + .verb(Verb.ATTEMPTED) + + .activityObject(o -> o.id("https://example.com/activity/simplestatement") + .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))), + + keyPair.getPrivate())) + .block(); +``` + ### Posting Statements Example: diff --git a/pom.xml b/pom.xml index d1567ac6..42f78914 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 3.2.1 10.6.0 1.0.0 + 0.11.5 Berry Cloud Ltd @@ -273,6 +274,21 @@ xapi-client 1.1.4-SNAPSHOT + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + diff --git a/samples/pom.xml b/samples/pom.xml index bb70b9ec..e8dc9580 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -37,6 +37,7 @@ get-statement get-statement-with-attachment post-statement + post-signed-statement post-statement-with-attachment get-statements get-more-statements diff --git a/samples/post-signed-statement/pom.xml b/samples/post-signed-statement/pom.xml new file mode 100644 index 00000000..29e6f779 --- /dev/null +++ b/samples/post-signed-statement/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + dev.learning.xapi.samples + xapi-samples-build + 1.1.4-SNAPSHOT + + post-signed-statement + Post xAPI Signed Statement Sample + Post xAPI Signed Statement + + + dev.learning.xapi + xapi-client + + + dev.learning.xapi.samples + core + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + + + diff --git a/samples/post-signed-statement/src/main/java/dev/learning/xapi/samples/poststatement/PostSignedStatementApplication.java b/samples/post-signed-statement/src/main/java/dev/learning/xapi/samples/poststatement/PostSignedStatementApplication.java new file mode 100644 index 00000000..3d8a61f7 --- /dev/null +++ b/samples/post-signed-statement/src/main/java/dev/learning/xapi/samples/poststatement/PostSignedStatementApplication.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.samples.poststatement; + +import dev.learning.xapi.client.XapiClient; +import dev.learning.xapi.model.Verb; +import java.security.KeyPairGenerator; +import java.util.Locale; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.http.ResponseEntity; + +/** + * Sample using xAPI client to post a statement. + *

+ * See pom.xml for extra dependencies. + *

+ * + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) + */ +@SpringBootApplication +public class PostSignedStatementApplication implements CommandLineRunner { + + /** + * Default xAPI client. Properties are picked automatically from application.properties. + */ + @Autowired + private XapiClient client; + + public static void main(String[] args) { + SpringApplication.run(PostSignedStatementApplication.class, args).close(); + } + + @Override + public void run(String... args) throws Exception { + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // Post a statement + ResponseEntity< + UUID> response = + client + .postStatement(r -> r.signedStatement( + s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) + + .verb(Verb.ATTEMPTED) + + .activityObject(o -> o.id("https://example.com/activity/simplestatement") + .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))), + + keyPair.getPrivate())) + .block(); + + // Print the statementId of the newly created statement to the console + System.out.println("StatementId " + response.getBody()); + } + +} diff --git a/samples/post-signed-statement/src/main/resources/application.properties b/samples/post-signed-statement/src/main/resources/application.properties new file mode 100644 index 00000000..de20217a --- /dev/null +++ b/samples/post-signed-statement/src/main/resources/application.properties @@ -0,0 +1,3 @@ +xapi.client.username = admin +xapi.client.password = password +xapi.client.baseUrl = https://example.com/xapi/ diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementRequest.java b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementRequest.java index 1cafb1b6..17a7b6fe 100644 --- a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementRequest.java +++ b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementRequest.java @@ -5,6 +5,7 @@ package dev.learning.xapi.client; import dev.learning.xapi.model.Statement; +import java.security.PrivateKey; import java.util.Map; import java.util.function.Consumer; import lombok.Builder; @@ -80,6 +81,25 @@ public Builder statement(Statement statement) { return this; } + /** + * Consumer Builder for signed statement. + * + * @param statement The Consumer Builder for signed-statement + * + * @paraam privateKey a PrivateKey for signing the Statement + * + * @return This builder + * + * @see PostStatementRequest#statement + */ + public Builder signedStatement(Consumer statement, PrivateKey privateKey) { + + final var builder = Statement.builder(); + + statement.accept(builder); + + return statement(builder.signAndBuild(privateKey)); + } } } diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java index d06ab622..cb34f993 100644 --- a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java +++ b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java @@ -6,6 +6,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import dev.learning.xapi.model.About; import dev.learning.xapi.model.Activity; @@ -14,6 +15,8 @@ import dev.learning.xapi.model.StatementFormat; import dev.learning.xapi.model.Verb; import java.net.URI; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -364,6 +367,38 @@ void whenPostingStatementThenContentTypeHeaderIsApplicationJson() throws Interru assertThat(recordedRequest.getHeader("content-type"), is("application/json")); } + // Posting a Signed Statement + + @Test + void whenPostingSignedStatementThenExceptionIsThrown() throws NoSuchAlgorithmException { + + mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK") + .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]") + .setHeader("Content-Type", "application/json")); + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // When posting Signed Statement Then Exception Is Thrown + // ( Signing statements requires additional dependencies which are + // NOT included in these tests by default. ) + assertThrows(IllegalStateException.class, + () -> client.postStatement(r -> r + .signedStatement( + s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) + + .verb(Verb.ATTEMPTED) + + .activityObject(o -> o.id("https://example.com/activity/simplestatement") + .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))), + + keyPair.getPrivate()) + + .build())); + + } + // Get Voided Statement @Test diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml index 682c67d8..643641f9 100644 --- a/xapi-model/pom.xml +++ b/xapi-model/pom.xml @@ -10,6 +10,20 @@ xAPI Model learning.dev xAPI Model + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + true + + + io.jsonwebtoken + jjwt-jackson + true + com.fasterxml.jackson.core jackson-databind diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java index 4d6d5e78..fe8aea49 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java @@ -13,12 +13,20 @@ import dev.learning.xapi.model.validation.constraints.ValidStatementRevision; import dev.learning.xapi.model.validation.constraints.ValidStatementVerb; import dev.learning.xapi.model.validation.constraints.Variant; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.UnknownClassException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import java.net.URI; +import java.security.PrivateKey; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.UUID; import java.util.function.Consumer; import lombok.Builder; @@ -127,6 +135,65 @@ public static class Builder { // This static class extends the lombok builder. + /** + * Special build method for signing and building a {@link Statement}. + *

+ * An signature attachment is automatically added to the Statement's attachments. + *

+ * + * @param privateKey a {@link PrivateKey} for signing the {@link Statement}. + * + * @return an immutable, signed {@link Statement} object. + * + * @see + * Signed Statements + */ + public Statement signAndBuild(PrivateKey privateKey) { + final Map claims = new HashMap<>(); + + // Put only the significant properties into the signature payload + // https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#statement-comparision-requirements + claims.put("actor", this.actor); + claims.put("verb", this.verb); + claims.put("object", this.object); + claims.put("result", this.result); + claims.put("context", this.context); + + try { + final var token = Jwts.builder().setClaims(claims) + .signWith(privateKey, SignatureAlgorithm.RS512).compact(); + + addAttachment(a -> a.usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) + + .addDisplay(Locale.ENGLISH, "JSW signature") + + .content(token) + + .length(token.length()) + + .contentType("application/octet-stream")); + + } catch (final UnknownClassException e) { + throw new IllegalStateException(""" + + Statement cannot be signed, because an optional dependency was NOT provided. + Please add the following dependencies into your project: + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + + """, e); + } + + return build(); + } + /** * Consumer Builder for agent. * diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java index 7b0bd86d..b83125a5 100644 --- a/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java +++ b/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java @@ -9,7 +9,10 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; @@ -19,11 +22,15 @@ import dev.learning.xapi.jackson.XapiStrictNullValuesModule; import dev.learning.xapi.jackson.XapiStrictObjectTypeModule; import dev.learning.xapi.jackson.XapiStrictTimestampModule; +import io.jsonwebtoken.Jwts; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Collections; import java.util.LinkedHashMap; @@ -41,6 +48,7 @@ * @author Lukáš Sahula * @author Martin Myslik * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) */ @DisplayName("Statement tests") class StatementTests { @@ -1286,4 +1294,336 @@ void whenDeserializingMinimalStatementWithAllTheModulesThenNoExceptionIsThrown() } + @Test + void whenSigningStatementThenSignatureIsAddedAsAttachment() throws NoSuchAlgorithmException { + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // When Signing Statement + final var extensions = new LinkedHashMap(); + extensions.put(URI.create("http://name"), "Kilby"); + + final var account = Account.builder() + + .homePage(URI.create("https://example.com")) + + .name("13936749") + + .build(); + + + final var statement = Statement.builder() + + .id(UUID.fromString("4b9175ba-367d-4b93-990b-34d4180039f1")) + + .actor(a -> a.name("A N Other")) + + .verb(v -> v.id(URI.create("http://example.com/xapi/verbs#sent-a-statement")) + .addDisplay(Locale.US, "attended")) + + .result(r -> r.success(true).completion(true).response("Response").duration("P1D")) + + .context(c -> c + + .registration(UUID.fromString("ec531277-b57b-4c15-8d91-d292c5b2b8f7")) + + .agentInstructor(a -> a.name("A N Other").account(account)) + + .team(t -> t.name("Team").mbox("mailto:team@example.com")) + + .platform("Example virtual meeting software") + + .language(Locale.ENGLISH) + + .statementReference(s -> s.id(UUID.fromString("6690e6c9-3ef0-4ed3-8b37-7f3964730bee"))) + + ) + + .timestamp(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .stored(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .agentAuthority(a -> a.account(account)) + + .activityObject(a -> a.id("http://www.example.com/meetings/occurances/34534") + + .definition(d -> d.addName(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .addDescription(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .type(URI.create("http://adlnet.gov/expapi/activities/meeting")) + + .moreInfo(URI.create("http://virtualmeeting.example.com/345256")) + + .extensions(extensions))) + + .version("1.0.0") + + .signAndBuild(keyPair.getPrivate()); + + // Then Signature is Added As Attachment + assertThat(statement.getAttachments(), hasSize(1)); + + } + + @Test + void whenSigningStatementWithAttachmentThenSignatureIsAddedAsAttachment() + throws NoSuchAlgorithmException { + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // When Signing Statement + final var extensions = new LinkedHashMap(); + extensions.put(URI.create("http://name"), "Kilby"); + + final var attachment = Attachment.builder().usageType(URI.create("http://example.com")) + .fileUrl(URI.create("http://example.com")) + + .addDisplay(Locale.ENGLISH, "value") + + .addDescription(Locale.ENGLISH, "value") + + .length(123) + + .sha2("123") + + .contentType("file") + + .build(); + + final var account = Account.builder() + + .homePage(URI.create("https://example.com")) + + .name("13936749") + + .build(); + + + final var statement = Statement.builder() + + .id(UUID.fromString("4b9175ba-367d-4b93-990b-34d4180039f1")) + + .actor(a -> a.name("A N Other")) + + .verb(v -> v.id(URI.create("http://example.com/xapi/verbs#sent-a-statement")) + .addDisplay(Locale.US, "attended")) + + .result(r -> r.success(true).completion(true).response("Response").duration("P1D")) + + .context(c -> c + + .registration(UUID.fromString("ec531277-b57b-4c15-8d91-d292c5b2b8f7")) + + .agentInstructor(a -> a.name("A N Other").account(account)) + + .team(t -> t.name("Team").mbox("mailto:team@example.com")) + + .platform("Example virtual meeting software") + + .language(Locale.ENGLISH) + + .statementReference(s -> s.id(UUID.fromString("6690e6c9-3ef0-4ed3-8b37-7f3964730bee"))) + + ) + + .timestamp(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .stored(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .agentAuthority(a -> a.account(account)) + + .activityObject(a -> a.id("http://www.example.com/meetings/occurances/34534") + + .definition(d -> d.addName(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .addDescription(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .type(URI.create("http://adlnet.gov/expapi/activities/meeting")) + + .moreInfo(URI.create("http://virtualmeeting.example.com/345256")) + + .extensions(extensions))) + + .addAttachment(attachment) + + .version("1.0.0") + + .signAndBuild(keyPair.getPrivate()); + + // Then Signature is Added As Attachment + assertThat(statement.getAttachments(), hasSize(2)); + + } + + @Test + void whenSigningStatementThenSignatureIsExpected() throws NoSuchAlgorithmException { + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // When Signing Statement + final var extensions = new LinkedHashMap(); + extensions.put(URI.create("http://name"), "Kilby"); + + final var account = Account.builder() + + .homePage(URI.create("https://example.com")) + + .name("13936749") + + .build(); + + + final var statement = Statement.builder() + + .id(UUID.fromString("4b9175ba-367d-4b93-990b-34d4180039f1")) + + .actor(a -> a.name("A N Other")) + + .verb(v -> v.id(URI.create("http://example.com/xapi/verbs#sent-a-statement")) + .addDisplay(Locale.US, "attended")) + + .result(r -> r.success(true).completion(true).response("Response").duration("P1D")) + + .context(c -> c + + .registration(UUID.fromString("ec531277-b57b-4c15-8d91-d292c5b2b8f7")) + + .agentInstructor(a -> a.name("A N Other").account(account)) + + .team(t -> t.name("Team").mbox("mailto:team@example.com")) + + .platform("Example virtual meeting software") + + .language(Locale.ENGLISH) + + .statementReference(s -> s.id(UUID.fromString("6690e6c9-3ef0-4ed3-8b37-7f3964730bee"))) + + ) + + .timestamp(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .stored(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .agentAuthority(a -> a.account(account)) + + .activityObject(a -> a.id("http://www.example.com/meetings/occurances/34534") + + .definition(d -> d.addName(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .addDescription(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .type(URI.create("http://adlnet.gov/expapi/activities/meeting")) + + .moreInfo(URI.create("http://virtualmeeting.example.com/345256")) + + .extensions(extensions))) + + .version("1.0.0") + + .signAndBuild(keyPair.getPrivate()); + + // Then Signature is Expected + assertThat(new String(statement.getAttachments().get(0).getContent(), StandardCharsets.UTF_8), + startsWith( + "eyJhbGciOiJSUzUxMiJ9.eyJhY3RvciI6eyJuYW1lIjoiQSBOIE90aGVyIn0sInJlc3VsdCI6eyJzdWNjZXNzIjp0cnVlLCJjb21wbGV0aW9uIjp0cnVlLCJyZXNwb25zZSI6IlJlc3BvbnNlIiwiZHVyYXRpb24iOiJQMUQifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS94YXBpL3ZlcmJzI3NlbnQtYS1zdGF0ZW1lbnQiLCJkaXNwbGF5Ijp7ImVuLVVTIjoiYXR0ZW5kZWQifX0sImNvbnRleHQiOnsicmVnaXN0cmF0aW9uIjoiZWM1MzEyNzctYjU3Yi00YzE1LThkOTEtZDI5MmM1YjJiOGY3IiwiaW5zdHJ1Y3RvciI6eyJvYmplY3RUeXBlIjoiQWdlbnQiLCJuYW1lIjoiQSBOIE90aGVyIiwiYWNjb3VudCI6eyJob21lUGFnZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJuYW1lIjoiMTM5MzY3NDkifX0sInRlYW0iOnsib2JqZWN0VHlwZSI6Ikdyb3VwIiwibmFtZSI6IlRlYW0iLCJtYm94IjoibWFpbHRvOnRlYW1AZXhhbXBsZS5jb20ifSwicGxhdGZvcm0iOiJFeGFtcGxlIHZpcnR1YWwgbWVldGluZyBzb2Z0d2FyZSIsImxhbmd1YWdlIjoiZW4iLCJzdGF0ZW1lbnQiOnsib2JqZWN0VHlwZSI6IlN0YXRlbWVudFJlZiIsImlkIjoiNjY5MGU2YzktM2VmMC00ZWQzLThiMzctN2YzOTY0NzMwYmVlIn19LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vd3d3LmV4YW1wbGUuY29tL21lZXRpbmdzL29jY3VyYW5jZXMvMzQ1MzQiLCJkZWZpbml0aW9uIjp7Im5hbWUiOnsiZW4tR0IiOiJBIHNpbXBsZSBFeHBlcmllbmNlIEFQSSBzdGF0ZW1lbnQuIE5vdGUgdGhhdCB0aGUgTFJTIGRvZXMgbm90IG5lZWQgdG8gaGF2ZSBhbnkgcHJpb3IgaW5mb3JtYXRpb24gYWJvdXQgdGhlIEFjdG9yIChsZWFybmVyKSwgdGhlIHZlcmIsIG9yIHRoZSBBY3Rpdml0eS9vYmplY3QuIn0sImRlc2NyaXB0aW9uIjp7ImVuLUdCIjoiQSBzaW1wbGUgRXhwZXJpZW5jZSBBUEkgc3RhdGVtZW50LiBOb3RlIHRoYXQgdGhlIExSUyBkb2VzIG5vdCBuZWVkIHRvIGhhdmUgYW55IHByaW9yIGluZm9ybWF0aW9uIGFib3V0IHRoZSBBY3RvciAobGVhcm5lciksIHRoZSB2ZXJiLCBvciB0aGUgQWN0aXZpdHkvb2JqZWN0LiJ9LCJ0eXBlIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL2FjdGl2aXRpZXMvbWVldGluZyIsIm1vcmVJbmZvIjoiaHR0cDovL3ZpcnR1YWxtZWV0aW5nLmV4YW1wbGUuY29tLzM0NTI1NiIsImV4dGVuc2lvbnMiOnsiaHR0cDovL25hbWUiOiJLaWxieSJ9fX19.")); + + } + + @Test + void whenSigningStatementThenSignatureIsValid() + throws NoSuchAlgorithmException, JsonMappingException, JsonProcessingException { + + final var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final var keyPair = keyPairGenerator.generateKeyPair(); + + // When Signing Statement + final var extensions = new LinkedHashMap(); + extensions.put(URI.create("http://name"), "Kilby"); + + final var account = Account.builder() + + .homePage(URI.create("https://example.com")) + + .name("13936749") + + .build(); + + + final var statement = Statement.builder() + + .id(UUID.fromString("4b9175ba-367d-4b93-990b-34d4180039f1")) + + .actor(a -> a.name("A N Other")) + + .verb(v -> v.id(URI.create("http://example.com/xapi/verbs#sent-a-statement")) + .addDisplay(Locale.US, "attended")) + + .result(r -> r.success(true).completion(true).response("Response").duration("P1D")) + + .context(c -> c + + .registration(UUID.fromString("ec531277-b57b-4c15-8d91-d292c5b2b8f7")) + + .agentInstructor(a -> a.name("A N Other").account(account)) + + .team(t -> t.name("Team").mbox("mailto:team@example.com")) + + .platform("Example virtual meeting software") + + .language(Locale.ENGLISH) + + .statementReference(s -> s.id(UUID.fromString("6690e6c9-3ef0-4ed3-8b37-7f3964730bee"))) + + ) + + .timestamp(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .stored(Instant.parse("2013-05-18T05:32:34.804+00:00")) + + .agentAuthority(a -> a.account(account)) + + .activityObject(a -> a.id("http://www.example.com/meetings/occurances/34534") + + .definition(d -> d.addName(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .addDescription(Locale.UK, + "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.") + + .type(URI.create("http://adlnet.gov/expapi/activities/meeting")) + + .moreInfo(URI.create("http://virtualmeeting.example.com/345256")) + + .extensions(extensions))) + + .version("1.0.0") + + .signAndBuild(keyPair.getPrivate()); + + // Then Signature is Valid + final var body = Jwts.parserBuilder().setSigningKey(keyPair.getPublic()).build() + .parseClaimsJws( + new String(statement.getAttachments().get(0).getContent(), StandardCharsets.UTF_8)) + .getBody(); + + final var bodyStatement = + objectMapper.readValue(objectMapper.writeValueAsString(body), Statement.class); + + assertThat(bodyStatement, is(statement)); + + } }