Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.opentdf.platform.sdk;

import java.util.Optional;

/**
* Why an assertion, or the manifest that carries it, was rejected. Every
* {@link SDK.AssertionFailureException} carries exactly one of these.
* <p>
* The {@link #code() codes} are the stable strings from Section 9 of the ZTDF assertions
* specification. They are shared with the other OpenTDF SDKs and appear in error messages as
* {@code reason=<code>}, so tools can match on them.
*/
public enum AssertionFailureReason {
/** The manifest is not I-JSON: invalid UTF-8, a lone surrogate, a noncharacter, or an out-of-range number. */
INVALID_JSON("invalid-json"),
/** An object in the manifest has two members with the same name, compared after decoding escapes. */
DUPLICATE_MEMBER("duplicate-member"),
/** An object has a case variant of a modelled member name, such as {@code ID} or {@code Assertions}. */
AMBIGUOUS_MEMBER("ambiguous-member"),
/** The assertion's {@code id} is missing, empty, or not a JSON string. */
INVALID_ID("invalid-id"),
/** The assertion's {@code statement} is not an object, or its {@code value} is neither a string nor an object. */
INVALID_STATEMENT("invalid-statement"),
/** The {@code binding} is missing or malformed, has both binding forms or neither, or its JWS is malformed. */
INVALID_BINDING("invalid-binding"),
/** The binding uses an unknown {@code method}, or is a binding object this SDK does not support. */
UNSUPPORTED_BINDING("unsupported-binding"),
/** The JWS {@code alg} is missing, {@code none}, or not supported by this SDK. */
UNSUPPORTED_ALGORITHM("unsupported-algorithm"),
/** The JWS {@code alg} differs from the algorithm of the selected verification key. */
ALGORITHM_MISMATCH("algorithm-mismatch"),
/** An embedded {@code jwk} or {@code x5c} is symmetric, private, malformed, contradictory, or present in HMAC mode. */
INVALID_EMBEDDED_KEY("invalid-embedded-key"),
/** An embedded {@code jwk} or {@code x5c} does not match the selected verification key. */
SIGNER_NOT_PERMITTED("signer-not-permitted"),
/** The binding signature does not verify under the selected key. */
SIGNATURE_INVALID("signature-invalid"),
/** The signed {@code assertionHash} or {@code assertionSig} does not match the recomputed value. */
BINDING_MISMATCH("binding-mismatch"),
/** A configured verification key is empty, of the wrong type for its algorithm, or otherwise unusable. */
CONFIGURATION_ERROR("configuration-error");

private final String code;

AssertionFailureReason(String code) {
this.code = code;
}

/** The stable reason string, for example {@code signer-not-permitted}. */
public String code() {
return code;
}

/** The reason with the given {@link #code() code}, if there is one. */
public static Optional<AssertionFailureReason> fromCode(String code) {
for (AssertionFailureReason reason : values()) {
if (reason.code.equals(code)) {
return Optional.of(reason);
}
}
return Optional.empty();
}

@Override
public String toString() {
return code;
}
}
38 changes: 38 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/AssertionJson.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.opentdf.platform.sdk;

import com.google.gson.stream.JsonToken;

import java.io.IOException;
import java.io.Reader;

