mirror of
https://github.com/NecroticBamboo/QRBee.git
synced 2025-12-21 12:11:53 +00:00
Encrypted Client card data block added. Android public and private keeys separated into different files.
This commit is contained in:
parent
81a87262c5
commit
4722585e17
@ -2,49 +2,20 @@
|
||||
{
|
||||
public class ClientCardData
|
||||
{
|
||||
|
||||
public string CardNumber
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string ExpirationDateMMYY
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string ValidFrom
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string CardHolderName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string CVC
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int? IssueNo
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TransactionId { get; set; }
|
||||
public string CardNumber { get; set; }
|
||||
public string ExpirationDateMMYY { get; set; }
|
||||
public string ValidFrom { get; set; }
|
||||
public string CardHolderName { get; set; }
|
||||
public string CVC { get; set; }
|
||||
public int? IssueNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert ClientCardData to string to be used as a source for encryption.
|
||||
/// WARNING: this should always be encrypted and never transmitted in clear text form.
|
||||
/// </summary>
|
||||
/// <returns>Converted string</returns>
|
||||
public string AsString() => $"{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}";
|
||||
public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,13 @@ namespace QRBee.Core.Data
|
||||
set;
|
||||
}
|
||||
|
||||
public string Certificate
|
||||
public string ClientCertificate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string APIServerCertificate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace QRBee.Core.Security
|
||||
{
|
||||
@ -38,6 +39,12 @@ namespace QRBee.Core.Security
|
||||
/// self-signed until CA issues a proper certificate
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
X509Certificate2 LoadPrivateKey();
|
||||
RSA LoadPrivateKey();
|
||||
|
||||
/// <summary>
|
||||
/// Get public key certificate
|
||||
/// </summary>
|
||||
/// <returns>Public key certificate</returns>
|
||||
X509Certificate2 GetCertificate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,6 +47,11 @@ namespace QRBee.Core.Security
|
||||
|
||||
// -------------------------- certificate services --------------------------
|
||||
|
||||
/// <summary>
|
||||
/// API Server certificate
|
||||
/// </summary>
|
||||
X509Certificate2 APIServerCertificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert binary block to X509Certificate2.
|
||||
/// <see cref="X509Certificate2.CreateFromPem"/>
|
||||
|
||||
@ -39,12 +39,13 @@ namespace QRBee.Core.Security
|
||||
/// <inheritdoc/>
|
||||
public byte[] Decrypt(byte[] data)
|
||||
{
|
||||
using var myCert = LoadPrivateKey();
|
||||
using var rsa = myCert.GetRSAPrivateKey();
|
||||
using var rsa = LoadPrivateKey();
|
||||
var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found");
|
||||
return res;
|
||||
}
|
||||
|
||||
public abstract X509Certificate2 APIServerCertificate { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte [] Encrypt(byte[] data, X509Certificate2 destCert)
|
||||
{
|
||||
@ -63,8 +64,7 @@ namespace QRBee.Core.Security
|
||||
/// <inheritdoc/>
|
||||
public byte[] Sign(byte[] data)
|
||||
{
|
||||
using var myCert = LoadPrivateKey();
|
||||
using var rsa = myCert.GetRSAPrivateKey();
|
||||
using var rsa = LoadPrivateKey();
|
||||
var res = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
|
||||
return res;
|
||||
}
|
||||
@ -86,16 +86,12 @@ namespace QRBee.Core.Security
|
||||
return serNo;
|
||||
}
|
||||
|
||||
private X509Certificate2 LoadPrivateKey()
|
||||
private RSA LoadPrivateKey()
|
||||
{
|
||||
if (!PrivateKeyHandler.Exists())
|
||||
PrivateKeyHandler.GeneratePrivateKey(); //TODO: subject name
|
||||
|
||||
var pk = PrivateKeyHandler.LoadPrivateKey();
|
||||
if (!IsValid(pk) )
|
||||
{
|
||||
throw new CryptographicException("CA private key is not valid");
|
||||
}
|
||||
|
||||
return pk;
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ namespace QRBee.Droid.Services
|
||||
/// <inheritdoc/>
|
||||
public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
|
||||
{
|
||||
using var rsa = LoadRsaPrivateKey();
|
||||
using var rsa = LoadPrivateKey();
|
||||
|
||||
var request = new ReadableCertificateRequest
|
||||
{
|
||||
@ -96,85 +96,68 @@ namespace QRBee.Droid.Services
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate EXPORTABLE certificate
|
||||
/// </summary>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <returns></returns>
|
||||
private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName)
|
||||
///// <summary>
|
||||
///// Generate EXPORTABLE certificate
|
||||
///// </summary>
|
||||
///// <param name="subjectName"></param>
|
||||
///// <returns></returns>
|
||||
//private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName)
|
||||
//{
|
||||
// // https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
|
||||
|
||||
// var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
|
||||
|
||||
// using var rsa = RSA.Create(RSABits);
|
||||
// var request = CreateRequest(distinguishedName, rsa);
|
||||
|
||||
// var certificate = request.CreateSelfSigned(
|
||||
// new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
|
||||
// new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
|
||||
// );
|
||||
|
||||
// return certificate;
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// Generate CA certificate request (i.e. with KeyCertSign usage extension)
|
||||
///// </summary>
|
||||
///// <param name="distinguishedName"></param>
|
||||
///// <param name="rsa"></param>
|
||||
///// <returns></returns>
|
||||
//private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
||||
//{
|
||||
// //TODO not supported on Android
|
||||
// var request = new CertificateRequest(
|
||||
// distinguishedName,
|
||||
// rsa,
|
||||
// HashAlgorithmName.SHA256,
|
||||
// RSASignaturePadding.Pkcs1
|
||||
// );
|
||||
|
||||
// request.CertificateExtensions.Add(
|
||||
// new X509KeyUsageExtension(
|
||||
// X509KeyUsageFlags.DataEncipherment
|
||||
// | X509KeyUsageFlags.KeyEncipherment
|
||||
// | X509KeyUsageFlags.DigitalSignature,
|
||||
// false));
|
||||
|
||||
// return request;
|
||||
//}
|
||||
|
||||
|
||||
public X509Certificate2 GetCertificate()
|
||||
{
|
||||
// https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
|
||||
|
||||
var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
|
||||
|
||||
using var rsa = RSA.Create(RSABits);
|
||||
var request = CreateRequest(distinguishedName, rsa);
|
||||
|
||||
var certificate = request.CreateSelfSigned(
|
||||
new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
|
||||
new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
|
||||
);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
|
||||
/// </summary>
|
||||
/// <param name="distinguishedName"></param>
|
||||
/// <param name="rsa"></param>
|
||||
/// <returns></returns>
|
||||
private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
||||
{
|
||||
//TODO not supported on Android
|
||||
var request = new CertificateRequest(
|
||||
distinguishedName,
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1
|
||||
);
|
||||
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DataEncipherment
|
||||
| X509KeyUsageFlags.KeyEncipherment
|
||||
| X509KeyUsageFlags.DigitalSignature,
|
||||
false));
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public X509Certificate2 LoadPrivateKey()
|
||||
{
|
||||
if (_certificate != null)
|
||||
return _certificate;
|
||||
|
||||
// double locking
|
||||
lock ( _syncObject )
|
||||
{
|
||||
if (_certificate != null)
|
||||
return _certificate;
|
||||
|
||||
if (!Exists())
|
||||
throw new CryptographicException("PrivateKey does not exist");
|
||||
|
||||
_certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword);
|
||||
return _certificate;
|
||||
}
|
||||
|
||||
var cert = new X509Certificate2(PrivateKeyFileName);
|
||||
return cert;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachCertificate(X509Certificate2 cert)
|
||||
{
|
||||
// heavily modified version of:
|
||||
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
|
||||
using var rsa = LoadRsaPrivateKey();
|
||||
var bytes = cert.Export(X509ContentType.Cert);
|
||||
|
||||
var newPk = cert.CopyWithPrivateKey(rsa);
|
||||
|
||||
var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword);
|
||||
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
|
||||
File.WriteAllBytes(PrivateKeyFileName, bytes);
|
||||
|
||||
lock ( _syncObject )
|
||||
{
|
||||
@ -184,7 +167,7 @@ namespace QRBee.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
private RSA LoadRsaPrivateKey()
|
||||
public RSA LoadPrivateKey()
|
||||
{
|
||||
var bytes = File.ReadAllBytes(PrivateRsaKeyFileName);
|
||||
var s = CryptoHelper.DecryptStringAES(bytes, VeryBadNeverUsePrivateKeyPassword);
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using QRBee.Core.Security;
|
||||
|
||||
namespace QRBee.Droid.Services
|
||||
{
|
||||
internal class AndroidSecurityService : SecurityServiceBase
|
||||
{
|
||||
private X509Certificate2 _apiServerCertificate;
|
||||
private string ApiServerCertificateFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/ApiServerCertificate.bin";
|
||||
|
||||
public AndroidSecurityService(IPrivateKeyHandler privateKeyHandler)
|
||||
: base(privateKeyHandler)
|
||||
@ -51,6 +54,29 @@ namespace QRBee.Droid.Services
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public override X509Certificate2 APIServerCertificate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_apiServerCertificate != null)
|
||||
{
|
||||
return _apiServerCertificate;
|
||||
}
|
||||
|
||||
if (!File.Exists(ApiServerCertificateFileName))
|
||||
throw new ApplicationException($"File not found: {ApiServerCertificateFileName}");
|
||||
var bytes = File.ReadAllBytes(ApiServerCertificateFileName);
|
||||
_apiServerCertificate = new X509Certificate2(bytes);
|
||||
return _apiServerCertificate;
|
||||
}
|
||||
set
|
||||
{
|
||||
_apiServerCertificate = value;
|
||||
var bytes = _apiServerCertificate.Export(X509ContentType.Cert);
|
||||
File.WriteAllBytes(ApiServerCertificateFileName,bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ namespace QRBee.Services
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
//TODO add ClientId
|
||||
|
||||
public string ClientId { get; set; }
|
||||
public string PIN { get; set; }
|
||||
public bool IsRegistered => !string.IsNullOrWhiteSpace(ClientId);
|
||||
@ -20,7 +20,7 @@ namespace QRBee.Services
|
||||
public string ExpirationDate { get; set; }
|
||||
public string CardHolderName { get; set; }
|
||||
public string CVC { get; set; }
|
||||
public string IssueNo { get; set; }
|
||||
public int? IssueNo { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ namespace QRBee.ViewModels
|
||||
{
|
||||
private readonly IQRScanner _scanner;
|
||||
private readonly ISecurityService _securityService;
|
||||
private readonly ILocalSettings _localSettings;
|
||||
public bool _isAcceptDenyButtonVisible;
|
||||
public bool _isQrVisible;
|
||||
public bool _isScanButtonVisible;
|
||||
@ -21,10 +22,11 @@ namespace QRBee.ViewModels
|
||||
private string _qrCode;
|
||||
private MerchantToClientRequest _merchantToClientRequest;
|
||||
|
||||
public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService)
|
||||
public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService, ILocalSettings localSettings)
|
||||
{
|
||||
_scanner = scanner;
|
||||
_securityService = securityService;
|
||||
_localSettings = localSettings;
|
||||
ScanCommand = new Command(OnScanButtonClicked);
|
||||
AcceptQrCommand = new Command(OnAcceptQrCommand);
|
||||
DenyQrCommand = new Command(OnDenyQrCommand);
|
||||
@ -149,16 +151,14 @@ namespace QRBee.ViewModels
|
||||
|
||||
var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
|
||||
if (!answer) return;
|
||||
|
||||
var settings = _localSettings.LoadSettings();
|
||||
var response = new ClientToMerchantResponse
|
||||
{
|
||||
//TODO get client id from database
|
||||
ClientId = Guid.NewGuid().ToString("D"),
|
||||
ClientId = settings.ClientId,
|
||||
TimeStampUTC = DateTime.UtcNow,
|
||||
MerchantRequest = _merchantToClientRequest
|
||||
|
||||
MerchantRequest = _merchantToClientRequest,
|
||||
EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId)
|
||||
};
|
||||
// TODO Create client signature.
|
||||
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
|
||||
response.ClientSignature = Convert.ToBase64String(clientSignature);
|
||||
|
||||
@ -168,13 +168,30 @@ namespace QRBee.ViewModels
|
||||
IsScanButtonVisible = true;
|
||||
}
|
||||
|
||||
private string EncryptCardData(Settings settings, string transactionId)
|
||||
{
|
||||
var clientCardData = new ClientCardData
|
||||
{
|
||||
TransactionId = transactionId,
|
||||
CardNumber = settings.CardNumber,
|
||||
ExpirationDateMMYY = settings.ExpirationDate,
|
||||
ValidFrom = settings.ValidFrom,
|
||||
CardHolderName = settings.CardHolderName,
|
||||
CVC = settings.CVC,
|
||||
IssueNo = settings.IssueNo
|
||||
};
|
||||
|
||||
var bytes = _securityService.Encrypt(Encoding.UTF8.GetBytes(clientCardData.AsString()),_securityService.APIServerCertificate);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
public void OnDenyQrCommand(object obj)
|
||||
{
|
||||
QrCode = null;
|
||||
IsQrVisible = false;
|
||||
QrCode = null;
|
||||
IsQrVisible = false;
|
||||
IsAcceptDenyButtonVisible = false;
|
||||
IsScanButtonVisible = true;
|
||||
Amount = "";
|
||||
IsScanButtonVisible = true;
|
||||
Amount = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -131,8 +131,7 @@ namespace QRBee.ViewModels
|
||||
{
|
||||
var trans = new MerchantToClientRequest
|
||||
{
|
||||
//TODO get merchant id from database
|
||||
MerchantId = Guid.NewGuid().ToString("D"),
|
||||
MerchantId = _settings.LoadSettings().ClientId,
|
||||
MerchantTransactionId = Guid.NewGuid().ToString("D"),
|
||||
Name = Name,
|
||||
Amount = Amount,
|
||||
|
||||
@ -50,7 +50,7 @@ namespace QRBee.ViewModels
|
||||
public string ExpirationDate { get; set; }
|
||||
public string CardHolderName { get; set; }
|
||||
public string CVC { get; set; }
|
||||
public string IssueNo { get; set; }
|
||||
public int? IssueNo { get; set; }
|
||||
|
||||
public Color Password1Color { get; set; }
|
||||
public Color Password2Color { get; set;}
|
||||
@ -125,7 +125,7 @@ namespace QRBee.ViewModels
|
||||
|
||||
await _settings.SaveSettings(settings);
|
||||
|
||||
if (!_privateKeyHandler.Exists())
|
||||
//if (!_privateKeyHandler.Exists())
|
||||
{
|
||||
_privateKeyHandler.GeneratePrivateKey(settings.Name);
|
||||
}
|
||||
@ -149,6 +149,7 @@ namespace QRBee.ViewModels
|
||||
|
||||
try
|
||||
{
|
||||
//TODO Register if not, otherwise update
|
||||
// FOR TESTING PURPOSES
|
||||
//!settings.IsRegistered
|
||||
if (true)
|
||||
@ -158,10 +159,14 @@ namespace QRBee.ViewModels
|
||||
// Save ClientId to LocalSettings
|
||||
settings = _settings.LoadSettings();
|
||||
settings.ClientId = response.ClientId;
|
||||
|
||||
// Save server public key certificate
|
||||
_securityService.APIServerCertificate = _securityService.Deserialize(response.APIServerCertificate);
|
||||
|
||||
await _settings.SaveSettings(settings);
|
||||
|
||||
// Attach certificate to privateKey (replace self-sighed with server issued certificate)
|
||||
_privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.Certificate));
|
||||
_privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.ClientCertificate));
|
||||
|
||||
var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
|
||||
await page.DisplayAlert("Success", "You have been registered successfully", "Ok");
|
||||
|
||||
@ -62,7 +62,8 @@ namespace QRBee.Api.Services
|
||||
return new RegistrationResponse
|
||||
{
|
||||
ClientId = clientId,
|
||||
Certificate = _securityService.Serialize(clientCertificate)
|
||||
ClientCertificate = _securityService.Serialize(clientCertificate),
|
||||
APIServerCertificate = _securityService.Serialize(_securityService.APIServerCertificate)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,10 @@ namespace QRBee.Api.Services
|
||||
var req = CreateClientCertRequest(distinguishedName, rsa);
|
||||
|
||||
var pk = PrivateKeyHandler.LoadPrivateKey();
|
||||
var clientCert = req.Create(pk,
|
||||
var cert = PrivateKeyHandler.GetCertificate();
|
||||
var newCert = cert.CopyWithPrivateKey(pk);
|
||||
|
||||
var clientCert = req.Create(newCert,
|
||||
DateTimeOffset.UtcNow - TimeSpan.FromDays(1),
|
||||
DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays),
|
||||
Guid.NewGuid()
|
||||
@ -92,6 +95,13 @@ namespace QRBee.Api.Services
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override X509Certificate2 APIServerCertificate
|
||||
{
|
||||
get => PrivateKeyHandler.GetCertificate();
|
||||
set => throw new ApplicationException("Do not call this");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ namespace QRBee.Api.Services
|
||||
public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
|
||||
{
|
||||
//TODO in fact server should create certificate request in standard format if we ever want to get externally sighed certificate.
|
||||
var pk = LoadPrivateKey();
|
||||
var pk = Load();
|
||||
var rsa = pk.GetRSAPrivateKey();
|
||||
|
||||
if (rsa == null)
|
||||
@ -172,7 +172,7 @@ namespace QRBee.Api.Services
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public X509Certificate2 LoadPrivateKey()
|
||||
private X509Certificate2 Load()
|
||||
{
|
||||
if (_certificate != null)
|
||||
return _certificate;
|
||||
@ -191,6 +191,20 @@ namespace QRBee.Api.Services
|
||||
}
|
||||
}
|
||||
|
||||
public RSA LoadPrivateKey()
|
||||
{
|
||||
var pk = Load();
|
||||
return pk.GetRSAPrivateKey()?? throw new ApplicationException("Private key not found");
|
||||
}
|
||||
|
||||
public X509Certificate2 GetCertificate()
|
||||
{
|
||||
var pk = Load();
|
||||
var bytes = pk.Export(X509ContentType.Cert);
|
||||
var cert = new X509Certificate2(bytes);
|
||||
return cert;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachCertificate(X509Certificate2 cert)
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user