Skip to content

fix: correct validation for cert chain #11

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
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions src/SignedDataVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@
namespace Mimo.AppStoreServerLibrary;

public class SignedDataVerifier(
byte[] appleRootCertificates,
byte[][] appleRootCertificates,
bool enableOnlineChecks,
AppStoreEnvironment environment,
string bundleId
)
{
// To compatibility with the previous version (<= 0.1.0) of the constructor
public SignedDataVerifier(
byte[] appleRootCertificate,
bool enableOnlineChecks,
AppStoreEnvironment environment,
string bundleId
)
: this([appleRootCertificate], enableOnlineChecks, environment, bundleId) { }

// It's recommended to reuse the JsonSerializerOptions instance.
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-8-0#reuse-jsonserializeroptions-instances
private readonly JsonSerializerOptions jsonSerializerOptions = new()
Expand Down Expand Up @@ -198,9 +207,6 @@ private bool CheckCertificateChain(X509Certificate2Collection certificates)
{
var chain = new X509Chain();

// Apple root intermediate certificate https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
var rootCertificate = new X509Certificate2(appleRootCertificates);

// Configure the chain to check for certificate revocation online.
// Online check can result in a longer delay while the certificate authority is contacted.
// It's still unclear if OCSP is supported by Apple, see following comment in the Java Library code base :
Expand All @@ -209,28 +215,31 @@ private bool CheckCertificateChain(X509Certificate2Collection certificates)
// The default value for DisableOnlineCertificateRevocationCheck is false
chain.ChainPolicy.RevocationMode = enableOnlineChecks ? X509RevocationMode.Online : X509RevocationMode.NoCheck;

// By allowing unknown certificates we can avoid adding the Apple root certificate to the trust store
// It's one less step to take when setting up the environment (docker file)
// Not setting this flag will result in a "Chain validation failed. self-signed certificate - Status : UntrustedRoot" error when the app is running in a container
// See following issue for more details about this flag behavior : https://github.com/dotnet/runtime/issues/26449#issue-558387269
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
// We need to set the trust mode to custom root trust so we can add our own root certificate
// This is needed because the root certificate is not in the default trust store
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;

foreach (X509Certificate2 cert in certificates)
// Add the root certificates to the trust store
foreach (var rootCert in appleRootCertificates)
{
//Add the intermediate certificates to the extra store so they can be used to build the chain
chain.ChainPolicy.ExtraStore.Add(cert);
chain.ChainPolicy.CustomTrustStore.Add(new X509Certificate2(rootCert));
}

bool isValid = chain.Build(rootCertificate);
//Add the intermediate certificates to the extra store so they can be used to build the chain
chain.ChainPolicy.ExtraStore.Add(certificates[1]);

bool isValid = chain.Build(certificates[0]);

if (!isValid)
{
string message = "Chain validation failed. ";
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
{
message += chainStatus.StatusInformation + " - Status : " + chainStatus.Status;
}

var message =
"Chain validation failed. "
+ string.Join(
" -> ",
chain.ChainStatus.Select(chainStatus =>
chainStatus.StatusInformation + " - Status : " + chainStatus.Status
)
);
throw new VerificationException(message);
}

Expand Down
24 changes: 12 additions & 12 deletions tests/SignedDataVerifierTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async Task VerifyAndDecode_TestNotification_Success()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -74,7 +74,7 @@ public async Task VerifyAndDecode_AlgParameterIsUnsupported_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -98,7 +98,7 @@ public async Task VerifyAndDecode_JWSIsMissingAPart_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -122,7 +122,7 @@ public async Task VerifyAndDecode_Nox5cParameter_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -146,7 +146,7 @@ public async Task VerifyAndDecode_ChainCertificateCompromised_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -155,7 +155,7 @@ public async Task VerifyAndDecode_ChainCertificateCompromised_Fails()
() => dataVerifier.VerifyAndDecodeNotification(testNotificationPayload)
);

Assert.Contains("Payload signature could not be verified", exception.Message);
Assert.Contains("Chain validation failed", exception.Message);
}

[Fact]
Expand All @@ -169,7 +169,7 @@ public async Task VerifyAndDecode_InvalidSignature_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand Down Expand Up @@ -198,7 +198,7 @@ public async Task VerifyAndDecode_RenewalInfo_Success()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -222,12 +222,12 @@ public async Task VerifyAndDecode_TransactionInfo_Success()
}
*/
string didRenewNotificationPayload = await File.ReadAllTextAsync(
"./MockedSignedData/InputFor_VerifyAndDecode_RenewalInfo_Success.txt"
"./MockedSignedData/InputFor_VerifyAndDecode_TransactionInfo_Success.txt"
);

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
BundleId
);
Expand All @@ -250,7 +250,7 @@ public async Task VerifyAndDecode_WrongBundleId_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
AppStoreEnvironment.Sandbox,
wrongBundleId
);
Expand All @@ -273,7 +273,7 @@ public async Task VerifyAndDecode_WrongEnvironment_Fails()

var dataVerifier = new SignedDataVerifier(
Convert.FromBase64String(RootCaBase64Encoded),
true,
false,
wrongEnvironment,
BundleId
);
Expand Down
Loading