diff options
Diffstat (limited to 'security-utils/src/main/java')
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><ephemeral public key, T></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><ephemeral public key, T></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: |