-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Correctly implement fuzzers for new encryption streams #131306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alinpahontu2912
wants to merge
2
commits into
dotnet:main
Choose a base branch
from
alinpahontu2912:zipstream_fuzzers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+266
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/WinZipAesStreamFuzzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Buffers; | ||
| using System.IO.Compression; | ||
| using System.Runtime.Versioning; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace DotnetFuzzing.Fuzzers; | ||
|
|
||
| [UnsupportedOSPlatform("browser")] | ||
| internal sealed class WinZipAesStreamFuzzer : IFuzzer | ||
| { | ||
| public string[] TargetAssemblies { get; } = ["System.IO.Compression"]; | ||
| public string[] TargetCoreLibPrefixes => []; | ||
|
|
||
| private const string Password = "fuzz-password"; | ||
| private const string EntryName = "entry"; | ||
|
|
||
| private static readonly ZipEncryptionMethod[] s_aesMethods = | ||
| [ | ||
| ZipEncryptionMethod.Aes128, | ||
| ZipEncryptionMethod.Aes192, | ||
| ZipEncryptionMethod.Aes256, | ||
| ]; | ||
|
|
||
| public void FuzzTarget(ReadOnlySpan<byte> bytes) | ||
| { | ||
| // The encryption streams only produce ciphertext for non-empty content; an empty | ||
| // entry is stored unencrypted, so it does not exercise the WinZipAesStream at all. | ||
| if (bytes.IsEmpty) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Use the first byte to select the AES key strength so all three variants get exercised. | ||
| ZipEncryptionMethod method = s_aesMethods[bytes[0] % s_aesMethods.Length]; | ||
|
|
||
| byte[] content = CopyToRentedArray(bytes); | ||
| try | ||
| { | ||
| RoundTrip(content, bytes.Length, method, async: false).GetAwaiter().GetResult(); | ||
| RoundTrip(content, bytes.Length, method, async: true).GetAwaiter().GetResult(); | ||
| } | ||
| finally | ||
| { | ||
| ArrayPool<byte>.Shared.Return(content); | ||
| } | ||
| } | ||
|
|
||
| private static async Task RoundTrip(byte[] content, int length, ZipEncryptionMethod method, bool async) | ||
| { | ||
| using var archiveStream = new MemoryStream(); | ||
|
|
||
| // Encrypt the fuzz input using WinZip AES, exercising the WinZipAesStream encryption + HMAC path. | ||
| ZipArchive writeArchive = async | ||
| ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null) | ||
| : new ZipArchive(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null); | ||
|
|
||
| ZipArchiveEntry writeEntry = writeArchive.CreateEntry(EntryName, Password.AsSpan(), method); | ||
|
|
||
| Stream entryWriteStream = async ? await writeEntry.OpenAsync() : writeEntry.Open(); | ||
| if (async) | ||
| { | ||
| await entryWriteStream.WriteAsync(content.AsMemory(0, length)); | ||
| await entryWriteStream.DisposeAsync(); | ||
| await writeArchive.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| entryWriteStream.Write(content, 0, length); | ||
| entryWriteStream.Dispose(); | ||
| writeArchive.Dispose(); | ||
| } | ||
|
|
||
| archiveStream.Position = 0; | ||
|
|
||
| // Decrypt with the correct password and verify the round-trip is lossless. | ||
| ZipArchive readArchive = async | ||
| ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null) | ||
| : new ZipArchive(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null); | ||
|
|
||
| ZipArchiveEntry readEntry = readArchive.GetEntry(EntryName)!; | ||
| Assert.True(readEntry.IsEncrypted); | ||
| Assert.Equal(method, readEntry.EncryptionMethod); | ||
|
|
||
| using (var decrypted = new MemoryStream()) | ||
| { | ||
| Stream entryReadStream = async ? await readEntry.OpenAsync(Password.AsSpan()) : readEntry.Open(Password.AsSpan()); | ||
| if (async) | ||
| { | ||
| await entryReadStream.CopyToAsync(decrypted); | ||
| await entryReadStream.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| entryReadStream.CopyTo(decrypted); | ||
| entryReadStream.Dispose(); | ||
| } | ||
|
|
||
| Assert.SequenceEqual(content.AsSpan(0, length), decrypted.ToArray()); | ||
| } | ||
|
|
||
| // Decrypting with a wrong password must fail cleanly with InvalidDataException, never crash. | ||
| try | ||
| { | ||
| using Stream stream = readEntry.Open("wrong-password".AsSpan()); | ||
| stream.CopyTo(Stream.Null); | ||
| } | ||
| catch (InvalidDataException) | ||
| { | ||
| // Expected: the AES password verifier / HMAC rejects the wrong key. | ||
| } | ||
|
alinpahontu2912 marked this conversation as resolved.
|
||
|
|
||
| if (async) | ||
| { | ||
| await readArchive.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| readArchive.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| private static byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes) | ||
| { | ||
| byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length); | ||
| bytes.CopyTo(buffer); | ||
| return buffer; | ||
| } | ||
| } | ||
119 changes: 119 additions & 0 deletions
119
src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipCryptoStreamFuzzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Buffers; | ||
| using System.IO.Compression; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace DotnetFuzzing.Fuzzers; | ||
|
|
||
| internal sealed class ZipCryptoStreamFuzzer : IFuzzer | ||
| { | ||
| public string[] TargetAssemblies { get; } = ["System.IO.Compression"]; | ||
| public string[] TargetCoreLibPrefixes => []; | ||
|
|
||
| private const string Password = "fuzz-password"; | ||
| private const string EntryName = "entry"; | ||
|
|
||
| public void FuzzTarget(ReadOnlySpan<byte> bytes) | ||
| { | ||
| // The encryption streams only produce ciphertext for non-empty content; an empty | ||
| // entry is stored unencrypted, so it does not exercise the ZipCryptoStream at all. | ||
| if (bytes.IsEmpty) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| byte[] content = CopyToRentedArray(bytes); | ||
| try | ||
| { | ||
| RoundTrip(content, bytes.Length, async: false).GetAwaiter().GetResult(); | ||
| RoundTrip(content, bytes.Length, async: true).GetAwaiter().GetResult(); | ||
| } | ||
| finally | ||
| { | ||
| ArrayPool<byte>.Shared.Return(content); | ||
| } | ||
| } | ||
|
|
||
| private static async Task RoundTrip(byte[] content, int length, bool async) | ||
| { | ||
| using var archiveStream = new MemoryStream(); | ||
|
|
||
| // Encrypt the fuzz input using legacy ZipCrypto, exercising the ZipCryptoStream encryption path. | ||
| ZipArchive writeArchive = async | ||
| ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null) | ||
| : new ZipArchive(archiveStream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null); | ||
|
|
||
| ZipArchiveEntry writeEntry = writeArchive.CreateEntry(EntryName, Password.AsSpan(), ZipEncryptionMethod.ZipCrypto); | ||
|
|
||
| Stream entryWriteStream = async ? await writeEntry.OpenAsync() : writeEntry.Open(); | ||
| if (async) | ||
| { | ||
| await entryWriteStream.WriteAsync(content.AsMemory(0, length)); | ||
| await entryWriteStream.DisposeAsync(); | ||
| await writeArchive.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| entryWriteStream.Write(content, 0, length); | ||
| entryWriteStream.Dispose(); | ||
| writeArchive.Dispose(); | ||
| } | ||
|
|
||
| archiveStream.Position = 0; | ||
|
|
||
| // Decrypt with the correct password and verify the round-trip is lossless. | ||
| ZipArchive readArchive = async | ||
| ? await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null) | ||
| : new ZipArchive(archiveStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null); | ||
|
|
||
| ZipArchiveEntry readEntry = readArchive.GetEntry(EntryName)!; | ||
| Assert.True(readEntry.IsEncrypted); | ||
| Assert.Equal(ZipEncryptionMethod.ZipCrypto, readEntry.EncryptionMethod); | ||
|
|
||
| using (var decrypted = new MemoryStream()) | ||
| { | ||
| Stream entryReadStream = async ? await readEntry.OpenAsync(Password.AsSpan()) : readEntry.Open(Password.AsSpan()); | ||
| if (async) | ||
| { | ||
| await entryReadStream.CopyToAsync(decrypted); | ||
| await entryReadStream.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| entryReadStream.CopyTo(decrypted); | ||
| entryReadStream.Dispose(); | ||
| } | ||
|
|
||
| Assert.SequenceEqual(content.AsSpan(0, length), decrypted.ToArray()); | ||
| } | ||
|
alinpahontu2912 marked this conversation as resolved.
|
||
|
|
||
| // Decrypting with a wrong password must fail cleanly with InvalidDataException, never crash. | ||
| try | ||
| { | ||
| using Stream stream = readEntry.Open("wrong-password".AsSpan()); | ||
| stream.CopyTo(Stream.Null); | ||
| } | ||
| catch (InvalidDataException) | ||
| { | ||
| // Expected: the header password verifier rejects the wrong key. | ||
| } | ||
|
alinpahontu2912 marked this conversation as resolved.
|
||
|
|
||
| if (async) | ||
| { | ||
| await readArchive.DisposeAsync(); | ||
| } | ||
| else | ||
| { | ||
| readArchive.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| private static byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes) | ||
| { | ||
| byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length); | ||
| bytes.CopyTo(buffer); | ||
| return buffer; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.