🎬🍿 Streams make the world go round 🌍 Episode 1#273
🎬🍿 Streams make the world go round 🌍 Episode 1#273erwinkramer wants to merge 37 commits intodvsekhvalnov:masterfrom
Conversation
|
@dvsekhvalnov please let me know what you think. |
|
Cool, thanks for that ! I'll try to take a look next week after weekend. |
…codingStream with variable buffer sizes
… sizes and buffer configurations
|
@erwinkramer help me understand :) So, we are trying to introduce support for encoding/verifying big detached payloads, right? If so. Would be nice to see a practical test: let's say couple hundrends MB/GB payload which causing OOM with current approach, but handled with no issues using streaming. |
|
@dvsekhvalnov exactly, 2 things:
I added a |
|
Yeah, see the test, cool. What's an example of system streaming in nature that wants to do JWT on it? Let me wrap my head around changes, may take a while and i'll pop here and there with random questions. My usual concerns are:
|
Request and response bodies in asp.net core are streams by nature: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/use-http-context?view=aspnetcore-9.0
Sure!
You mean ConcatenatedStream.cs? Is there any way you think we can further test this? |
…rewinds streams and encapsulates the verify in a using
Well, what i think is i'd prefer to isolate new streaming support from existing non-streaming code to limit possible blast radius if any new vulnerabilities found. Let's start with new
public static string EncodeStream(Stream payload, object key, JwsAlgorithm algorithm, IDictionary<string, object> extraHeaders = null, JwtSettings settings = null, JwtOptions options = null) {
return Sign(payload, key,...., new StreamingPayloadDelegate())
}
public static string EncodeBytes(byte[] payload, object key, JwsAlgorithm algorithm, IDictionary<string, object> extraHeaders = null, JwtSettings settings = null, JwtOptions options = null) {
return Sign(payload, key,...., new BytePayloadDelegate())
}
|
| /// <exception cref="IntegrityException">if signature validation failed</exception> | ||
| /// <exception cref="EncryptionException">if JWT token can't be decrypted</exception> | ||
| /// <exception cref="InvalidAlgorithmException">if JWT signature, encryption or compression algorithm is not supported</exception> | ||
| public static Stream DecodeStream(string token, object key, JwsAlgorithm alg, JwtSettings settings = null, Stream payload = null) |
There was a problem hiding this comment.
Here is question. What's the point of returning Stream?
It either:
-
payload from token, that have been already loaded into memory and can be trivially wrapped to
MemoryStreamwith almost one line of code -
or it is essentially same detached payload stream that have been passed as argument
There was a problem hiding this comment.
It's simply because all other public decode functions work that way, DecodeBytes and Decode both return the payload itself (whether from body or from token), it was rather confusing as to why that's already happening in those existing functions. If you want, i can just make this method return a void but that wouldn't make it more consistent.
There was a problem hiding this comment.
Well, let's call it decade long sdk design error. I'd truly prefer to have interface that deal only with byte[] and let everything else go to caller side. But may be too late.
Does it make sense to have DecodeStream() at all? Looks like VerifyStream() should cover all possible use-cases.
I attempted to do this earlier, but the |
|
Do you have previous version to compare? I'm fine with algorithms to be But the only difference in implementation is the line: byte[] signature = jwsAlgorithm.Sign(securedInput(headerBytes, payload, jwtOptions.EncodePayload), key);
// which is simply delegate that takes: (alg, header, payload, encode, key and return byte[] array), e.g.
// you either pass one version or another or can do something fancy with pattern matching too
// byte[] signature = _doSign(jwsAlgorithm, key, headerBytes, payload, jwtOptions.EncodePayload) |
|
I have no previous for you to compare.
The In the end, this |
|
If you mean My motivation is simple, it is security. Encoding detached payload is quite narrow use-case. There is nothing wrong to have streaming support for big payloads. But implementation wise streaming should be narrowed to given use case as well and not affecting rest of flows that were pretty stable last years. Ok, let me may be just dump original public static string EncodeBytes(.....)
// ==== that part stays, can be moved to internal '_encode(..)' methods as is ====
{
if (payload == null)
throw new ArgumentNullException(nameof(payload));
var jwtSettings = GetSettings(settings);
var jwtOptions = options ?? JwtOptions.Default;
var jwtHeader = new Dictionary<string, object> { { "alg", jwtSettings.JwsHeaderValue(algorithm) } };
if (extraHeaders == null) //allow overload, but keep backward compatible defaults
{
extraHeaders = new Dictionary<string, object> { { "typ", "JWT" } };
}
if (!jwtOptions.EncodePayload)
{
jwtHeader["b64"] = false;
jwtHeader["crit"] = Collections.Union(new[] { "b64" }, Dictionaries.Get<object>(extraHeaders, "crit"));
}
Dictionaries.Append(jwtHeader, extraHeaders);
byte[] headerBytes = Encoding.UTF8.GetBytes(jwtSettings.JsonMapper.Serialize(jwtHeader));
var jwsAlgorithm = jwtSettings.Jws(algorithm);
if (jwsAlgorithm == null)
{
throw new JoseException(string.Format("Unsupported JWS algorithm requested: {0}", algorithm));
}
// ==== end region ====
// ==== here we have real difference that can be abstracted to delegate or whatever pattern:
// it takes header and returns string
// or as an option may be all the code that constructing 'headerBytes' can be extracted to its own method
byte[] signature = jwsAlgorithm.Sign(securedInput(headerBytes, payload, jwtOptions.EncodePayload), key);
byte[] payloadBytes = jwtOptions.DetachPayload ? new byte[0] : payload;
return jwtOptions.EncodePayload
? Compact.Serialize(headerBytes, payloadBytes, signature)
: Compact.Serialize(headerBytes, Encoding.UTF8.GetString(payloadBytes), signature);
}Also be careful with base64 encoding, somebody asked support for other encoding version: #271 |
I have no interest in placing the byte[] implementation separate from the streaming implementation since i believe that the foundational logic should just be of 1 type to prevent any more complexity. I might be wrong, and it might turn out not that much complex, but then I'd like you or someone else to continue building on this.
If you consider the current approach potentially unsafe, then it's also unsafe for the streaming methods if they were built completely separate and implemented in this library. So, the security motivation I consider flawed. However, i do agree that it is risky at this stage since it's not battle-tested, and i would consider (if you ever change your mind and implement it as-is) to make this a beta/preview release, with a major version increment (let's say v6.0.0). You can go ahead and take some parts and implement it side-by-side, but i understand if you don't, then you can go ahead and close this PR or leave it open for someone else to work on. |
|
@erwinkramer yeah, i can understand frustration, sorry about that. But nevertheless thanks for your contribution, great ideas and let's keep it open. I'm interested to play around and see what can we make out it. |
Contributes to #3.
EncodeandEncodeBytesnow call the newly introducedEncodeStream, which is also directly callable as proven with the newEncodeStreamHS512test. This means the entire encoding foundation is now streaming based.New
DecodeStreamandVerifyStreamintroduced, which make use of the modifiedDecodeBytes, now calledDecodeStream, that handles input and output payload now via Streams.Non-seekable streams are not supported for now, i only focused on making seekable streams for payloads work in a streaming matter.
I'm sure there is room for more improvements, please review.
Progress on solutions and their unit tests:
jose-jwt-net40.sln- ✅ compiles and tests run successfully. Requires vs2019 and a seperate install of NUnit extension, but it's not easy to find that installer link for vs2019.jose-jwt-net46.sln- ✅ compiles and tests run successfully.jose-jwt-net47.sln- ✅ compiles and tests run successfully.jose-jwt.sln- ✅ compiles and tests run successfully.