diff --git a/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj b/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj
index 9bfcd9434..725531024 100644
--- a/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj
+++ b/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj
@@ -7,6 +7,7 @@
+
diff --git a/Duplicati/Library/AutoUpdater/UpdaterManager.cs b/Duplicati/Library/AutoUpdater/UpdaterManager.cs
index f282afd61..20468fafd 100644
--- a/Duplicati/Library/AutoUpdater/UpdaterManager.cs
+++ b/Duplicati/Library/AutoUpdater/UpdaterManager.cs
@@ -31,6 +31,7 @@ using System.Text.Json;
using System.Net.Http;
using System.Threading;
using Duplicati.Library.Common.IO;
+using JsonSignature;
namespace Duplicati.Library.AutoUpdater
{
diff --git a/Duplicati/Library/Utility/CombinedStream.cs b/Duplicati/Library/Utility/CombinedStream.cs
deleted file mode 100644
index b801049e1..000000000
--- a/Duplicati/Library/Utility/CombinedStream.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-namespace Duplicati.Library.Utility;
-
-///
-/// A stream that combines multiple streams into one.
-///
-public class CombinedStream : Stream
-{
- ///
- /// The streams to combine
- ///
- private readonly Queue _streams;
- ///
- /// Flag keeping track of whether to leave the streams open when the combined stream is closed
- ///
- private readonly bool _leaveOpen;
-
- ///
- /// Creates a new combined stream
- ///
- /// The streams to combine
- /// True to leave the streams open when the combined stream is closed
- public CombinedStream(IEnumerable streams, bool leaveOpen = false)
- {
- _streams = new Queue(streams);
- _leaveOpen = leaveOpen;
- }
-
- ///
- public override bool CanRead => true;
-
- ///
- public override bool CanSeek => false;
-
- ///
- public override bool CanWrite => false;
-
- ///
- public override long Length => throw new NotSupportedException();
-
- ///
- public override long Position
- {
- get => throw new NotSupportedException();
- set => throw new NotSupportedException();
- }
-
- ///
- public override void Flush()
- {
- }
-
- ///
- public override int Read(byte[] buffer, int offset, int count)
- {
- if (_streams.Count == 0)
- return 0;
-
- var bytesRead = _streams.Peek().Read(buffer, offset, count);
-
- if (bytesRead == 0)
- {
- var prev = _streams.Dequeue();
- if (!_leaveOpen)
- prev.Dispose();
-
- return Read(buffer, offset, count);
- }
-
- return bytesRead;
- }
-
- ///
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotSupportedException();
- }
-
- ///
- public override void SetLength(long value)
- {
- throw new NotSupportedException();
- }
-
- ///
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotSupportedException();
- }
-
- ///
- protected override void Dispose(bool disposing)
- {
- if (!disposing)
- return;
-
- if (!_leaveOpen)
- foreach (var stream in _streams)
- stream.Dispose();
-
- _streams.Clear();
- }
-}
diff --git a/Duplicati/Library/Utility/JSONSignature.cs b/Duplicati/Library/Utility/JSONSignature.cs
deleted file mode 100644
index ed6f97f2f..000000000
--- a/Duplicati/Library/Utility/JSONSignature.cs
+++ /dev/null
@@ -1,363 +0,0 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-
-#nullable enable
-
-using System;
-using System.Buffers.Binary;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Duplicati.Library.Utility;
-
-///
-/// Implementation of the SIGJSON signature format
-///
-public static class JSONSignature
-{
- ///
- /// The RSA SHA256 signature method with PKCS1 padding
- ///
- public const string RSA_SHA256 = "RS256";
- ///
- /// The RSA SHA384 signature method with PKCS1 padding
- ///
- public const string RSA_SHA384 = "RS384";
- ///
- /// The RSA SHA512 signature method with PKCS1 padding
- ///
- public const string RSA_SHA512 = "RS512";
-
- ///
- /// Mapping of the headers to the version number
- ///
- private static readonly IReadOnlyDictionary SIGJSONHeaders = new Dictionary
- {
- {Convert.ToBase64String("//SIGJSONv1: "u8), 1},
- {Convert.ToBase64String("//SIGJSONv2: "u8), 2},
- {Convert.ToBase64String("//SIGJSONv3: "u8), 3},
- {Convert.ToBase64String("//SIGJSONv4: "u8), 4},
- {Convert.ToBase64String("//SIGJSONv5: "u8), 5},
- {Convert.ToBase64String("//SIGJSONv6: "u8), 6},
- {Convert.ToBase64String("//SIGJSONv7: "u8), 7},
- {Convert.ToBase64String("//SIGJSONv8: "u8), 8},
- {Convert.ToBase64String("//SIGJSONv9: "u8), 9}
- };
-
- ///
- /// The header type for the signature
- ///
- private const string HEADER_TYPE = "SIGJSONv1";
-
- ///
- /// The length of the signature header string
- ///
- private static readonly int SignatureLength = "//SIGJSONv1: ".Length;
-
- ///
- /// The built-in signing methods
- ///
- private static readonly IReadOnlyDictionary> _builtInSigningMethods = new Dictionary>
- {
- [RSA_SHA256] = (stream, key) => SignRSA(stream, key.PrivateKey, HashAlgorithmName.SHA256),
- [RSA_SHA384] = (stream, key) => SignRSA(stream, key.PrivateKey, HashAlgorithmName.SHA384),
- [RSA_SHA512] = (stream, key) => SignRSA(stream, key.PrivateKey, HashAlgorithmName.SHA512)
- };
-
- ///
- /// The built-in verification methods
- ///
- private static readonly IReadOnlyDictionary> _builtInVerificationMethods = new Dictionary>
- {
- [RSA_SHA256] = (stream, key, signature) => VerifyRSA(stream, key.PublicKey, signature, HashAlgorithmName.SHA256),
- [RSA_SHA384] = (stream, key, signature) => VerifyRSA(stream, key.PublicKey, signature, HashAlgorithmName.SHA384),
- [RSA_SHA512] = (stream, key, signature) => VerifyRSA(stream, key.PublicKey, signature, HashAlgorithmName.SHA512)
- };
-
- ///
- /// Signs a stream with the specified method
- ///
- /// The algorithm to use
- /// The public key to validate with
- /// The private key, if signing
- /// The additional header values, if any
- /// A custom method to sign with; Use null for the built-in methods
- public record SignOperation(string Algorithm, string PublicKey, string PrivateKey, IEnumerable>? HeaderValues = null, Func? SignMethod = null);
-
- ///
- /// Verifies a stream with the specified method
- ///
- /// The algorithm to use
- /// The key to verify with
- /// A custom method to verify with; Use null for the built-in methods
- public record VerifyOperation(string Algorithm, string PublicKey, Func? VerifyMethod = null);
-
- ///
- /// The result of a verification with a match
- ///
- /// The algorithm matched
- /// The public key matched
- /// The header values matched
- public record VerifyMatch(string Algorithm, string PublicKey, IEnumerable> HeaderValues);
-
- ///
- /// Signs a stream with SIGJSONv1 for multiple keys
- ///
- /// The stream of data to sign; must be seekable
- /// The target stream where signed data is written
- /// The key setups to sign with
- /// An awaitable task
- public static async Task SignAsync(Stream source, Stream target, IEnumerable signOperations)
- {
- if (signOperations == null || !signOperations.Any())
- throw new InvalidOperationException("No sign operations provided, cannot sign data");
-
- source.Position = 0;
- var buffer = new byte["//SIGJSON".Length];
- await source.ReadAsync(buffer, 0, buffer.Length);
- if (Encoding.UTF8.GetString(buffer) == "//SIGJSON")
- throw new Exception("Cannot sign data with an existing signature");
-
- foreach (var key in signOperations)
- {
- source.Position = 0;
- await target.WriteAsync(Encoding.UTF8.GetBytes(CreateSignature(source, key)));
- }
-
-
- source.Position = 0;
- await source.CopyToAsync(target);
- await target.FlushAsync();
- }
-
- ///
- /// Validates a stream with SIGJSONv1 for at least one key match
- ///
- /// The stream of data to validate; must be seekable
- /// The verification operations to use
- /// true if at least one match is found; false otherwise
- /// The stream is reset to the content position after validation
- public static bool VerifyAtLeastOne(Stream source, IEnumerable verifyOperations)
- => Verify(source, verifyOperations).Any();
-
- ///
- /// Validates a stream with SIGJSONv1 for multiple keys
- ///
- /// The stream of data to validate; must be seekable
- /// The verification operations to use
- /// A list of matches
- /// The stream is reset to the content position after validation
- public static IEnumerable Verify(Stream source, IEnumerable verifyOperations)
- {
- if (verifyOperations == null || !verifyOperations.Any())
- throw new InvalidOperationException("No verify operations provided, cannot verify data");
-
- var buffer = new byte[8 * 1024];
- var headerLines = new List<(byte[] Header, byte[] Signature)>();
-
- while (true)
- {
- // Record the start, so we can return to it if the line is not a signature
- var position = source.Position;
-
- // Get the buffer as a span
- var line = buffer.AsSpan(0, source.Read(buffer));
- // Find the line delimiter
- var newlineIndex = Math.Max(0, line.IndexOf((byte)'\n'));
- // Get the part of the buffer that contains the line
- line = line[..newlineIndex];
-
- // Check if the signature matches a known version
- var version = 0;
- if (line.Length > SignatureLength)
- SIGJSONHeaders.TryGetValue(Convert.ToBase64String(line[..SignatureLength]), out version);
-
- // No matching header found, reset stream to include the comment line and stop searching
- if (version < 1)
- {
- source.Position = position;
- break;
- }
-
- // Valid signature line, set the stream to exclude the comment line
- source.Position = position + newlineIndex + 1;
-
- // We only parse the v1 lines for now, and ignore the others
- if (version == 1)
- {
- // This can throw on non-UTF data, non-base64 data, etc
- try
- {
- var kp = Encoding.UTF8.GetString(line[SignatureLength..]).Split('.', StringSplitOptions.TrimEntries);
- if (kp.Length == 2)
- headerLines.Add((
- Header: Convert.FromBase64String(kp[0]),
- Signature: Convert.FromBase64String(kp[1])
- ));
- }
- catch
- {
- // Treat invalid lines as not a signature
- source.Position = position;
- break;
- }
- }
- }
-
- // No more header lines, so we are at the start of the content
- var startOfContent = source.Position;
- var result = new List();
-
- // For each header line, validate the signature
- foreach (var (header, signature) in headerLines)
- {
- source.Position = startOfContent;
- result.AddRange(ValidateSignature(source, header, verifyOperations, signature));
- }
-
- // Reset the stream to the start of the content
- source.Position = startOfContent;
- return result;
- }
-
- ///
- /// Creates a single signature line
- ///
- /// The data to sign
- /// The sign operation data
- /// The signature string
- private static string CreateSignature(Stream data, SignOperation skey)
- {
- var headers = skey.HeaderValues ?? new List>();
- headers = headers.Append(new KeyValuePair("alg", skey.Algorithm));
- headers = headers.Append(new KeyValuePair("key", skey.PublicKey));
- headers = headers.Append(new KeyValuePair("typ", HEADER_TYPE));
-
- if (headers.DistinctBy(x => x.Key).Count() != headers.Count())
- throw new InvalidOperationException("Duplicate headers are not allowed");
-
- var headerData = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(headers, new System.Text.Json.JsonSerializerOptions { WriteIndented = false });
- var signMethod = skey.SignMethod ?? _builtInSigningMethods[skey.Algorithm];
- if (signMethod == null)
- throw new InvalidOperationException($"No signing method found for: {skey.Algorithm}");
-
- var intBuf = new byte[4];
- BinaryPrimitives.WriteUInt32BigEndian(intBuf, (uint)headerData.Length);
-
- using var headerStream = new MemoryStream();
- headerStream.Write(intBuf);
- headerStream.Write(headerData);
-
- headerStream.Position = 0;
- using var prefixedStream = new CombinedStream([headerStream, data], leaveOpen: true);
- var signature = signMethod(prefixedStream, skey);
-
- var sigJsonV1 = new StringBuilder();
- sigJsonV1.Append("//SIGJSONv1: ");
- sigJsonV1.Append(Convert.ToBase64String(headerData));
- sigJsonV1.Append(".");
- sigJsonV1.Append(Convert.ToBase64String(signature));
- sigJsonV1.Append("\n");
-
- return sigJsonV1.ToString();
- }
-
- ///
- /// Verifies a single signature line
- ///
- /// The payload data
- /// The header data
- /// The verification operations possible
- /// The signature to match
- /// A list of matches
- private static IEnumerable ValidateSignature(Stream data, byte[] header, IEnumerable verifiers, byte[] signature)
- {
- var startOfContent = data.Position;
-
- List>? headers = null;
- try { headers = System.Text.Json.JsonSerializer.Deserialize>>(header); }
- catch { }
-
- var alg = headers?.FirstOrDefault(x => x.Key == "alg").Value;
- var key = headers?.FirstOrDefault(x => x.Key == "key").Value;
- var typ = headers?.FirstOrDefault(x => x.Key == "typ").Value;
- if (string.IsNullOrWhiteSpace(alg) || string.IsNullOrWhiteSpace(key) || typ != HEADER_TYPE)
- yield break;
-
- var potentialMatches = verifiers.Where(x => x.Algorithm == alg && x.PublicKey == key);
- if (!potentialMatches.Any())
- yield break;
-
- var intBuf = new byte[4];
- BinaryPrimitives.WriteUInt32BigEndian(intBuf, (uint)header.Length);
-
- using var headerStream = new MemoryStream();
- headerStream.Write(intBuf);
- headerStream.Write(header);
-
- foreach (var verifier in potentialMatches)
- {
- var verifyMethod = verifier.VerifyMethod ?? _builtInVerificationMethods[verifier.Algorithm];
- if (verifyMethod == null)
- throw new InvalidOperationException($"No verification method found for: {verifier.Algorithm}");
-
- headerStream.Position = 0;
- data.Position = startOfContent;
-
- using var prefixedStream = new CombinedStream([headerStream, data], leaveOpen: true);
- if (verifyMethod(prefixedStream, verifier, signature))
- yield return new VerifyMatch(alg, key, headers!);
- }
- }
-
- ///
- /// Signs a stream with RSA and the specified hash algorithm
- ///
- /// The data to sign
- /// The key to sign with
- /// The hash algorithm to use
- /// The signature
- private static byte[] SignRSA(Stream data, string privateKey, HashAlgorithmName hashAlgorithmName)
- {
- using var rsa = new RSACryptoServiceProvider();
- rsa.FromXmlString(privateKey);
- return rsa.SignData(data, hashAlgorithmName, RSASignaturePadding.Pkcs1);
- }
-
- ///
- /// Verifies a stream signed with RSA and the specified hash algorithm
- ///
- /// The data to verify
- /// The key to verify with
- /// The signature to use
- /// The algorithm to use
- /// true if the signature is valid; false otherwise
- private static bool VerifyRSA(Stream data, string publicKey, byte[] signature, HashAlgorithmName hashAlgorithmName)
- {
- using var rsa = new RSACryptoServiceProvider();
- rsa.FromXmlString(publicKey);
- return rsa.VerifyData(data, signature, hashAlgorithmName, RSASignaturePadding.Pkcs1);
- }
-}
\ No newline at end of file
diff --git a/Duplicati/UnitTest/JSONSignatureTest.cs b/Duplicati/UnitTest/JSONSignatureTest.cs
deleted file mode 100644
index 7521a0415..000000000
--- a/Duplicati/UnitTest/JSONSignatureTest.cs
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (C) 2025, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using Duplicati.Library.Utility;
-using NUnit.Framework;
-
-namespace Duplicati.UnitTest;
-
-[Category("SignatureTest")]
-public class JSONSignatureTest
-{
- [Test]
- [TestCase(true)]
- [TestCase(false)]
- public void SignWithASingleKeyShouldWork(bool withHeaders)
- {
- var content = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new { test = "test", extra = 234 }));
- var headers = withHeaders ? new System.Collections.Generic.Dictionary { { "test", "1234" } } : null;
- var key = new RSACryptoServiceProvider(2048);
-
- using var source = new MemoryStream(content);
- using var target = new MemoryStream();
-
- JSONSignature.SignAsync(source, target, [new JSONSignature.SignOperation(JSONSignature.RSA_SHA256, key.ToXmlString(false), key.ToXmlString(true), headers)]).Await();
-
- target.Position = 0;
-
- var valids = JSONSignature.Verify(target, [new JSONSignature.VerifyOperation(JSONSignature.RSA_SHA256, key.ToXmlString(false))]);
- Assert.That(valids, Has.Count.EqualTo(1));
- }
-
- [Test]
- [TestCase(true)]
- [TestCase(false)]
- public void SignWithMultipleAlgsShouldWork(bool withHeaders)
- {
- var content = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new { test = "test", extra = 234 }));
- var headers = withHeaders ? new System.Collections.Generic.Dictionary { { "test", "1234" } } : null;
- var key = new RSACryptoServiceProvider(2048);
-
- using var source = new MemoryStream(content);
- using var target = new MemoryStream();
-
- var algs = new[] { JSONSignature.RSA_SHA256, JSONSignature.RSA_SHA384, JSONSignature.RSA_SHA512 };
-
- JSONSignature.SignAsync(source, target, algs.Select(x => new JSONSignature.SignOperation(x, key.ToXmlString(false), key.ToXmlString(true), headers))).Await();
-
- target.Position = 0;
- var valids = JSONSignature.Verify(target, algs.Select(x => new JSONSignature.VerifyOperation(x, key.ToXmlString(false))));
- Assert.That(valids, Has.Count.EqualTo(algs.Length));
- foreach (var alg in algs)
- {
- target.Position = 0;
- var valid = JSONSignature.Verify(target, [new JSONSignature.VerifyOperation(alg, key.ToXmlString(false))]);
- Assert.That(valid, Has.Count.EqualTo(1));
- Assert.That(valid.First().Algorithm, Is.EqualTo(alg));
- Assert.That(valid.First().PublicKey, Is.EqualTo(key.ToXmlString(false)));
-
- target.Position = 0;
- var hasOne = JSONSignature.VerifyAtLeastOne(target, [new JSONSignature.VerifyOperation(alg, key.ToXmlString(false))]);
- Assert.That(hasOne, Is.True);
- }
- }
-
- [Test]
- [TestCase(true)]
- [TestCase(false)]
- public void SignWithMultipleKeysShouldWork(bool withHeaders)
- {
- var content = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new { test = "test", extra = 234 }));
- var headers = withHeaders ? new System.Collections.Generic.Dictionary { { "test", "1234" } } : null;
- var keys = new[] { new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(1024) };
-
- using var source = new MemoryStream(content);
- using var target = new MemoryStream();
-
- var algs = new[] { JSONSignature.RSA_SHA256, JSONSignature.RSA_SHA384, JSONSignature.RSA_SHA512 };
- var algkeycombos = algs.SelectMany(alg => keys.Select(key => (Alg: alg, Key: key))).ToArray();
-
- JSONSignature.SignAsync(source, target, algkeycombos.Select(x => new JSONSignature.SignOperation(x.Alg, x.Key.ToXmlString(false), x.Key.ToXmlString(true), headers))).Await();
-
- target.Position = 0;
- var valids = JSONSignature.Verify(target, algkeycombos.Select(x => new JSONSignature.VerifyOperation(x.Alg, x.Key.ToXmlString(false))));
- Assert.That(valids, Has.Count.EqualTo(algkeycombos.Length));
- foreach (var alg in algkeycombos)
- {
- target.Position = 0;
- var valid = JSONSignature.Verify(target, [new JSONSignature.VerifyOperation(alg.Alg, alg.Key.ToXmlString(false))]);
- Assert.That(valid, Has.Count.EqualTo(1));
- Assert.That(valid.First().Algorithm, Is.EqualTo(alg.Alg));
- Assert.That(valid.First().PublicKey, Is.EqualTo(alg.Key.ToXmlString(false)));
-
- target.Position = 0;
- var hasOne = JSONSignature.VerifyAtLeastOne(target, [new JSONSignature.VerifyOperation(alg.Alg, alg.Key.ToXmlString(false))]);
- Assert.That(hasOne, Is.True);
- }
- }
-
- [Test]
- public void InvalidSignaturesShouldNotThrow()
- {
- var broken1 = new[] {
- // Invalid Base64 data
- "//SIGJSONv1: ####.abc=\n{\"x\": 1}"u8.ToArray(),
-
- // Invalid Base64 data
- "//SIGJSONv1: abc.abc\n{\"x\": 1}"u8.ToArray(),
-
- // Invalid header
- "//SIGJSONv1:abc=.abc=\n{\"x\": 1}"u8.ToArray(),
-
- // No newline
- "//SIGJSONv1: abc=.abc={\"x\": 1}"u8.ToArray(),
-
- // No newline
- "//SIGJSONv1: abc=.abc={\"x\": 1}\n"u8.ToArray(),
-
- // Extra newline
- "//SIGJSONv1: abc=.abc=\n{\"x\": 1}\n"u8.ToArray(),
- };
-
- var key = new RSACryptoServiceProvider(2048);
- foreach (var c in broken1)
- {
- using var source = new MemoryStream(c);
- using var target = new MemoryStream();
-
- var valids = JSONSignature.Verify(target, [new JSONSignature.VerifyOperation(JSONSignature.RSA_SHA256, key.ToXmlString(false))]);
- Assert.That(valids, Has.Count.EqualTo(0));
- }
- }
-
- [Test]
- [TestCase(true)]
- [TestCase(false)]
- public void TaintedDataShouldNotValidate(bool withHeaders)
- {
- var content = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new { test = "test", extra = 234 }));
- var headers = withHeaders ? new System.Collections.Generic.Dictionary { { "test", "1234" } } : null;
- var keys = new[] { new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(1024) };
-
- using var source = new MemoryStream(content);
- using var target = new MemoryStream();
-
- var algs = new[] { JSONSignature.RSA_SHA256, JSONSignature.RSA_SHA384, JSONSignature.RSA_SHA512 };
- var algkeycombos = algs.SelectMany(alg => keys.Select(key => (Alg: alg, Key: key))).ToArray();
-
- JSONSignature.SignAsync(source, target, algkeycombos.Select(x => new JSONSignature.SignOperation(x.Alg, x.Key.ToXmlString(false), x.Key.ToXmlString(true), headers))).Await();
-
- // Taint the data
- target.Position = target.Length - 1;
- target.WriteByte((byte)'\n');
-
- target.Position = 0;
- var valids = JSONSignature.Verify(target, algkeycombos.Select(x => new JSONSignature.VerifyOperation(x.Alg, x.Key.ToXmlString(false))));
- Assert.That(valids, Has.Count.EqualTo(0));
- }
-
- [Test]
- [TestCase(true, 1)] // Offset=1 breaks the JSON
- [TestCase(false, 1)]
- [TestCase(true, 4)] // Offset=4 changes the header key name
- [TestCase(false, 4)]
- public void TaintedHeadersShouldNotValidate(bool withHeaders, int offset)
- {
- var content = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new { test = "test", extra = 234 }));
- var headers = withHeaders ? new System.Collections.Generic.Dictionary { { "test", "1234" } } : null;
- var keys = new[] { new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(2048), new RSACryptoServiceProvider(1024) };
-
- using var source = new MemoryStream(content);
- using var target = new MemoryStream();
-
- var algs = new[] { JSONSignature.RSA_SHA256, JSONSignature.RSA_SHA384, JSONSignature.RSA_SHA512 };
- var algkeycombos = algs.SelectMany(alg => keys.Select(key => (Alg: alg, Key: key))).ToArray();
-
- JSONSignature.SignAsync(source, target, algkeycombos.Select(x => new JSONSignature.SignOperation(x.Alg, x.Key.ToXmlString(false), x.Key.ToXmlString(true), headers))).Await();
-
- // Taint the header data for the first signature
- target.Position = "//SIGJSONv1: ".Length + offset;
- var cur = target.ReadByte();
- target.Position -= 1;
- target.WriteByte((byte)(cur - 1));
-
- target.Position = 0;
- var valids = JSONSignature.Verify(target, algkeycombos.Select(x => new JSONSignature.VerifyOperation(x.Alg, x.Key.ToXmlString(false))));
- Assert.That(valids, Has.Count.EqualTo(algkeycombos.Length - 1));
- }
-}