Skip to content
Merged
Changes from 2 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
86 changes: 75 additions & 11 deletions eng/devices/windows.cake
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,13 @@ Task("GenerateMsixCert")
.WithCriteria(isPackagedTestRun)
.Does(() =>
{
// We need the key to be in LocalMachine -> TrustedPeople to install the msix signed with the key
// We need the key to be in LocalMachine -> TrustedPeople to install the msix signed with the key.
// Open read-only first so we can detect an existing cert without requiring admin. Only escalate
// to ReadWrite (which requires admin on LocalMachine) when we actually need to create the cert.
var localTrustedPeopleStore = new X509Store("TrustedPeople", StoreLocation.LocalMachine);
localTrustedPeopleStore.Open(OpenFlags.ReadWrite);

// We need to have the key also in CurrentUser -> My so that the msix can be built and signed
// with the key by passing the key's thumbprint to the build
var currentUserMyStore = new X509Store("My", StoreLocation.CurrentUser);
currentUserMyStore.Open(OpenFlags.ReadWrite);
localTrustedPeopleStore.Open(OpenFlags.ReadOnly);
certificateThumbprint = localTrustedPeopleStore.Certificates.FirstOrDefault(c => c.Subject.Contains(certCN))?.Thumbprint;
Comment thread
akoeplinger marked this conversation as resolved.
Outdated
localTrustedPeopleStore.Close();

if (string.IsNullOrEmpty(certificateThumbprint))
{
Expand Down Expand Up @@ -111,14 +109,80 @@ Task("GenerateMsixCert")
cert.FriendlyName = certCN;
}

var tmpCert = new X509Certificate2(cert.Export(X509ContentType.Pfx), "", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// Store the private key in the *user* key container (not the machine container) so the
// current non-elevated user can use it to sign. LocalMachine\TrustedPeople only needs the
// cert's public key for sideload trust validation, so a user-scope private key is enough.
// Using MachineKeySet here would put the key in C:\ProgramData\Microsoft\Crypto\...
// which is unreadable from a non-admin process — signtool then fails with "No certificates
// were found that met all the given criteria" even though the cert is visible in the store.
var tmpCert = new X509Certificate2(cert.Export(X509ContentType.Pfx), "", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
certificateThumbprint = tmpCert.Thumbprint;
localTrustedPeopleStore.Add(tmpCert);

// Writing to LocalMachine\TrustedPeople requires admin. If we don't have it, fail with a
// clear message rather than the raw "Access is denied" from the store.
try
{
localTrustedPeopleStore.Open(OpenFlags.ReadWrite);
localTrustedPeopleStore.Add(tmpCert);
localTrustedPeopleStore.Close();
}
catch (System.Security.Cryptography.CryptographicException ex)
{
throw new Exception(
"Failed to install signing cert into LocalMachine\\TrustedPeople. " +
"This step requires an elevated (administrator) shell on first run. " +
"After the cert is created once, subsequent runs can be performed without elevation.",
ex);
}

// CurrentUser\My only needs admin if the process doesn't own the profile, so do it after
// the LocalMachine write succeeded.
var currentUserMyStore = new X509Store("My", StoreLocation.CurrentUser);
currentUserMyStore.Open(OpenFlags.ReadWrite);
currentUserMyStore.Add(tmpCert);
currentUserMyStore.Close();
}
else
{
// Cert already exists in LocalMachine\TrustedPeople. Make sure it's also in CurrentUser\My
// with a usable user-scoped private key so the build can sign with it. A cert installed by
// an older version of this script may live in CurrentUser\My but reference a private key
// stored in the machine key container (C:\ProgramData\Microsoft\Crypto\...), which is
// unreadable from a non-elevated process — signtool would then fail mid-build. Verify by
// touching the private key, not just by checking HasPrivateKey.
var currentUserMyStore = new X509Store("My", StoreLocation.CurrentUser);
currentUserMyStore.Open(OpenFlags.ReadOnly);
var matchingCert = currentUserMyStore.Certificates
.Cast<X509Certificate2>()
.FirstOrDefault(c => c.Thumbprint == certificateThumbprint);
var usable = false;
if (matchingCert != null && matchingCert.HasPrivateKey)
{
try
{
using var key = matchingCert.GetRSAPrivateKey();
usable = key != null && key.ExportParameters(false) != null;
Comment thread
akoeplinger marked this conversation as resolved.
Outdated
}
catch (System.Security.Cryptography.CryptographicException)
{
// Private key handle exists but the key material is in a container we can't read
// (typically a machine key container without admin rights).
usable = false;
}
}
currentUserMyStore.Close();

localTrustedPeopleStore.Close();
currentUserMyStore.Close();
if (!usable)
Comment thread
akoeplinger marked this conversation as resolved.
Outdated
{
Warning(
"Cert {0} is in LocalMachine\\TrustedPeople but no usable user-scoped private key " +
"was found in CurrentUser\\My (the cert may be missing, or its private key may live " +
"in the machine key container and require elevation to read). The build will not be " +
"able to sign the MSIX. Run this task elevated once to reinstall the cert into both " +
Comment thread
akoeplinger marked this conversation as resolved.
Outdated
"stores with a user-scoped key.",
certificateThumbprint);
}
}

Information("Cert thumbprint: " + certificateThumbprint ?? "null");
});
Expand Down
Loading