Ecv2DotNet 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Ecv2DotNet --version 1.0.0
                    
NuGet\Install-Package Ecv2DotNet -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ecv2DotNet" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ecv2DotNet" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Ecv2DotNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Ecv2DotNet --version 1.0.0
                    
#r "nuget: Ecv2DotNet, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Ecv2DotNet@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Ecv2DotNet&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Ecv2DotNet&version=1.0.0
                    
Install as a Cake Tool

Ecv2DotNet

Ecv2DotNet is a .NET library that provides a simple way to verify the integrity of payloads that are signed/encrypted using the Google ecv2 protocol. Currently it supports ECv2SigningOnly protocol, which is used for signing payloads without encryption. Intention is to eventually support ECv2 protocol as well.

Below is an example of a payload that is signed using the ECv2SigningOnly protocol. This has come from the Google Wallet callback API.

{
  "signature": "MEUCIQCJi26vl+ak17dsHDbZZnRZxm51duUAPiYLwOIr9rVvAAIgGUfR18gpKTq1+Msav0vPrWvC6x9dDRwWFX/b85+jE1k\u003d",
  "intermediateSigningKey": {
    "signedKey": "{\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVsEtOdPMaE+DJDzuCJaO7EJXaHor4Kyklp411iwfBa+5TmdbiEWUXzewA79H0PXjdyRMhKBY99+sh056JB75LQ\\u003d\\u003d\",\"keyExpiration\":\"1754778096000\"}",
    "signatures": [
      "MEUCIC29Ju3bt9kklbbA9QAJZW0hh2zecbHDzGo4hF1zRi1zAiEA6e201l1TEl85Row6XHybfDoewIKC4vYpnrlmUT9WbrE\u003d"
    ]
  },
  "protocolVersion": "ECv2SigningOnly",
  "signedMessage": "{\"classId\":\"1388000000022025937.LOYALTY_CLASS_dada6069-0799-44ec-a38d-c482484902e1\",\"objectId\":\"3388000000022025937.LOYALTY_OBJECT_xxxxxxxxxxxxx\",\"eventType\":\"save\",\"expTimeMillis\":1754114831806,\"count\":1,\"nonce\":\"40a8e5af-5b7f-4ea4-b152-63d96858550e\"}"
}

Installation

dotnet add package Ecv2DotNet

Basic Usage

1. Dependency Injection Setup

NOTES

using AWS.Lambda.Powertools.Logging; using Core.Constants; using Core.Features.Google.Callback; using Core.Shared.ExternalServices.GoogleWallet.Dtos; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.Security; using System.Text; using System.Text.Json;

namespace Core.Shared.ExternalServices.GoogleWallet { public class GooglePayAuthenticationService : IGooglePayAuthenticationService { private readonly HttpClient _httpClient; private readonly string _issuerId;

