summaryrefslogtreecommitdiffstats
path: root/security-utils/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'security-utils/src/main/java/com')
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java19
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java155
-rw-r--r--security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java8
3 files changed, 173 insertions, 9 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
index d570cd799cc..99d07465812 100644
--- a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
+++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
@@ -20,11 +20,14 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public static final int CURRENT_TOKEN_VERSION = 2;
/** Encryption context for v{1,2} tokens is always a 32-byte X25519 public key */
public static final int MAX_ENC_CONTEXT_LENGTH = 255;
+ // Expected max decoded size for v1 is 3 + 255 + 32 + 32 = 322. For simplicity, round this
+ // up to 512 to effectively not have to care about the overhead of any reasonably chosen encoding.
+ public static final int MAX_TOKEN_STRING_LENGTH = 512;
public SealedSharedKey {
if (enc.length > MAX_ENC_CONTEXT_LENGTH) {
throw new IllegalArgumentException("Encryption context is too large to be encoded (max is %d, got %d)"
- .formatted(MAX_ENC_CONTEXT_LENGTH, enc.length));
+ .formatted(MAX_ENC_CONTEXT_LENGTH, enc.length));
}
}
@@ -33,6 +36,10 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
* reconstruct the SealedSharedKey instance when passed verbatim to fromTokenString().
*/
public String toTokenString() {
+ return Base62.codec().encode(toSerializedBytes());
+ }
+
+ byte[] toSerializedBytes() {
byte[] keyIdBytes = keyId.asBytes();
// u8 token version || u8 length(key id) || key id || u8 length(enc) || enc || ciphertext
ByteBuffer encoded = ByteBuffer.allocate(1 + 1 + keyIdBytes.length + 1 + enc.length + ciphertext.length);
@@ -46,7 +53,7 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
byte[] encBytes = new byte[encoded.remaining()];
encoded.get(encBytes);
- return Base62.codec().encode(encBytes);
+ return encBytes;
}
/**
@@ -56,6 +63,10 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public static SealedSharedKey fromTokenString(String tokenString) {
verifyInputTokenStringNotTooLarge(tokenString);
byte[] rawTokenBytes = Base62.codec().decode(tokenString);
+ return fromSerializedBytes(rawTokenBytes);
+ }
+
+ static SealedSharedKey fromSerializedBytes(byte[] rawTokenBytes) {
if (rawTokenBytes.length < 1) {
throw new IllegalArgumentException("Decoded token too small to contain a version");
}
@@ -81,9 +92,7 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public int tokenVersion() { return version; }
private static void verifyInputTokenStringNotTooLarge(String tokenString) {
- // Expected max decoded size for v1 is 3 + 255 + 32 + 32 = 322. For simplicity, round this
- // up to 512 to effectively not have to care about the overhead of any reasonably chosen encoding.
- if (tokenString.length() > 512) {
+ if (tokenString.length() > MAX_TOKEN_STRING_LENGTH) {
throw new IllegalArgumentException("Token string is too long to possibly be a valid token");
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java
new file mode 100644
index 00000000000..6e79b86d832
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java
@@ -0,0 +1,155 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.util.Arrays;
+
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.interfaces.XECPublicKey;
+import java.util.Optional;
+
+/**
+ * <p>Delegated resealing protocol for getting access to a shared secret key of a token
+ * whose private key we do not possess.</p>
+ *
+ * <p>The primary benefit of the interactive resealing protocol is that none of the data
+ * exchanged can reveal anything about the underlying sealed secret itself.</p>
+ *
+ * <p>Note that neither resealing requests nor responses are authenticated (this is a property
+ * inherited from the sealed shared key tokens themselves). It is assumed that an attacker
+ * can <em>observe</em> all requests and responses in transit, but cannot modify them.</p>
+ *
+ * <h2>Protocol details</h2>
+ *
+ * <p>Decryptor (requester):</p>
+ * <ol>
+ * <li>Create a resealing session instance that maintains an ephemeral X25519 key pair that
+ * is valid only for the lifetime of the session.</li>
+ * <li>Create a resealing request for a token <em>T</em>. The session emits a Base62-encoded
+ * binary representation of the tuple <em>&lt;ephemeral public key, T&gt;</em>.</li>
+ * <li>Send the request to the private key holder. The session must be kept alive until the
+ * response is received, or the ephemeral private key associated with the public key will
+ * be irrevocably lost.</li>
+ * </ol>
+ * <p>Private key holder (re-sealer):</p>
+ * <ol>
+ * <li>Decode Base62-encoded request into tuple <em>&lt;ephemeral public key, T&gt;</em>.</li>
+ * <li>Look up the correct private key from the key ID contained in token <em>T</em>.</li>
+ * <li>Reseal token <em>T</em> for the requested ephemeral public key using the correct private key.</li>
+ * <li>Return resealed token <em>T<sub>R</sub></em> to requester.</li>
+ * </ol>
+ * <p>Decryptor (requester):</p>
+ * <ol>
+ * <li>Decrypt token <em>T<sub>R</sub></em> using ephemeral private key.</li>
+ * <li>Use secret key in token to decrypt the payload protected by original token <em>T</em>.</li>
+ * </ol>
+ *
+ * @author vekterli
+ */
+public class SharedKeyResealingSession {
+
+ private final KeyPair ephemeralKeyPair;
+
+ SharedKeyResealingSession(KeyPair ephemeralKeyPair) {
+ this.ephemeralKeyPair = ephemeralKeyPair;
+ }
+
+ public static SharedKeyResealingSession newEphemeralSession() {
+ return new SharedKeyResealingSession(KeyUtils.generateX25519KeyPair());
+ }
+
+ @FunctionalInterface
+ public interface PrivateKeyProvider {
+ Optional<PrivateKey> privateKeyForId(KeyId id);
+ }
+
+ public record ResealingRequest(XECPublicKey ephemeralPubKey, SealedSharedKey sealedKey) {
+
+ private static final byte[] HEADER_BYTES = new byte[] {'R','S'};
+ private static final byte CURRENT_VERSION = 1;
+
+ public String toSerializedString() {
+ byte[] pubKeyBytes = KeyUtils.toRawX25519PublicKeyBytes(ephemeralPubKey);
+ byte[] tokenBytes = sealedKey.toSerializedBytes();
+
+ ByteBuffer encoded = ByteBuffer.allocate(HEADER_BYTES.length + 1 + 1 + pubKeyBytes.length + tokenBytes.length);
+ encoded.put(HEADER_BYTES);
+ encoded.put(CURRENT_VERSION);
+ encoded.put((byte)pubKeyBytes.length);
+ encoded.put(pubKeyBytes);
+ encoded.put(tokenBytes);
+ encoded.flip();
+
+ byte[] encBytes = new byte[encoded.remaining()];
+ encoded.get(encBytes);
+ return Base62.codec().encode(encBytes);
+ }
+
+ public static ResealingRequest fromSerializedString(String request) {
+ verifyInputStringNotTooLarge(request);
+ byte[] rawBytes = Base62.codec().decode(request);
+ if (rawBytes.length < HEADER_BYTES.length + 2) {
+ throw new IllegalArgumentException("Resealing request too short to contain a header and key length");
+ }
+ ByteBuffer decoded = ByteBuffer.wrap(rawBytes);
+ byte[] header = new byte[2];
+ decoded.get(header);
+ if (!Arrays.areEqual(header, HEADER_BYTES)) {
+ throw new IllegalArgumentException("No resealing request header found");
+ }
+ byte version = decoded.get();
+ if (version != CURRENT_VERSION) {
+ throw new IllegalArgumentException("Unsupported version in resealing request header");
+ }
+ int pubKeyLen = Byte.toUnsignedInt(decoded.get());
+ byte[] pubKeyBytes = new byte[pubKeyLen];
+ decoded.get(pubKeyBytes);
+
+ byte[] rawTokenBytes = new byte[decoded.remaining()];
+ decoded.get(rawTokenBytes);
+
+ return new ResealingRequest(KeyUtils.fromRawX25519PublicKey(pubKeyBytes),
+ SealedSharedKey.fromSerializedBytes(rawTokenBytes));
+ }
+
+ private static void verifyInputStringNotTooLarge(String tokenString) {
+ if (tokenString.length() > SealedSharedKey.MAX_TOKEN_STRING_LENGTH + 64) {
+ throw new IllegalArgumentException("String is too long to possibly be a valid resealing request");
+ }
+ }
+
+ }
+
+ public record ResealingResponse(SealedSharedKey resealedKey) {
+
+ public String toSerializedString() {
+ return resealedKey.toTokenString();
+ }
+
+ public static ResealingResponse fromSerializedString(String response) {
+ return new ResealingResponse(SealedSharedKey.fromTokenString(response));
+ }
+
+ }
+
+ public ResealingRequest resealingRequestFor(SealedSharedKey sealedSharedKey) {
+ return new ResealingRequest((XECPublicKey) ephemeralKeyPair.getPublic(), sealedSharedKey);
+ }
+
+ public static ResealingResponse reseal(ResealingRequest request, PrivateKeyProvider privateKeyProvider) {
+ var privKey = privateKeyProvider.privateKeyForId(request.sealedKey.keyId()).orElseThrow(
+ () -> new IllegalArgumentException("Could not find a private key for key ID '%s'".formatted(request.sealedKey.keyId())));
+
+ var secretShared = SharedKeyGenerator.fromSealedKey(request.sealedKey, privKey);
+ var resealed = SharedKeyGenerator.reseal(secretShared, request.ephemeralPubKey, KeyId.ofString("resealed-token")); // TODO key id
+
+ return new ResealingResponse(resealed.sealedSharedKey());
+ }
+
+
+ public SecretSharedKey openResealingResponse(ResealingResponse response) {
+ return SharedKeyGenerator.fromSealedKey(response.resealedKey, ephemeralKeyPair.getPrivate());
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
index 133798faa99..51f41ab7da7 100644
--- a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
+++ b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
@@ -175,7 +175,7 @@ public final class Hpke {
}
}
- private static record ContextBase(byte[] key, byte[] nonce, long seqNum, byte[] exporterSecret) { }
+ private record ContextBase(byte[] key, byte[] nonce, long seqNum, byte[] exporterSecret) { }
/**
* Section 5.1 Creating the Encryption Context:
@@ -219,8 +219,8 @@ public final class Hpke {
return new ContextBase(key, baseNonce, 0, exporterSecret);
}
- private static record ContextS(byte[] enc, ContextBase base) {}
- private static record ContextR(ContextBase base) {}
+ private record ContextS(byte[] enc, ContextBase base) {}
+ private record ContextR(ContextBase base) {}
/**
* Section 5.1.1 Encryption to a Public Key:
@@ -253,7 +253,7 @@ public final class Hpke {
return new ContextR(keySchedule(MODE_BASE, sharedSecret, info, DEFAULT_PSK, DEFAULT_PSK_ID));
}
- public static record Sealed(byte[] enc, byte[] ciphertext) {}
+ public record Sealed(byte[] enc, byte[] ciphertext) {}
/**
* Section 6.1 Encryption and Decryption: