Skip to content
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

Pgp signing #39

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
37 changes: 37 additions & 0 deletions PluginBuilder/AccountSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;

namespace PluginBuilder
{
public class AccountSettings
{
[Display(Name = "Github username")]
public string Github { get; set; }

[Display(Name = "Nostr Npub key")]
public string? Nostr { get; set; }

[Display(Name = "Twitter handle")]
public string? Twitter { get; set; }

[Display(Name = "Email address")]
public string? Email { get; set; }
public List<PgpKey> PgpKeys { get; set; }
}

public class PgpKey
{
public string KeyBatchId { get; set; }
public string KeyUserId { get; set; }
public string Title { get; set; }
public string PublicKey { get; set; }
public string KeyId { get; set; }
public int BitStrength { get; set; }
public bool IsMasterKey { get; set; }
public bool IsEncryptionKey { get; set; }
public string Algorithm { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime AddedDate { get; set; }
public long ValidDays { get; set; }
public int Version { get; set; }
}
}
11 changes: 11 additions & 0 deletions PluginBuilder/Components/MainNav/Default.cshtml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@using PluginBuilder.Enums
@inject Microsoft.AspNetCore.Identity.SignInManager<Microsoft.AspNetCore.Identity.IdentityUser> SignInManager
@model PluginBuilder.Components.MainNav.MainNavViewModel

Expand Down Expand Up @@ -130,6 +131,16 @@
</li>
</ul>
</li>
@if (ViewData.IsActiveCategory(typeof(AccountNavPages)))
{
<li class="nav-item nav-item-sub">
<a id="[email protected]()" class="nav-link @ViewData.ActivePageClass(AccountNavPages.Settings)" asp-controller="Account" asp-action="AccountDetails" text-translate="true">Account Settings</a>
</li>
<li class="nav-item nav-item-sub">
<a id="[email protected]()" class="nav-link @ViewData.ActivePageClass(AccountNavPages.Keys)" asp-controller="Account" asp-action="AccountKeySettings" text-translate="true">Key Settings</a>
</li>
<vc:ui-extension-point location="user-nav" model="@Model" />
}
</ul>
}
</nav>
7 changes: 7 additions & 0 deletions PluginBuilder/Constants/WellKnownTempData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PluginBuilder.Constants;

public class WellKnownTempData
{
public const string SuccessMessage = nameof(SuccessMessage);
public const string ErrorMessage = nameof(ErrorMessage);
}
67 changes: 65 additions & 2 deletions PluginBuilder/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PluginBuilder.DataModels;
using PluginBuilder.ModelBinders;
using PluginBuilder.Constants;
using PluginBuilder.Services;
using PluginBuilder.ViewModels;
using PluginBuilder.ViewModels.Account;
Expand All @@ -12,14 +11,17 @@ namespace PluginBuilder.Controllers
[Authorize]
public class AccountController : Controller
{
private readonly PgpKeyService _pgpKeyService;
private DBConnectionFactory ConnectionFactory { get; }
private UserManager<IdentityUser> UserManager { get; }

public AccountController(
DBConnectionFactory connectionFactory,
PgpKeyService pgpKeyService,
UserManager<IdentityUser> userManager)
{
ConnectionFactory = connectionFactory;
_pgpKeyService = pgpKeyService;
UserManager = userManager;
}

Expand Down Expand Up @@ -60,5 +62,66 @@ public async Task<IActionResult> AccountDetails(AccountDetailsViewModel model)
TempData[TempDataConstant.SuccessMessage] = "Account details updated successfully";
return RedirectToAction(nameof(AccountDetails));
}

[HttpPost("saveaccountkeys")]
public async Task<IActionResult> SaveAccountPgpKeys(AccountKeySettingsViewModel model)
{
try
{
await _pgpKeyService.AddNewPGGKeyAsync(model.PublicKey, model.Title, UserManager.GetUserId(User));
TempData[WellKnownTempData.SuccessMessage] = "Account key added successfully";
return RedirectToAction("AccountKeySettings");
}
catch (Exception ex)
{
ModelState.AddModelError("", ex.Message);
return RedirectToAction("AccountKeySettings");
}
}


