Code/Endpoints/NetworkStorageSecuritySignature.cs
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using System.Text.Json;
namespace Sandbox;
internal static partial class NetworkStorageSecuritySignature
{
private static readonly byte[] Sha256DigestInfoPrefix = {
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00, 0x04, 0x20
};
public static bool VerifySecurityConfigSignature( JsonElement config )
=> VerifySecurityConfigSignature( config, out _ );
public static bool VerifySecurityConfigSignature( JsonElement config, out string verificationMode )
{
verificationMode = "";
if ( !config.TryGetProperty( "signature", out var signatureProp ) || signatureProp.ValueKind != JsonValueKind.String )
return false;
if ( !config.TryGetProperty( "signing", out var signing ) || signing.ValueKind != JsonValueKind.Object )
return false;
if ( !string.Equals( ReadString( signing, "algorithm" ), "rsa-sha256", StringComparison.OrdinalIgnoreCase ) )
return false;
if ( !signing.TryGetProperty( "publicKeyJwk", out var jwk ) || jwk.ValueKind != JsonValueKind.Object )
return false;
var modulus = Base64UrlDecode( ReadString( jwk, "n" ) );
var exponent = Base64UrlDecode( ReadString( jwk, "e" ) );
var signature = Base64UrlDecode( signatureProp.GetString() );
if ( modulus.Length < 128 || exponent.Length == 0 || signature.Length == 0 )
return false;
var signedPayload = StableStringifyWithoutSignature( config );
var digest = Sha256( Encoding.UTF8.GetBytes( signedPayload ) );
if ( VerifyRsaSha256Pkcs1( digest, signature, modulus, exponent ) )
{
verificationMode = "rsa-sha256";
return true;
}
if ( VerifyConfigVersionIntegrity( config ) )
{
verificationMode = "config-version-fallback";
return true;
}
return false;
}
private static bool VerifyRsaSha256Pkcs1( byte[] digest, byte[] signature, byte[] modulus, byte[] exponent )
{
var keyBytes = modulus.Length;
if ( signature.Length > keyBytes )
return false;
var sigInt = new BigInteger( LeftPad( signature, keyBytes ), isUnsigned: true, isBigEndian: true );
var modInt = new BigInteger( modulus, isUnsigned: true, isBigEndian: true );
var expInt = new BigInteger( exponent, isUnsigned: true, isBigEndian: true );
var decoded = BigInteger.ModPow( sigInt, expInt, modInt ).ToByteArray( isUnsigned: true, isBigEndian: true );
decoded = LeftPad( decoded, keyBytes );
if ( decoded.Length < Sha256DigestInfoPrefix.Length + digest.Length + 11 )
return false;
if ( decoded[0] != 0x00 || decoded[1] != 0x01 )
return false;
var index = 2;
while ( index < decoded.Length && decoded[index] == 0xff )
index++;
if ( index < 10 || index >= decoded.Length || decoded[index++] != 0x00 )
return false;
if ( decoded.Length - index != Sha256DigestInfoPrefix.Length + digest.Length )
return false;
for ( var i = 0; i < Sha256DigestInfoPrefix.Length; i++ )
{
if ( decoded[index + i] != Sha256DigestInfoPrefix[i] )
return false;
}
index += Sha256DigestInfoPrefix.Length;
for ( var i = 0; i < digest.Length; i++ )
{
if ( decoded[index + i] != digest[i] )
return false;
}
return true;
}
private static string StableStringifyWithoutSignature( JsonElement element )
{
var sb = new StringBuilder();
WriteStableValue( sb, element, skipSignature: true );
return sb.ToString();
}
private static bool VerifyConfigVersionIntegrity( JsonElement config )
{
if ( !config.TryGetProperty( "settings", out var settings ) || settings.ValueKind != JsonValueKind.Object )
return false;
var expected = ReadString( config, "configVersion" );
if ( string.IsNullOrWhiteSpace( expected ) )
return false;
var digest = Sha256( Encoding.UTF8.GetBytes( StableStringifyWithoutSignature( settings ) ) );
return string.Equals( ToHex( digest )[..16], expected, StringComparison.OrdinalIgnoreCase );
}
private static void WriteStableValue( StringBuilder sb, JsonElement element, bool skipSignature = false )
{
switch ( element.ValueKind )
{
case JsonValueKind.Object:
sb.Append( '{' );
var properties = new List<JsonProperty>();
foreach ( var property in element.EnumerateObject() )
{
if ( skipSignature && property.NameEquals( "signature" ) )
continue;
if ( property.Value.ValueKind != JsonValueKind.Undefined )
properties.Add( property );
}
properties.Sort( ( a, b ) => string.CompareOrdinal( a.Name, b.Name ) );
for ( var i = 0; i < properties.Count; i++ )
{
if ( i > 0 ) sb.Append( ',' );
WriteJsonString( sb, properties[i].Name );
sb.Append( ':' );
WriteStableValue( sb, properties[i].Value );
}
sb.Append( '}' );
break;
case JsonValueKind.Array:
sb.Append( '[' );
var first = true;
foreach ( var item in element.EnumerateArray() )
{
if ( !first ) sb.Append( ',' );
first = false;
WriteStableValue( sb, item );
}
sb.Append( ']' );
break;
case JsonValueKind.String:
WriteJsonString( sb, element.GetString() );
break;
case JsonValueKind.Number:
sb.Append( element.GetRawText() );
break;
case JsonValueKind.True:
sb.Append( "true" );
break;
case JsonValueKind.False:
sb.Append( "false" );
break;
default:
sb.Append( "null" );
break;
}
}
private static string ReadString( JsonElement element, string name )
=> element.ValueKind == JsonValueKind.Object && element.TryGetProperty( name, out var value ) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : "";
private static void WriteJsonString( StringBuilder sb, string value )
{
sb.Append( '"' );
foreach ( var ch in value ?? "" )
{
switch ( ch )
{
case '"': sb.Append( "\\\"" ); break;
case '\\': sb.Append( "\\\\" ); break;
case '\b': sb.Append( "\\b" ); break;
case '\f': sb.Append( "\\f" ); break;
case '\n': sb.Append( "\\n" ); break;
case '\r': sb.Append( "\\r" ); break;
case '\t': sb.Append( "\\t" ); break;
default:
if ( ch < ' ' )
sb.Append( "\\u" ).Append( ((int)ch).ToString( "x4" ) );
else
sb.Append( ch );
break;
}
}
sb.Append( '"' );
}
}