    public GooglePayAuthenticationService(HttpClient httpClient, string issuerId)
    {
        _issuerId = issuerId ?? throw new ArgumentNullException(nameof(issuerId));
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<bool> IsValidECv2SignatureAsync(CallbackGoogleCardCommand callbackCommand)
    {
        try
        {
            // Step 1: Validate protocol version
            if (callbackCommand.ProtocolVersion != GoogleConstants.Protocol)
            {
                Logger.LogError($"Invalid protocol version: {callbackCommand.ProtocolVersion}");
                return false;
            }

            // Step 2: Validate recipient ID in signed message
            if (!ValidateRecipientId(callbackCommand.SignedMessage))
            {
                Logger.LogError("Recipient ID validation failed");
                return false;
            }

            var publicKeysResponse = await FetchGooglePublicKeysAsync();

            var result = VerifyCallbackSignatureInternal(callbackCommand, publicKeysResponse);

            return result; 

        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Failed to process Google public keys for ECv2 signature validation.");
            return false;
        }
    }

    private bool ValidateRecipientId(SignedMessageData signedMessage)
    {
        try
        {
            // Extract issuer ID from classId (format: "issuerId.CLASS_SUFFIX")
            if (!string.IsNullOrEmpty(signedMessage.ClassId))
            {
                var classIdParts = signedMessage.ClassId.Split('.');
                if (classIdParts.Length > 0 && classIdParts[0] == _issuerId)
                {
                    return true;
                }
            }

            // Extract issuer ID from objectId (format: "issuerId.OBJECT_SUFFIX")
            if (!string.IsNullOrEmpty(signedMessage.ObjectId))
            {
                var objectIdParts = signedMessage.ObjectId.Split('.');
                if (objectIdParts.Length > 0 && objectIdParts[0] == _issuerId)
                {
                    return true;
                }
            }

            Logger.LogError($"Recipient ID mismatch. Expected: {_issuerId}, ClassId: {signedMessage.ClassId}, ObjectId: {signedMessage.ObjectId}");
            return false;
        }
        catch (Exception ex)
        {
            Logger.LogError($"Error validating recipient ID: {ex.Message}");
            return false;
        }
    }

    private async Task<GooglePublicKeysResponseDto> FetchGooglePublicKeysAsync()
    {
        var response = await _httpClient.GetStringAsync(GoogleConstants.PublicKeyUrl);
        return JsonSerializer.Deserialize<GooglePublicKeysResponseDto>(response);
    }

    private bool VerifyIntermediateSignature(CallbackGoogleCardCommand callbackCommand, byte[] generatedSignature, GooglePublicKeysResponseDto googleKeys)
    {
        List<bool> results = [];
        foreach (GooglePublicKey key in googleKeys.Keys)
        {
            foreach (string internalSignature in callbackCommand.IntermediateSigningKey.Signatures)
            {
                results.Add(VerifySignature(key.KeyValue, generatedSignature, Convert.FromBase64String(internalSignature)));
            }
        }

        return results.Any(x => x == true);
    }

    private bool VerifySignature(string key, byte[] generatedSignature, byte[] expectedSignature)
    {
        byte[] keyBytes = Org.BouncyCastle.Utilities.Encoders.Base64.Decode(key);

        ECPublicKeyParameters? signingKey;
        try
        {
            signingKey = (ECPublicKeyParameters)PublicKeyFactory.CreateKey(keyBytes);
        }
        catch (Exception)
        {
            return false;
        }

        var dsaSigner = new DsaDigestSigner(new ECDsaSigner(), new Sha256Digest());
        dsaSigner.Init(false, signingKey);
        dsaSigner.BlockUpdate(generatedSignature, 0, generatedSignature.Length);
        return dsaSigner.VerifySignature(expectedSignature);
    }

    private bool VerifyMessageSignature(CallbackGoogleCardCommand callbackCommand, byte[] generatedSignature)
    {
        byte[] signatureBytes = Convert.FromBase64String(callbackCommand.Signature);
        string? intermediateKey = callbackCommand.IntermediateKeyData.KeyValue;
        if (intermediateKey == null)
            return false;

        return VerifySignature(intermediateKey, generatedSignature, signatureBytes);
    }

    private static byte[] GetLengthRepresentation(string str)
    {
        byte[] strBytes = Encoding.UTF8.GetBytes(str);
        byte[] bytes = BitConverter.GetBytes(strBytes.Length);

        return bytes;
    }

    private bool VerifyCallbackSignatureInternal(CallbackGoogleCardCommand? callbackCommand, GooglePublicKeysResponseDto googleKeys)
    {
        if (callbackCommand == null)
            return false;

        // format of signedStringForIntermediateSigningKeySignature:
        // length_of_sender_id || sender_id || length_of_protocol_version || protocol_version || length_of_signed_key || signed_key

        string senderId = GoogleConstants.SenderId;

        byte[] signedStringForIntermediateSigningKeySignature =
        [
            .. GetLengthRepresentation(senderId),
            .. Encoding.UTF8.GetBytes(senderId),
            .. GetLengthRepresentation(callbackCommand.ProtocolVersion),
            .. Encoding.UTF8.GetBytes(callbackCommand.ProtocolVersion),
            .. GetLengthRepresentation(callbackCommand.IntermediateSigningKey.SignedKey),
            .. Encoding.UTF8.GetBytes(callbackCommand.IntermediateSigningKey.SignedKey),
        ];

        if (!VerifyIntermediateSignature(callbackCommand, signedStringForIntermediateSigningKeySignature, googleKeys))
            return false;

        if (callbackCommand.IntermediateKeyData.KeyExpiration == null)
            return false;

        if (!IsFutureExpiry(long.Parse(callbackCommand.IntermediateKeyData.KeyExpiration)))
        {
            Logger.LogError("Intermediate key has expired: {KeyExpiration}", callbackCommand.IntermediateKeyData.KeyExpiration);
            return false;
        }

        // format of signedStringForMessageSignature:
        // length_of_sender_id || sender_id || length_of_recipient_id || recipient_id || length_of_protocolVersion || protocolVersion || length_of_signedMessage || signedMessage

        byte[] signedStringForMessageSignature =
        [
            .. GetLengthRepresentation(senderId),
            .. Encoding.UTF8.GetBytes(senderId),
            .. GetLengthRepresentation(_issuerId),
            .. Encoding.UTF8.GetBytes(_issuerId),
            .. GetLengthRepresentation(callbackCommand.ProtocolVersion),
            .. Encoding.UTF8.GetBytes(callbackCommand.ProtocolVersion),
            .. GetLengthRepresentation(callbackCommand.OriginalSignedMessageJson),
            .. Encoding.UTF8.GetBytes(callbackCommand.OriginalSignedMessageJson)
        ];

        if (callbackCommand.SignedMessage?.ExpTimeMillis == null)
            return false;

        // If expired
        if (!IsFutureExpiry(callbackCommand.SignedMessage.ExpTimeMillis))
        {
            Logger.LogError("Signed message has expired: {ExpTimeMillis}", callbackCommand.SignedMessage.ExpTimeMillis);
            return false;
        }

        return VerifyMessageSignature(callbackCommand, signedStringForMessageSignature);
    }

    private bool IsFutureExpiry(long epochTime)
    {

        var expiryDate = DateTimeOffset.FromUnixTimeMilliseconds(epochTime);
        var currentDate = DateTimeOffset.UtcNow;
        if (expiryDate < currentDate)
        {
            Logger.LogError("Expiry date is in the past: {ExpiryDate}", expiryDate);
            return false;
        }
        return true;
    }
}

}

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 85 8/2/2025
1.0.0 46 8/2/2025