diff options
author | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-04-29 15:31:38 +0200 |
---|---|---|
committer | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-04-29 15:31:38 +0200 |
commit | d97a74b62d84a2a5820c390197eb50fd0f3c1ba4 (patch) | |
tree | 3a24f55e22048f454a2b26615c85f63f458aff54 /hosted-api/src | |
parent | e74ed697f8e4e60e9b86f9472c2161a32d74658f (diff) |
Add HTTP request signer and verifier
Diffstat (limited to 'hosted-api/src')
-rw-r--r-- | hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java | 97 | ||||
-rw-r--r-- | hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java | 46 |
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; + } + } + +} |