[HttpGet("accountkeysettings")]
public async Task<IActionResult> AccountKeySettings()
{
await using var conn = await ConnectionFactory.Open();
string userId = UserManager.GetUserId(User);
var accountSettings = await conn.GetAccountDetailSettings(userId) ?? new AccountSettings();

var pgpKeyViewModels = accountSettings?.PgpKeys?
.GroupBy(k => k.KeyBatchId)
.Select(g => new PgpKeyViewModel
{
BatchId = g.FirstOrDefault()?.KeyBatchId,
Title = g.FirstOrDefault()?.Title,
KeyUserId = g.FirstOrDefault()?.KeyUserId,
KeyId = g.FirstOrDefault(k => k.IsMasterKey)?.KeyId,
Subkeys = string.Join(", ", g.Where(k => !k.IsMasterKey).Select(k => k.KeyId)),
AddedDate = g.FirstOrDefault()?.AddedDate
})
.ToList();
return View(pgpKeyViewModels);
}

[HttpPost("deleteaccountkey/{batchId}")]
public async Task<IActionResult> DeleteAccountPgpKey(string batchId)
{
await using var conn = await ConnectionFactory.Open();
string userId = UserManager.GetUserId(User);
var accountSettings = await conn.GetAccountDetailSettings(userId);
if (accountSettings == null)
{
TempData[WellKnownTempData.ErrorMessage] = "Account settings not found";
return RedirectToAction("AccountKeySettings");
}
int removedCount = accountSettings.PgpKeys?.RemoveAll(k => k.KeyBatchId == batchId) ?? 0;
if (removedCount == 0)
{
TempData[WellKnownTempData.ErrorMessage] = "Invalid batch key";
return RedirectToAction("AccountKeySettings");
}
await conn.SetAccountDetailSettings(accountSettings, userId);
TempData[WellKnownTempData.SuccessMessage] = "Account key deleted successfully";
return RedirectToAction("AccountKeySettings");
}
}
}
54 changes: 51 additions & 3 deletions PluginBuilder/Controllers/PluginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Newtonsoft.Json;
using PluginBuilder.Components.PluginVersion;
using PluginBuilder.Events;
using Microsoft.AspNetCore.Identity;
using PluginBuilder.Constants;

