It took me some time to figure out how to verify a Google Play In-App Billing signature in ASP.NET so I thought I would share how I did it on StackOverflow.
As described in Implementing In-app Billing (IAB Version 3):
To help ensure the integrity of the transaction information that is sent to your application, Google Play signs the JSON string that contains the response data for a purchase order. Google Play uses the private key that is associated with your application in the Developer Console to create this signature. The Developer Console generates an RSA key pair for each application.
Note:To find the public key portion of this key pair, open your application's details in the Developer Console, then click on Services & APIs, and look at the field titled Your License Key for This Application.
The Base64-encoded RSA public key generated by Google Play is in binary encoded, X.509 subjectPublicKeyInfo DER SEQUENCE format. It is the same public key that is used with Google Play licensing.
When your application receives this signed response you can use the public key portion of your RSA key pair to verify the signature. By performing signature verification you can detect responses that have been tampered with or that have been spoofed. You can perform this signature verification step in your application; however, if your application connects to a secure remote server then we recommend that you perform the signature verification on that server.
I will self-answer this question with the code I wrote to decode the binary encoded, X.509 public key in DER sequence format and use the .Net RSA API's to verify the signature.
Google Play signs the purchase info (which includes your developer payload) with a PKCS #1 version 1.5 signature. To verify the signature you need the signature (signed purchase info), the raw purchase info text and the public key which is available in the Services & API settings in the Google Play Developer Console.
The public key is encoded in ASN.1 format with DER (which is a subset of BER).
Assuming the purchase info and signature all available as strings that were posted to your web form or MVC controller, and the public key is available in a string loaded from a database or web.config, with the purchase info being raw and the signature and key being Base64 encoded, this code converts them to byte arrays and calls a method to verify the signature:
byte[] purchaseInfoBytes = Encoding.UTF8.GetBytes(purchaseInfo);
byte[] signatureBytes = Convert.FromBase64String(signature);
byte[] publicKeyBytes = Convert.FromBase64String(publicKey);
result = CryptUtil.VerifySignature_2048_Bit_PKCS1_v1_5(
purchaseInfoBytes,
signatureBytes,
publicKeyBytes);
Here is the code for a simple class that decodes the public key and verifies the signature. The comments inside the class explain how the signature is decoded and include links to helpful articles about ASN.1, PKCS #1 version 1.5 and DER (BER) encoding.
using System;
using System.Security.Cryptography;
using System.Text;
namespace YourNamespace.Cryptography
{
public static class CryptUtil
{
public static RSAParameters GetRsaParameters_2048_Bit_PKCS1_v1_5(byte[] publicKey)
{
// From RFC 2313, PKCS #1, Version 1.5:https://www.rfc-editor.org/rfc/rfc2313
// 7.1 Public-key syntax
//
// An RSA public key shall have ASN.1 type RSAPublicKey:
//
// RSAPublicKey ::= SEQUENCE {
// modulus INTEGER, -- n
// publicExponent INTEGER -- e }
//
// (This type is specified in X.509 and is retained here for
// compatibility.)
//
// The fields of type RSAPublicKey have the following meanings:
//
// o modulus is the modulus n.
//
// o publicExponent is the public exponent e.
//
// BER Encoding
// http://en.wikipedia.org/wiki/Distinguished_Encoding_Rules#DER_encoding
//
// ASN.1 Format with DER (subset of BER) encoding
// http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
// It's important to know that the RSAPublicKey is encoded in an ASN.1 (Abstract Syntax Notation One)
// representation using DER encoding. I had to use a couple articles on Wikipedia to understand
// ASN.1 and then I manually decoded the public key to determine where the modulus and exponent were
// located within the 2048 bit public key from Google.
//
// Bytes of sample 2048 bit Public Key (hexadecimal) with ASN.1 decoding shown for each byte
// 30 Identifier: 30 hex = 00110000, P/C = Constructed (1), TAG = SEQUENCE (10000)
// 82 Length: 82 hex = 130 decimal = 10000010, Long Form Length with 2 octects for length
// 01 Byte 1/2 of long form length
// 22 Byte 2/2 of long form length, 0x01 0x22, 00000001 00100010 = 290 bytes
// 30 Identifier: 30 hex = 00110000, P/C = Constructed (1), TAG = SEQUENCE (10000)
// 0d Length: 0d hex = 13 decimal
// 06 Identifier: 06 hex = 00000110, P/C = Primitive (0), TAG = OBJECT IDENTIFIER (00110)
// 09 Length: 09 hex = 9 decimal
// 2a Byte 1/9 of OBJECT IDENTIFIER
// 86 Byte 2/9 of OBJECT IDENTIFIER
// 48 Byte 3/9 of OBJECT IDENTIFIER
// 86 Byte 4/9 of OBJECT IDENTIFIER
// f7 Byte 5/9 of OBJECT IDENTIFIER
// 0d Byte 6/9 of OBJECT IDENTIFIER
// 01 Byte 7/9 of OBJECT IDENTIFIER
// 01 Byte 8/9 of OBJECT IDENTIFIER
// 01 Byte 9/9 of OBJECT IDENTIFIER
// 05 Identifier: 05 hex = 00000101, P/C = Primitive (0), TAG = NULL (00101)
// 00 Length: 00 hex = 0 decimal
// 03 Identifier: 03 hex = 00000011, P/C = Primitive (0), TAG = BIT STRING (00011)
// 82 Length: 82 hex = 130 decimal = 10000010, Long Form Length with 2 octects for length
// 01 Byte 1/2 of long form length
// 0f Byte 2/2 of long form length, 0x01 0x0f, 00000001 00010000 = 272 bytes
// 00 ???? Why 0, what does this mean?
// 30 Identifier: 30 hex = 00110000, P/C = Constructed (1), TAG = SEQUENCE (10000)
// 82 Length: 82 hex = 130 decimal = 10000010, Long Form Length with 2 octects for length
// 01 Byte 1/2 of long form length
// 0a Byte 2/2 of long form length, 0x01 0x0a, 00000001 00001010 = 266 bytes
// 02 Identifier: 02 hex = 00000010, P/C = Primitive (0), TAG = INTEGER (00010)
// 82 Length: 82 hex = 130 decimal = 10000010, Long Form Length with 2 octects for length
// 01 Byte 1/2 of long form length
// 01 Byte 2/2 of long form length, 0x01 0x01, 00000001 00000001 = 257 bytes
// 00 Byte 1/257 of modulus (padded left with a 0, leaves 256 actual values)
// a9 Byte 2/257 of modulus... public key (modulus) starts here??
// 87 Byte 3/257 of modulus
// ....
// 8f Byte 255/257 of modulus
// 14 Byte 256/257 of modulus93 Byte 257/257 of modulus
// 02 Identifier: 02 hex = 00000010, P/C = Primitive (0), TAG = INTEGER (00010)
// 03 Length: 03 hex = 3 decimal
// 01 Byte 1/3 of exponent
// 00 Byte 2/3 of exponent
// 01 Byte 3/3 of exponent
// Modulus starts at byte offset 33 and is 2048 bits (256 bytes)
// Exponent starts at byte offset 291 and is 3 bytes
RSAParameters rsaParameters = new RSAParameters();
int modulusOffset = 33; // See comments above
int modulusBytes = 256; // 2048 bit key
int exponentOffset = 291; // See comments above
int exponentBytes = 3; // See comments above
byte[] modulus = new byte[modulusBytes];
for (int i = 0; i < modulusBytes; i++)
modulus[i] = publicKey[modulusOffset + i];
byte[] exponent = new byte[exponentBytes];
for (int i = 0; i < exponentBytes; i++)
exponent[i] = publicKey[exponentOffset + i];
rsaParameters.Modulus = modulus;
rsaParameters.Exponent = exponent;
return rsaParameters;
}
public static bool VerifySignature_2048_Bit_PKCS1_v1_5(byte[] data, byte[] signature, byte[] publicKey)
{
// Links for information about PKCS #1 version 1.5:
// RFC 2313: https://www.rfc-editor.org/rfc/rfc2313
// PKCS #1 on Wikipedia: http://en.wikipedia.org/wiki/PKCS_1
// Compute an SHA1 hash of the raw data
SHA1 sha1 = SHA1.Create();
byte[] hash = sha1.ComputeHash(data);
// Specify the public key
RSAParameters rsaParameters = GetRsaParameters_2048_Bit_PKCS1_v1_5(publicKey);
// Use RSACryptoProvider to verify the signature with the public key
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048);
rsa.ImportParameters(rsaParameters);
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA1");
return rsaDeformatter.VerifySignature(hash, signature);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With