Open niloc132 opened 10 months ago
Here are a few thoughts:
Given the above I would suggest using the Servlet API init()
methods to initialize the signing code. However because these methods are not marked final in our GWT-RPC base servlets the code cannot fail fast during deployment. Maybe we should think about making it final and introduce a second, empty initService()
method that subclasses can override. However it would be a breaking change. If we don't want that we can only fail fast at runtime / request time.
API wise I could imagine
public class RemoteServiceServlet ... {
// To support public/private key cryptography the public key would
// need to be distributed manually. The API below does not allow
// sending a public key along every response.
interface SignatureHandler {
byte[] createEncryptedSignature(byte[] data);
boolean isValid(byte[] data, byte[] encryptedSignature);
}
private SignatureHandler signatureHandler;
@Override
public void init() {
super.init();
signatureHandler = createSignatureHandler();
}
protected SignatureHandler createSignatureHandler() {
// SHA256 + AES256 based, allows configuration of AES keys via system properties.
// Should cover most cases
return new DefaultSignatureHandler();
}
}
The SignatureHandler
would then be passed on to the code that requires it. The API is as simple as possible and is primarily meant to be used with symmetric cryptography. Public/private key cryptography can be implemented but requires external distribution of public keys because the public key will not be send along every GWT-RPC response.
I think this can work - two payloads instead of one does offer the complication that it technically changes the wire format ("instead of one opaque string payload added to each object, there are now two"), but with the use of raw byte[]
payloads we can base64 them individually and then join them on an unused character, or length-prefix each payload and concatenate them before base64.
String
or ByteBuffer
over byte[]
to prevent the implementation from changing the data. This would imply base64'ing the original payload first, and then joining on some unused character (the first thought I mentioned above).I'm content to leave the other details to the implementor, if someone wants to volunteer or sponsor this work. I think however that you can sidestep the init()
problem by either asserting that signatureHandler
not be null when loading the policy file if it will be required, or by renaming the create
method to merely be get
(and let the implementation deal with making it a singleton or returning fresh instances if stateless, etc. I might also suggest that to reduce the risk of name collision with existing methods, it could be a bit more specific - createEnhancedClassSignatureHandler
etc.
- With regard to symmetric vs private key, I have personal knowledge of applications using this strategy as insisted by their own security reviews, requiring that even two instances within the same cluster and company ought not share their private keys, so that, at least in theory, one key can be revoked at will.
That's fine we should not forbid that. The above API allows public/private keys but does not easily allow every server in the cluster having its own key pair as you would need to find a way to figure out the public key to use during verification.
Technically it is possible with the above API, although not obvious. Since the implementation is responsible to create and verify the signature, nobody stops it from adding additional information to the signature. So createEncryptedSignature()
could return bytes in the form of <encrypted signature>+<public key>
. Thinking a step further, creative devs could even use protobuf or similar to encode whatever they want in the signature.
- That's a fair point, but relative unchanged between your API and mine - I tend to favor
String
orByteBuffer
overbyte[]
to prevent the implementation from changing the data. This would imply base64'ing the original payload first, and then joining on some unused character (the first thought I mentioned above).
Good point about changing data. I guess we should settle on a readonly ByteBuffer then. I don't like String because it requires knowledge about character encoding.
- I'm not sure why that should need to be true - if the service has no need for signing, there should be no error/warning. A quick survey of publicly accessible GWT apps using RPC seems to suggest that this is the typical case (that is, I found zero vulnerable to this), but of course I don't have a way to guarantee this is true universally.
This was under the impression that we would revert the commit producing warnings/errors for the time being but instead always have a default implementation for signing in place if the developer does not provide its own implementation. That way enhanced classes can simply be used or experimented with without thinking about signing.
That's why I tend to favor a breaking change making init()
final, so GWT-RPC gains a spot in code to do initialization work that is required to execute.
Following https://github.com/gwtproject/gwt/issues/9709 and the PR https://github.com/gwtproject/gwt/pull/9879 to mitigate this by disabling the feature, this issue is to track efforts to make this feature safe to use again. There are several approaches that can be used to make this safe, and reasons that no one solution is a guaranteed fit for all applications, so I propose that we find a general extension point to add, and if populated by an application, permit the use of enhanced classes without warning or error.
Basic approaches:
onBeforeRequestDeserialized
can be overridden today to read from the request or session to validate in this way.ObjectInputStream
and surrounding code. In some cases it could be sufficient to offer a limited set of allowed classes that ObjectInputStream.resolveClass could read, and to check each one as it is read that it matches the expected field to write. SeeServerSerializationStreamReader.checkResolve
for the current implementation here.Option 1 seems to be the superior choice in terms of granting flexibility and not requiring trusting malicious clients, but it also likely requires the most flexible configuration. We could go so far as to delegate this as a simple interface such as
and offer a simple (single server), in-memory only (won't survive restarts) implementation to start from, and configuration to provide a custom implementation.