namespace PluginBuilder.Controllers
{
Expand All @@ -17,17 +19,23 @@ public class PluginController : Controller
{
public PluginController(
DBConnectionFactory connectionFactory,
UserManager<IdentityUser> userManager,
BuildService buildService,
EventAggregator eventAggregator)
EventAggregator eventAggregator,
PgpKeyService pgpKeyService)
{
ConnectionFactory = connectionFactory;
BuildService = buildService;
EventAggregator = eventAggregator;
_pgpKeyService = pgpKeyService;
UserManager = userManager;
}

private DBConnectionFactory ConnectionFactory { get; }
private BuildService BuildService { get; }
private EventAggregator EventAggregator { get; }
private UserManager<IdentityUser> UserManager { get; }
private readonly PgpKeyService _pgpKeyService;

[HttpGet("settings")]
public async Task<IActionResult> Settings(
Expand Down Expand Up @@ -190,8 +198,8 @@ public async Task<IActionResult> Build(
long buildId)
{
await using var conn = await ConnectionFactory.Open();
var row = await conn.QueryFirstOrDefaultAsync<(string manifest_info, string build_info, string state, DateTimeOffset created_at, bool published, bool pre_release)>(
"SELECT manifest_info, build_info, state, created_at, v.ver IS NOT NULL, v.pre_release FROM builds b " +
var row = await conn.QueryFirstOrDefaultAsync<(string manifest_info, string build_info, string state, DateTimeOffset created_at, bool published, bool pre_release, bool isbuildsigned)>(
"SELECT manifest_info, build_info, state, created_at, v.ver IS NOT NULL, v.pre_release, isbuildsigned FROM builds b " +
"LEFT JOIN versions v ON b.plugin_slug=v.plugin_slug AND b.id=v.build_id " +
"WHERE b.plugin_slug=@pluginSlug AND id=@buildId " +
"LIMIT 1",
Expand All @@ -217,6 +225,7 @@ public async Task<IActionResult> Build(
vm.ManifestInfo = NiceJson(row.manifest_info);
vm.BuildInfo = buildInfo?.ToString(Formatting.Indented);
vm.DownloadLink = buildInfo?.Url;
vm.IsBuildSigned = row.isbuildsigned;
vm.State = row.state;
vm.CreatedDate = (DateTimeOffset.UtcNow - row.created_at).ToTimeAgo();
vm.Commit = buildInfo?.GitCommit?.Substring(0, 8);
Expand All @@ -234,6 +243,45 @@ public async Task<IActionResult> Build(
return View(vm);
}


[HttpPost("builds/sign/{buildId}")]
public async Task<IActionResult> SignPluginBuild(PluginApprovalStatusUpdateViewModel model,
[ModelBinder(typeof(PluginSlugModelBinder))] PluginSlug pluginSlug,long buildId)
{
await using var conn = await ConnectionFactory.Open();
string userId = UserManager.GetUserId(User);
var accountSettings = await conn.GetAccountDetailSettings(userId);
if (accountSettings == null || accountSettings.PgpKeys == null || !accountSettings.PgpKeys.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "Kindly add new GPG Keys to proceed with plugin action";
return RedirectToAction(nameof(Build), new { pluginSlug, buildId });
}
var manifest_info = await conn.QueryFirstOrDefaultAsync<string>(
"SELECT manifest_info FROM builds b WHERE b.plugin_slug=@pluginSlug AND id=@buildId LIMIT 1",
new
{
pluginSlug = pluginSlug.ToString(),
buildId
});
List<string> publicKeys = accountSettings.PgpKeys.Select(key => key.PublicKey).ToList();
string manifestShasum = _pgpKeyService.ComputeSHA256(NiceJson(manifest_info));
var validateSignature = _pgpKeyService.VerifyPgpMessage(model.ArmoredMessage, manifestShasum, publicKeys);
if (!validateSignature.success)
{
TempData[WellKnownTempData.ErrorMessage] = validateSignature.response;
return RedirectToAction(nameof(Build), new { pluginSlug, buildId });
}
await conn.ExecuteAsync(
"UPDATE builds SET isbuildsigned = true WHERE plugin_slug = @pluginSlug AND id = @buildId",
new
{
pluginSlug = pluginSlug.ToString(),
buildId
});
TempData[WellKnownTempData.SuccessMessage] = $"{model.PluginSlug} signed and verified successfully";
return RedirectToAction(nameof(Build), new { pluginSlug, buildId });
}

private string? NiceJson(string? json)
{
if (json is null)
Expand Down
5 changes: 5 additions & 0 deletions PluginBuilder/Data/Scripts/12.PgpBuildSigning.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE builds
ADD COLUMN isbuildsigned BOOLEAN DEFAULT false;

UPDATE builds
SET isbuildsigned = true;
6 changes: 6 additions & 0 deletions PluginBuilder/Enums/AccountNavPages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace PluginBuilder.Enums;

public enum AccountNavPages
{
Keys, Settings
}
2 changes: 1 addition & 1 deletion PluginBuilder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public void AddServices(IConfiguration configuration, IServiceCollection service
services.AddHostedService<DockerStartupHostedService>();
services.AddHostedService<AzureStartupHostedService>();
services.AddHostedService<PluginHubHostedService>();

services.AddTransient<PgpKeyService>();
services.AddSingleton<DBConnectionFactory>();
services.AddSingleton<BuildService>();
services.AddSingleton<ProcessRunner>();
Expand Down
Loading