/**
* Parsing of assertion wire objects, as Section 3 of the ZTDF assertions specification defines
* it.
*/
final class AssertionJson {

private AssertionJson() {
}

/**
* Applies only the manifest-level checks of Section 3 to a whole manifest, without building
* the typed manifest. Malformed JSON is reported as {@code invalid-json} here; the manifest
* reader proper reports it as a malformed TDF.
*
* @throws SDK.AssertionFailureException on the first failure in document order
*/
static void checkManifest(Reader manifestJson) {
try (StrictJsonReader reader = new StrictJsonReader(manifestJson, StrictJsonReader.Mode.MANIFEST)) {
reader.skipValue();
if (reader.peek() != JsonToken.END_DOCUMENT) {
throw invalidJson("unexpected content after the manifest");
}
} catch (IOException | IllegalStateException | NumberFormatException e) {
throw invalidJson("the manifest is not valid JSON");
}
}

private static SDK.AssertionFailureException invalidJson(String detail) {
return new SDK.AssertionFailureException(null, AssertionFailureReason.INVALID_JSON, detail);
}
}
43 changes: 38 additions & 5 deletions sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
Expand Down Expand Up @@ -47,6 +48,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Type;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -1179,17 +1181,29 @@ protected static Manifest readManifest(String manifestJson) {
/**
* Parses a manifest without materializing it as a {@link String}, which a
* manifest with tens of millions of segments cannot be.
* <p>
* The JSON must be strict (RFC 8259) and must meet the JSON profile of the ZTDF assertions
* specification, Section 3, which this checks in the same single pass: no duplicate member
* names in any object, no case variants of modelled member names (such as
* {@code Assertions} or an assertion's {@code ID}), and I-JSON content in every assertion.
* Those checks apply whether or not assertions are later verified.
*
* @throws SDK.AssertionFailureException if the manifest breaks that profile, or if
* {@code manifestJson} reports undecodable input
*/
protected static Manifest readManifest(Reader manifestJson) {
FailureTrackingReader reader = new FailureTrackingReader(manifestJson);
StrictJsonReader jsonReader = new StrictJsonReader(reader, StrictJsonReader.Mode.MANIFEST);
Manifest result;
try {
result = gson.fromJson(reader, Manifest.class);
} catch (JsonParseException e) {
if (reader.failure != null) {
throw new SDKException("error reading manifest from zip file", reader.failure);
result = gson.fromJson(jsonReader, TypeToken.get(Manifest.class));
if (result != null && jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonSyntaxException("unexpected content after the manifest object");
}
throw e;
} catch (JsonParseException e) {
throw readFailure(reader, e);
} catch (IOException e) {
throw readFailure(reader, new JsonSyntaxException(e));
}
if (result == null) {
throw new IllegalArgumentException("Manifest is empty");
Expand All @@ -1198,6 +1212,25 @@ protected static Manifest readManifest(Reader manifestJson) {
return result;
}

/**
* Applies only the manifest-level parsing rules of the assertions specification, Section 3,
* without building the typed manifest. Malformed JSON is reported as {@code invalid-json}.
*/
static void checkManifestJson(String manifestJson) {
AssertionJson.checkManifest(new StringReader(manifestJson));
}

private static RuntimeException readFailure(FailureTrackingReader reader, JsonParseException e) {
if (reader.failure instanceof CharacterCodingException) {
return new SDK.AssertionFailureException(null, AssertionFailureReason.INVALID_JSON,
"the manifest is not valid UTF-8");
}
if (reader.failure != null) {
return new SDKException("error reading manifest from zip file", reader.failure);
}
return e;
}

private static final class FailureTrackingReader extends Reader {
private final Reader delegate;
private IOException failure;
Expand Down
107 changes: 107 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -224,6 +225,9 @@
* @param tdfBytes A SeekableByteChannel containing the TDF data
* @return The parsed {@link Manifest} object
* @throws MalformedTDFException if the bytes are not a well formed TDF or its manifest is invalid
* @throws AssertionFailureException if the manifest is not valid UTF-8, or breaks the JSON
* profile the assertions specification requires (for example duplicate member names).
* This does not verify the assertions themselves.
* @throws SDKException if an SDK-specific error occurs
* @throws IOException if an I/O error occurs
*/
Expand Down Expand Up @@ -560,6 +564,109 @@
public AssertionException(String errorMessage, String id) {
super("assertion id: "+ id + "; " + errorMessage);
}

/** For subclasses that format the whole message themselves. */
protected AssertionException(String errorMessage) {
super(errorMessage);
}
}

/**
* {@link AssertionFailureException} is thrown when an assertion fails verification, or when
* the manifest that carries the assertions cannot be parsed under the JSON profile the
* assertions specification requires (duplicate member names, case variants of modelled
* member names, or non-I-JSON content in an assertion).
* <p>
* It carries the exact {@code id} of the failing assertion when one could be read, and one
* {@link AssertionFailureReason}. The message contains {@code reason=<code>}, and
* {@code id: <id>} when the id is known.
*/
public static class AssertionFailureException extends AssertionException {

Check warning on line 584 in sdk/src/main/java/io/opentdf/platform/sdk/SDK.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This class has 7 parents which is greater than 5 authorized.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaHAzf_H4KxBVUKC8zPE&open=AaHAzf_H4KxBVUKC8zPE&pullRequest=436
private final String assertionId;
private final AssertionFailureReason reason;

public AssertionFailureException(String assertionId, AssertionFailureReason reason, String detail) {
super(formatMessage(assertionId, reason, detail));
this.assertionId = assertionId;
this.reason = Objects.requireNonNull(reason, "reason");
}

/**
* The exact {@code id} of the failing assertion, or {@code null} when the failure is not
* tied to an assertion with a valid id (for example a duplicate member elsewhere in the
* manifest, or an assertion whose {@code id} is itself invalid).
*/
public String getAssertionId() {
return assertionId;
}

public AssertionFailureReason getReason() {
return reason;
}

/** Rendered ids are cut to this many code points, then marked with an ellipsis. */
static final int MAX_RENDERED_ID = 256;
/** Details quote manifest content, such as member names and paths, so they are cut too. */
static final int MAX_RENDERED_DETAIL = 512;

private static String formatMessage(String assertionId, AssertionFailureReason reason, String detail) {
StringBuilder message = new StringBuilder("tdf: issue verifying assertions");
if (assertionId != null) {
message.append(", id: ");
render(assertionId, MAX_RENDERED_ID, message);
}
message.append(", reason=").append(reason.code());
if (detail != null && !detail.isEmpty()) {
message.append(": ");
render(detail, MAX_RENDERED_DETAIL, message);
}
return message.toString();
}

/**
* Appends untrusted manifest text for a message: {@code \} and the control characters
* U+0000 to U+001F and U+007F to U+009F escaped as in JSON, and at most
* {@code maxCodePoints} code points of the text, followed by an ellipsis when it is
* longer. An id read from a manifest then cannot start a new log line or end the message.
*/
static void render(String text, int maxCodePoints, StringBuilder out) {
int codePoints = 0;
for (int i = 0; i < text.length(); ) {
if (codePoints == maxCodePoints) {
out.append('\u2026');
return;
}
int codePoint = text.codePointAt(i);
i += Character.charCount(codePoint);

Check warning on line 640 in sdk/src/main/java/io/opentdf/platform/sdk/SDK.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code in order to not assign to this loop counter from within the loop body.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaHAzf_H4KxBVUKC8zPF&open=AaHAzf_H4KxBVUKC8zPF&pullRequest=436
codePoints++;
switch (codePoint) {
case '\\':
out.append("\\\\");
break;
case '\b':
out.append("\\b");
break;
case '\t':
out.append("\\t");
break;
case '\n':
out.append("\\n");
break;
case '\f':
out.append("\\f");
break;
case '\r':
out.append("\\r");
break;
default:
if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint <= 0x9f)) {
out.append(String.format("\\u%04x", codePoint));
} else {
out.appendCodePoint(codePoint);
}
}
}
}
}

/**
Expand Down
Loading
Loading