180 lines
7.0 KiB
C#
180 lines
7.0 KiB
C#
using System.Text;
|
||
using Newtonsoft.Json;
|
||
using Org.BouncyCastle.Bcpg;
|
||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||
using PluginBuilder.DataModels;
|
||
using PluginBuilder.Util;
|
||
using PluginBuilder.Util.Extensions;
|
||
using PluginBuilder.ViewModels;
|
||
|
||
namespace PluginBuilder.Services;
|
||
|
||
public class GPGKeyService(DBConnectionFactory connectionFactory)
|
||
{
|
||
public bool ValidateArmouredPublicKey(string publicKey, out string message, out PgpKeyViewModel? vm)
|
||
{
|
||
publicKey = publicKey.Trim();
|
||
vm = null;
|
||
if (publicKey.Contains("-----BEGIN PGP PRIVATE KEY BLOCK-----", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
message = "Private key block detected; upload only the public key";
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
using var publicKeyStream = new MemoryStream(Encoding.ASCII.GetBytes(publicKey));
|
||
using var decoderStream = PgpUtilities.GetDecoderStream(publicKeyStream);
|
||
var keyRingBundle = new PgpPublicKeyRingBundle(decoderStream);
|
||
PgpPublicKey? key = null;
|
||
|
||
foreach (var keyRing in keyRingBundle.GetKeyRings())
|
||
{
|
||
foreach (var k in keyRing.GetPublicKeys())
|
||
{
|
||
if (k.GetSignatures().All(sig => (sig.GetHashedSubPackets()?.GetKeyFlags() & PgpKeyFlags.CanSign) == 0))
|
||
continue;
|
||
key = k;
|
||
break;
|
||
}
|
||
|
||
if (key != null)
|
||
break;
|
||
}
|
||
|
||
if (key == null)
|
||
{
|
||
message = "Signing key is required";
|
||
return false;
|
||
}
|
||
|
||
var canSign = key.Algorithm switch
|
||
{
|
||
PublicKeyAlgorithmTag.RsaGeneral or
|
||
PublicKeyAlgorithmTag.RsaSign or
|
||
PublicKeyAlgorithmTag.Dsa or
|
||
PublicKeyAlgorithmTag.ECDsa or
|
||
PublicKeyAlgorithmTag.EdDsa => true,
|
||
_ => false
|
||
};
|
||
if (!canSign)
|
||
{
|
||
message = "Public key provided does not support signing";
|
||
return false;
|
||
}
|
||
|
||
if (key.GetValidSeconds() != 0 && key.CreationTime.AddSeconds(key.GetValidSeconds()) <= DateTimeOffset.UtcNow)
|
||
{
|
||
message = "Key has expired";
|
||
return false;
|
||
}
|
||
|
||
if (key.IsRevoked() || key.GetSignatures().Any(sig => sig.SignatureType == PgpSignature.KeyRevocation))
|
||
{
|
||
message = "Key is revoked";
|
||
return false;
|
||
}
|
||
|
||
vm = new PgpKeyViewModel
|
||
{
|
||
KeyId = key.KeyId.ToString("X"),
|
||
Fingerprint = BitConverter.ToString(key.GetFingerprint()).Replace("-", ""),
|
||
PublicKey = publicKey,
|
||
CreatedDate = key.CreationTime,
|
||
AddedDate = DateTimeOffset.UtcNow,
|
||
ValidDays = key.GetValidSeconds() == 0 ? -1 : (long)(key.CreationTime.AddSeconds(key.GetValidSeconds()) - key.CreationTime).TotalDays,
|
||
Version = key.Version
|
||
};
|
||
message = "Public Key validated successfully";
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
message = "An error occurred while validating public key";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public async Task<SignatureProofResponse> VerifyDetachedSignature(string pluginslug, string userId, byte[] rawSignedBytes, byte[] signatureBytes)
|
||
{
|
||
try
|
||
{
|
||
if (signatureBytes is not { Length: > 0 })
|
||
return new SignatureProofResponse(false, "Signature data is required");
|
||
|
||
var publicKey = await GetPluginOwnerPublicKeys(pluginslug, userId);
|
||
if (string.IsNullOrEmpty(publicKey))
|
||
return new SignatureProofResponse(false, "No public keys found for this user. Kindly update your account profile with your GPG public key");
|
||
|
||
using var decodedStream = PgpUtilities.GetDecoderStream(new MemoryStream(signatureBytes));
|
||
|
||
var sigFact = new PgpObjectFactory(decodedStream);
|
||
PgpSignatureList? sigList = null;
|
||
object? obj;
|
||
while ((obj = sigFact.NextPgpObject()) != null)
|
||
{
|
||
switch (obj)
|
||
{
|
||
case PgpSignatureList list:
|
||
sigList = list;
|
||
break;
|
||
|
||
case PgpCompressedData compressed:
|
||
sigFact = new PgpObjectFactory(compressed.GetDataStream());
|
||
continue;
|
||
}
|
||
|
||
if (sigList != null)
|
||
break;
|
||
}
|
||
|
||
if (sigList == null || sigList.Count == 0)
|
||
return new SignatureProofResponse(false, "No signature found in the provided data");
|
||
|
||
var signature = sigList[0];
|
||
using var pubKeyStream = new MemoryStream(Encoding.ASCII.GetBytes(publicKey));
|
||
var pubBundle = new PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(pubKeyStream));
|
||
var signingKey = pubBundle.GetPublicKey(signature.KeyId);
|
||
if (signingKey == null)
|
||
return new SignatureProofResponse(false, "Signature was created with a key not associated with the user's public key");
|
||
|
||
signature.InitVerify(signingKey);
|
||
signature.Update(rawSignedBytes);
|
||
if (!signature.Verify())
|
||
return new SignatureProofResponse(false, "Signature verification failed – manifest data mismatch");
|
||
|
||
var signatureProof = new SignatureProof
|
||
{
|
||
Armour = Convert.ToBase64String(signatureBytes),
|
||
KeyId = signature.KeyId.ToString("X"),
|
||
Fingerprint = BitConverter.ToString(signingKey.GetFingerprint()).Replace("-", ""),
|
||
SignedAt = signature.CreationTime,
|
||
VerifiedAt = DateTimeOffset.UtcNow
|
||
};
|
||
return new SignatureProofResponse(true, "Signature verified successfully", signatureProof);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new SignatureProofResponse(false, $"Verification failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private async Task<string> GetPluginOwnerPublicKeys(string pluginSlug, string userId)
|
||
{
|
||
await using var conn = await connectionFactory.Open();
|
||
var pluginOwners = await conn.GetPluginOwners(pluginSlug);
|
||
if (pluginOwners is null || pluginOwners.Count <= 0)
|
||
return string.Empty;
|
||
|
||
var owner = pluginOwners.FirstOrDefault(o => o.UserId == userId);
|
||
if (owner == null || string.IsNullOrEmpty(owner.AccountDetail))
|
||
return string.Empty;
|
||
|
||
var accountSettings = JsonConvert.DeserializeObject<AccountSettings>(owner.AccountDetail, CamelCaseSerializerSettings.Instance);
|
||
if (accountSettings?.GPGKey == null || string.IsNullOrEmpty(accountSettings.GPGKey.PublicKey))
|
||
return string.Empty;
|
||
|
||
return accountSettings.GPGKey.PublicKey;
|
||
}
|
||
}
|