I have some C# code (.NET Framework 4.7.2 – unfortunately can’t be upgraded) that generates an RSA key pair using the BouncyCastle library, and then converts it to the .NET-native RSA:
var bouncyKeyGen = new RsaKeyPairGenerator();
bouncyKeyGen.Init(
new RsaKeyGenerationParameters(
new BigInteger("10001", 16),
SecureRandom.GetInstance("SHA1PRNG"),
1024,
80
)
);
var bouncyKeyPair = bouncyKeyGen.GenerateKeyPair();
var nativeKeyPair = DotNetUtilities.ToRSA(keys.Private as RsaPrivateCrtKeyParameters);
However, when calling this last .ToRSA
method from my unit tests (running on a GitLab hosted Windows runner) I get a CryptographicException:
System.Security.Cryptography.CryptographicException : Access is denied.
at System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr)
at System.Security.Cryptography.Utils._CreateCSP(CspParameters param, Boolean randomKeyContainer, SafeProvHandle& hProv)
at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters parameters, Boolean randomKeyContainer)
at System.Security.Cryptography.Utils.GetKeyPairHelper(CspAlgorithmType keyType, CspParameters parameters, Boolean randomKeyContainer, Int32 dwKeySize, SafeProvHandle& safeProvHandle, SafeKeyHandle& safeKeyHandle)
at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair()
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(Int32 dwKeySize, CspParameters parameters, Boolean useDefaultKeySize)
at Org.BouncyCastle.Security.DotNetUtilities.CreateRSAProvider(RSAParameters rp)
at MyCode.Method in C:GitLab-Runnerbuildspathtomycode.cs:line 120
It works fine when specifying an explicit CspParameter
with Flags = CspProviderFlags.UseMachineKeyStore
– but doing so would change the behavior for the sake of allowing the unit tests to run on GitLab’s hosted Windows runner, something I am not interested in.
What is causing this “Access is denied” error, and how can I fix the environment to allow the RSA key to be converted?
Unfortunately following the stack trace leads to an internal call for the _CreateCSP
method, which means I can’t get more information on what the “Access is denied” error relates to.
From reading a variety of blog posts and Microsoft documentation, I have gathered that Windows persists RSA keys to a directory by default, and that the unhelpful “Access is denied” can be a result of the user not having access to those directories. I have tried testing access to all the directories mentioned in Microsoft’s documentation, prior to running my script in the GitLab runner:
test:
# ...
before_script:
- |
# all the paths used for RSA key storage must exist - apparently that's not the case on the hosted runners
# see https://learn.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval
$UserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
$AppdataStoragePath = Join-Path $env:APPDATA 'MicrosoftCryptoRSA';
$ProfilesStoragePath = Join-Path $env:ProgramData 'MicrosoftCryptoRSA';
$KeyStoragePaths = $(
(Join-Path $ProfilesStoragePath 'MachineKeys'),
(Join-Path $ProfilesStoragePath 'S-1-5-18'),
(Join-Path $ProfilesStoragePath 'S-1-5-19'),
(Join-Path $ProfilesStoragePath 'S-1-5-20'),
(Join-Path $AppdataStoragePath $UserSid),
(Join-Path $env:APPDATA 'MicrosoftCryptoKeys'),
(Join-Path $env:ProgramData 'MicrosoftCryptoSystemKeys')
);
foreach ($Dir in $KeyStoragePaths) {
if (-not (Test-Path $Dir -ErrorAction Continue)) {
Write-Host "Creating directory for key storage: $Dir";
New-Item -ItemType Directory -Path $Dir -Force -ErrorAction Continue;
} else {
Write-Host "Directory for key storage already exists: $Dir";
}
$Rule =New-Object System.Security.AccessControl.FileSystemAccessRule("Everyone", "FullControl", "Allow")
$Acl = Get-Acl $Dir
$Acl.SetAccessRule($Rule)
$Acl | Set-Acl -Path $Dir
Write-Host " Can access directory for key storage (Test-Path)? $(Test-Path $Dir)";
Set-Content -Path (Join-Path $Dir 'test.txt') -Value 'Test' -Force -ErrorAction Continue;
Write-Host " Read data from test.txt: $(Get-Content (Join-Path $Dir 'test.txt') -ErrorAction Continue)";
}
Each of them exists and can be written to by the running user. Unfortunately the error still occurs.