aboutsummaryrefslogtreecommitdiffstats
path: root/hosted-api
diff options
context:
space:
mode:
authorJon Marius Venstad <jvenstad@yahoo-inc.com>2019-04-29 15:31:38 +0200
committerJon Marius Venstad <jvenstad@yahoo-inc.com>2019-04-29 15:31:38 +0200
commitd97a74b62d84a2a5820c390197eb50fd0f3c1ba4 (patch)
tree3a24f55e22048f454a2b26615c85f63f458aff54 /hosted-api
parente74ed697f8e4e60e9b86f9472c2161a32d74658f (diff)
Add HTTP request signer and verifier
Diffstat (limited to 'hosted-api')
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java97
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java46
2 files changed, 143 insertions, 0 deletions
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java
new file mode 100644
index 00000000000..3f0913c2863
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java
@@ -0,0 +1,97 @@
+package ai.vespa.hosted.api;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.http.HttpRequest;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Key;
+import java.time.Clock;
+import java.util.Base64;
+import java.util.function.Supplier;
+
+import static ai.vespa.hosted.api.Signatures.encrypted;
+import static ai.vespa.hosted.api.Signatures.sha256Digest;
+
+/**
+ * Signs HTTP request headers using a private key, for verification by the indicated public key.
+ *
+ * @author jonmv
+ */
+public class RequestSigner {
+
+ private final Key privateKey;
+ private final String keyId;
+ private final Clock clock;
+
+ /** Creates a new request signer from the PEM encoded RSA key at the specified path, owned by the given application. */
+ public RequestSigner(String pemPrivateKey, String keyId) {
+ this(pemPrivateKey, keyId, Clock.systemUTC());
+ }
+
+ /** Creates a new request signer with a custom clock. */
+ RequestSigner(String pemPrivateKey, String keyId, Clock clock) {
+ this.privateKey = Signatures.parsePrivatePemPkcs8RsaKey(pemPrivateKey);
+ this.keyId = keyId;
+ this.clock = clock;
+ }
+
+ /**
+ * Completes, signs and returns the given request builder and data.<br>
+ * <br>
+ * The request builder's method and data are set to the given arguments, and a hash of the
+ * content is computed and added to a header, together with other meta data, like the URI
+ * of the request, the current UTC time, and the id of the public key which shall be used to
+ * verify this signature.
+ * Finally, a signature is computed from these fields, based on the private key of this, and
+ * added to the request as another header.
+ */
+ public HttpRequest signed(HttpRequest.Builder request, Method method, Supplier<InputStream> data) {
+ String timestamp = clock.instant().toString();
+ String contentHash = Base64.getEncoder().encodeToString(sha256Digest(data::get));
+ byte[] canonicalMessage = Signatures.canonicalMessageOf(method.name(), request.copy().build().uri(), timestamp, contentHash);
+ String signature = Base64.getEncoder().encodeToString(encrypted(canonicalMessage, privateKey));
+
+ request.setHeader("X-Timestamp", timestamp);
+ request.setHeader("X-Content-Hash", contentHash);
+ request.setHeader("X-Key-Id", keyId);
+ request.setHeader("X-Authorization", signature);
+
+ request.method(method.name(), HttpRequest.BodyPublishers.ofInputStream(data));
+ return request.build();
+ }
+
+ /**
+ * Completes, signs and returns the given request builder and data.
+ *
+ * This sets the Content-Type header from the given streamer, and returns
+ * {@code signed(request, method, streamer::data)}.
+ */
+ public HttpRequest signed(HttpRequest.Builder request, Method method, MultiPartStreamer streamer) {
+ request.setHeader("Content-Type", streamer.contentType());
+ return signed(request, method, streamer::data);
+ }
+
+ /**
+ * Completes, signs and returns the given request builder.<br>
+ * <br>
+ * This is simply a convenience for<br>
+ * {@code signed(request, method, () -> new ByteArrayInputStream(data))}.
+ */
+ public HttpRequest signed(HttpRequest.Builder request, Method method, byte[] data) {
+ return signed(request, method, () -> new ByteArrayInputStream(data));
+ }
+
+ /**
+ * Completes, signs and returns the given request builder.<br>
+ * <br>
+ * This sets the data of the request to be empty, and returns <br>
+ * {@code signed(request, method, InputStream::nullInputStream)}.
+ */
+ public HttpRequest signed(HttpRequest.Builder request, Method method) {
+ return signed(request, method, InputStream::nullInputStream);
+ }
+
+}
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java
new file mode 100644
index 00000000000..53a464058c1
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java
@@ -0,0 +1,46 @@
+package ai.vespa.hosted.api;
+
+import java.net.URI;
+import java.security.Key;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Base64;
+
+/**
+ * Verifies that signed HTTP requests match the indicated public key.
+ *
+ * @author jonmv
+ */
+public class RequestVerifier {
+
+ private final Key publicKey;
+ private final Clock clock;
+
+ public RequestVerifier(String pemPublicKey) {
+ this(pemPublicKey, Clock.systemUTC());
+ }
+
+ RequestVerifier(String pemPublicKey, Clock clock) {
+ this.publicKey = Signatures.parsePublicPemX509RsaKey(pemPublicKey);
+ this.clock = clock;
+ }
+
+ /** Returns whether the given data is a valid request now, as dictated by timestamp and the decryption key of this. */
+ public boolean verify(Method method, URI requestUri, String timestamp, String contentHash, String signature) {
+ try {
+ Instant now = clock.instant(), then = Instant.parse(timestamp);
+ if (Duration.between(now, then).abs().compareTo(Duration.ofMinutes(5)) > 0)
+ return false; // Timestamp mismatch between sender and receiver of more than 5 minutes is not acceptable.
+
+ byte[] canonicalMessage = Signatures.canonicalMessageOf(method.name(), requestUri, timestamp, contentHash);
+ byte[] decrypted = Signatures.decrypted(Base64.getDecoder().decode(signature), publicKey);
+ return Arrays.equals(canonicalMessage, decrypted);
+ }
+ catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+}