diff --git a/.gitignore b/.gitignore index 024762d..01ac543 100644 --- a/.gitignore +++ b/.gitignore @@ -312,3 +312,5 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ +.gnupg/ +secrets/ diff --git a/.gnupg/.gitkeep b/.gnupg/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 9304667..bf1531e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SecureSign ========== -SecureSign is a simple code signing server. It is designed to allow Authenticode signing of build artifacts in a secure way, without exposing the private key. +SecureSign is a simple code signing server. It is designed to allow Authenticode and GPG signing of build artifacts in a secure way, without exposing the private key. Features ======== @@ -12,12 +12,18 @@ Features Prerequisites ============= - [ASP.NET Core 2.0 runtime](https://www.microsoft.com/net/download/core#/runtime) must be installed - - On Linux, [osslsigncode](https://sourceforge.net/projects/osslsigncode/) needs to be installed. On Debian-based distros, you can `apt-get install osslsigncode`. - - On Windows, [signtool](https://docs.microsoft.com/en-us/dotnet/framework/tools/signtool-exe) needs to be installed. This comes with the Windows SDK. + - For Authenticode: + - On Linux, [osslsigncode](https://sourceforge.net/projects/osslsigncode/) needs to be installed. On Debian-based distros, you can `apt-get install osslsigncode`. + - On Windows, [signtool](https://docs.microsoft.com/en-us/dotnet/framework/tools/signtool-exe) needs to be installed. This comes with the Windows SDK. + - For GPG: + - On Linux, `libgpgme.so.11` needs to be available. On Debian and Ubuntu, install the [libgpgme11](https://packages.debian.org/stretch/libgpgme11) package. + - On Windows, you will need to install [Gpg4Win](https://www.gpg4win.org/). Usage ===== -Before using SecureSign, you need to add your private signing keys. Ensure you have the key as a `.pfx` file, and then use the `addkey` command: +Before using SecureSign, you need to add your private signing keys. For Authenticode, ensure you have the key as a `.pfx` file. For GPG, ensure you export the secret key without a passphrase, and name the file with a `.gpg` extension. + +Once you have your key file, use the `addkey` command: ``` $ dotnet SecureSign.Tools.dll addkey /tmp/my_key.pfx @@ -31,7 +37,7 @@ Valid from 2016-11-20 1:51:47 PM until 2017-12-31 4:00:00 PM Secret Code: 2eiONi53ihUkxxewk5VliJO29hLM3S68LHkTkt9aKuX4dgRop99zw ``` -This secret code is the decryption key required to create access tokens that use this key. It is not saved anywhere, so if you lose it you will need to reinstall the key. +This secret code is the decryption key required to create access tokens that use this key. It is not saved anywhere, so if you lose it you will need to reinstall the key. For GPG, you will see **multiple** secret codes - One for each subkey. Once the key has been added, create an access token for it using the `addtoken` command: ``` @@ -52,14 +58,14 @@ Some of the access token information is saved into `accessTokenConfig.json`. Rem Now that an access token has been created, you can start the server: ``` -$ ASPNETCORE_ENVIRONMENT=Production ASPNETCORE_URLS=http://*:5000/ dotnet SecureSign.Web.dll +$ dotnet SecureSign.Web.dll Hosting environment: Production Content root path: /var/www/securesign Now listening on: http://[::]:5000 Application started. Press Ctrl+C to shut down. ``` -To sign a file, send a POST request to `/sign/authenticode`. This can contain a URL to the artifact to sign: +To Authenticode sign a file, send a POST request to `/sign/authenticode`. This can contain a URL to the artifact to sign: ``` curl --show-error --fail \ -X POST \ @@ -79,11 +85,31 @@ curl --show-error --fail \ http://localhost:5000/sign/authenticode ``` +Similarly, for GPG, send a POST request to `/sign/gpg` instead. + HTTPS ----- -To use TLS, first obtain a certificate in .pfx format. Then, pass in the certificate file name and password as environment variables: +To use TLS, first obtain a certificate in .pfx format. Then, modify `appsettings.json` to specify the path to the certificate: ``` -$ ASPNETCORE_ENVIRONMENT=Production LISTEN_IP=* HTTPS_PORT=12345 HTTPS_CERT=test.pfx HTTPS_CERT_PASSWORD=password1 dotnet SecureSign.Web.dll +{ + "Kestrel": { + "EndPoints": { + "Https": { + "Url": "https://localhost:5001", + "Certificate": { + "Path": "", + "Password": "" + } + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning" + } + } + } +} ``` Restricting Usage of Access Tokens diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 0000000..ba20f24 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + $(MSBuildProgramFiles32)\dotnet\dotnet + $(ProgramW6432)\dotnet\dotnet + + \ No newline at end of file diff --git a/src/SecureSign.Core/Extensions/SecureStringExtensions.cs b/src/SecureSign.Core/Extensions/SecureStringExtensions.cs new file mode 100644 index 0000000..9a29037 --- /dev/null +++ b/src/SecureSign.Core/Extensions/SecureStringExtensions.cs @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace SecureSign.Core.Extensions +{ + public static class SecureStringExtensions + { + public static string ToAnsiString(this SecureString value) + { + var valuePtr = IntPtr.Zero; + try + { + valuePtr = Marshal.SecureStringToGlobalAllocAnsi(value); + return Marshal.PtrToStringAnsi(valuePtr); + } + finally + { + Marshal.ZeroFreeGlobalAllocAnsi(valuePtr); + } + } + } +} diff --git a/src/SecureSign.Core/Extensions/ServiceCollectionExtensions.cs b/src/SecureSign.Core/Extensions/ServiceCollectionExtensions.cs index 78a309a..95f4812 100644 --- a/src/SecureSign.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/SecureSign.Core/Extensions/ServiceCollectionExtensions.cs @@ -8,9 +8,11 @@ using System; using System.Collections.Generic; using System.IO; +using Libgpgme; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using SecureSign.Core.Models; using SecureSign.Core.Signers; @@ -34,11 +36,22 @@ public static IServiceCollection AddSecureSignCore(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); // Configuration services.Configure(config.GetSection("Paths")); services.Configure>(config.GetSection("AccessTokens")); + // GPG + services.AddScoped(provider => + { + var pathConfig = provider.GetRequiredService>(); + var ctx = new Context(); + ctx.SetEngineInfo(Protocol.OpenPGP, null, pathConfig.Value.GpgHome); + ctx.PinentryMode = PinentryMode.Error; + return ctx; + }); + return services; } diff --git a/src/SecureSign.Core/ISecretStorage.cs b/src/SecureSign.Core/ISecretStorage.cs index 721e272..063a03c 100644 --- a/src/SecureSign.Core/ISecretStorage.cs +++ b/src/SecureSign.Core/ISecretStorage.cs @@ -53,5 +53,11 @@ public interface ISecretStorage /// Name of the secret file /// Full file path for the secret file string GetPathForSecret(string name); + + /// + /// Throws an exception if a secret with the specified name already exists. + /// + /// Name of the secret file + void ThrowIfSecretExists(string name); } } diff --git a/src/SecureSign.Core/KeyType.cs b/src/SecureSign.Core/KeyType.cs new file mode 100644 index 0000000..cbc0c54 --- /dev/null +++ b/src/SecureSign.Core/KeyType.cs @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace SecureSign.Core +{ + /// + /// Represents a type of signing key supported by SecureSign + /// + public enum KeyType + { + /// + /// Authenticode (eg. to sign Windows installers) + /// + Authenticode, + + /// + /// GnuPG + /// + Gpg, + } +} diff --git a/src/SecureSign.Core/KeyTypeUtils.cs b/src/SecureSign.Core/KeyTypeUtils.cs new file mode 100644 index 0000000..76b1e78 --- /dev/null +++ b/src/SecureSign.Core/KeyTypeUtils.cs @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.IO; + +namespace SecureSign.Core +{ + public static class KeyTypeUtils + { + /// + /// Gets the that this file could contain. + /// + /// Name of the file + /// The key type + public static KeyType FromFilename(string filename) + { + switch (Path.GetExtension(filename)) + { + case ".pfx": + return KeyType.Authenticode; + case ".gpg": + return KeyType.Gpg; + default: + throw new Exception("Unsupported key type"); + } + } + } +} diff --git a/src/SecureSign.Core/Models/AccessToken.cs b/src/SecureSign.Core/Models/AccessToken.cs index b9a0c33..445dbff 100644 --- a/src/SecureSign.Core/Models/AccessToken.cs +++ b/src/SecureSign.Core/Models/AccessToken.cs @@ -28,10 +28,16 @@ public class AccessToken public DateTime IssuedAt { get; set; } /// - /// Name of the key to use for signing requests using this access token + /// Name of the key to use for signing requests using this access token. + /// For GPG, this is the keygrip. /// public string KeyName { get; set; } + /// + /// Fingerprint of the key to use for signing. Only used for GPG. + /// + public string KeyFingerprint { get; set; } + /// /// Decryption key for the key /// diff --git a/src/SecureSign.Core/Models/PathConfig.cs b/src/SecureSign.Core/Models/PathConfig.cs index 366dae5..d8a6edb 100644 --- a/src/SecureSign.Core/Models/PathConfig.cs +++ b/src/SecureSign.Core/Models/PathConfig.cs @@ -39,6 +39,11 @@ public class PathConfig /// public string AccessTokenConfig => Path.Combine(Root, ACCESS_TOKEN_FILENAME); + /// + /// Gets the full path to SecureSign's GnuPG home directory + /// + public string GpgHome => Path.Combine(Root, ".gnupg"); + /// /// Gets or sets the path to signtool.exe /// diff --git a/src/SecureSign.Core/SecretStorage.cs b/src/SecureSign.Core/SecretStorage.cs index a112322..d281177 100644 --- a/src/SecureSign.Core/SecretStorage.cs +++ b/src/SecureSign.Core/SecretStorage.cs @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +using System; using System.IO; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection; @@ -94,7 +95,20 @@ public string GetPathForSecret(string name) return Path.Combine(_pathConfig.Certificates, name); } - /// + /// + /// Throws an exception if a secret with the specified name already exists. + /// + /// Name of the secret file + public void ThrowIfSecretExists(string name) + { + var outputPath = GetPathForSecret(name); + if (File.Exists(outputPath)) + { + throw new Exception(outputPath + " already exists! I'm not going to overwrite it."); + } + } + + /// /// Creates an to encrypt files with the specified key. /// /// Key to use diff --git a/src/SecureSign.Core/SecureSign.Core.csproj b/src/SecureSign.Core/SecureSign.Core.csproj index bfc1290..f4d7a2e 100644 --- a/src/SecureSign.Core/SecureSign.Core.csproj +++ b/src/SecureSign.Core/SecureSign.Core.csproj @@ -5,6 +5,7 @@ + diff --git a/src/SecureSign.Core/Signers/GpgSigner.cs b/src/SecureSign.Core/Signers/GpgSigner.cs new file mode 100644 index 0000000..d9040a5 --- /dev/null +++ b/src/SecureSign.Core/Signers/GpgSigner.cs @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.IO; +using Libgpgme; +using Microsoft.Extensions.Options; +using SecureSign.Core.Models; + +namespace SecureSign.Core.Signers +{ + /// + /// Handles signing artifacts using GPG. + /// + public class GpgSigner : IGpgSigner + { + private readonly Context _ctx; + private readonly PathConfig _pathConfig; + private static readonly object _signLock = new object(); + + public GpgSigner(IOptions pathConfig, Context ctx) + { + _ctx = ctx; + _pathConfig = pathConfig.Value; + } + + /// + /// Signs the specified artifact using GPG + /// + /// Bytes to sign + /// Hex representation of keygrip + /// Contents of the secret key file + /// Fingerprint of the key + /// ASCII-armored signature + public byte[] Sign(byte[] input, string keygrip, byte[] secretKeyFile, string fingerprint) + { + // Since this signer messes with some global state (private keys in the GPG home directory), + // we must lock to ensure no other signing requests happen concurrently. + lock (_signLock) + { + var keygripPath = Path.Combine(_pathConfig.GpgHome, "private-keys-v1.d", keygrip + ".key"); + var inputPath = Path.GetTempFileName(); + try + { + File.WriteAllBytes(keygripPath, secretKeyFile); + File.WriteAllBytes(inputPath, input); + + var key = _ctx.KeyStore.GetKey(fingerprint, secretOnly: true); + _ctx.Signers.Clear(); + _ctx.Signers.Add(key); + _ctx.Armor = true; + + using (var inputData = new GpgmeFileData(inputPath, FileMode.Open, FileAccess.Read)) + using (var sigData = new GpgmeMemoryData { FileName = "signature.asc" }) + { + var result = _ctx.Sign(inputData, sigData, SignatureMode.Detach); + if (result.InvalidSigners != null) + { + throw new Exception($"Could not sign: {result.InvalidSigners.Reason}"); + } + + using (var memStream = new MemoryStream()) + { + sigData.Seek(0, SeekOrigin.Begin); + sigData.CopyTo(memStream); + return memStream.GetBuffer(); + } + } + } + finally + { + File.Delete(keygripPath); + File.Delete(inputPath); + } + } + } + } +} diff --git a/src/SecureSign.Core/Signers/IGpgSigner.cs b/src/SecureSign.Core/Signers/IGpgSigner.cs new file mode 100644 index 0000000..7bf039f --- /dev/null +++ b/src/SecureSign.Core/Signers/IGpgSigner.cs @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace SecureSign.Core.Signers +{ + /// + /// Handles signing artifacts using GPG. + /// + public interface IGpgSigner + { + /// + /// Signs the specified artifact using GPG + /// + /// Bytes to sign + /// Hex representation of keygrip + /// Contents of the secret key file + /// Fingerprint of the key + /// ASCII-armored signature + byte[] Sign(byte[] input, string keygrip, byte[] secretKeyFile, string fingerprint); + } +} diff --git a/src/SecureSign.Tools/AddToken.cs b/src/SecureSign.Tools/AddToken.cs new file mode 100644 index 0000000..4aaea8c --- /dev/null +++ b/src/SecureSign.Tools/AddToken.cs @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SecureSign.Core; +using SecureSign.Core.Models; +using SecureSign.Tools.KeyHandlers; + +namespace SecureSign.Tools +{ + /// + /// Handles creating new access tokens + /// + class AddToken + { + private readonly ISecretStorage _secretStorage; + private readonly IAccessTokenSerializer _accessTokenSerializer; + private readonly IKeyHandlerFactory _keyHandlerFactory; + private readonly PathConfig _pathConfig; + + public AddToken( + ISecretStorage secretStorage, + IAccessTokenSerializer accessTokenSerializer, + IOptions pathConfig, + IKeyHandlerFactory keyHandlerFactory + ) + { + _secretStorage = secretStorage; + _accessTokenSerializer = accessTokenSerializer; + _keyHandlerFactory = keyHandlerFactory; + _pathConfig = pathConfig.Value; + } + + public int Run() + { + var name = ConsoleUtils.Prompt("Key name"); + var code = ConsoleUtils.Prompt("Secret code"); + + try + { + _secretStorage.LoadSecret(name, code); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Could not load key: {ex.Message}"); + Console.Error.WriteLine("Please check that the name and secret code are valid."); + return 1; + } + + var handler = _keyHandlerFactory.GetHandler(name); + var (accessToken, accessTokenConfig) = handler.CreateAccessToken(code, name); + var encodedAccessToken = SaveAccessToken(accessToken, accessTokenConfig); + Console.WriteLine(); + Console.WriteLine("Created new access token:"); + Console.WriteLine(encodedAccessToken); + return 0; + } + + /// + /// Saves the provided access token to the config + /// + /// + /// Config to save + /// Serialized access token for use in SecureSign requests + private string SaveAccessToken(AccessToken accessToken, AccessTokenConfig accessTokenConfig) + { + // If this is the first time an access token is being added, we need to create the config file + if (!File.Exists(_pathConfig.AccessTokenConfig)) + { + File.WriteAllText(_pathConfig.AccessTokenConfig, JsonConvert.SerializeObject(new + { + AccessTokens = new Dictionary() + })); + } + + // Save access token config to config file + dynamic configFile = JObject.Parse(File.ReadAllText(_pathConfig.AccessTokenConfig)); + configFile.AccessTokens[accessToken.Id] = JToken.FromObject(accessTokenConfig); + File.WriteAllText(_pathConfig.AccessTokenConfig, JsonConvert.SerializeObject(configFile, Formatting.Indented)); + + var encodedAccessToken = _accessTokenSerializer.Serialize(accessToken); + return encodedAccessToken; + } + } +} diff --git a/src/SecureSign.Tools/KeyHandlers/AuthenticodeKeyHandler.cs b/src/SecureSign.Tools/KeyHandlers/AuthenticodeKeyHandler.cs new file mode 100644 index 0000000..0031829 --- /dev/null +++ b/src/SecureSign.Tools/KeyHandlers/AuthenticodeKeyHandler.cs @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using SecureSign.Core; +using SecureSign.Core.Extensions; +using SecureSign.Core.Models; + +namespace SecureSign.Tools.KeyHandlers +{ + /// + /// Handles storing and creating access tokens for Authenticode keys + /// + class AuthenticodeKeyHandler : IKeyHandler + { + private readonly IPasswordGenerator _passwordGenerator; + private readonly ISecretStorage _secretStorage; + + public AuthenticodeKeyHandler(IPasswordGenerator passwordGenerator, ISecretStorage secretStorage) + { + _passwordGenerator = passwordGenerator; + _secretStorage = secretStorage; + } + + /// + /// Gets the key type that this key handler supports + /// + public KeyType KeyType => KeyType.Authenticode; + + /// + /// Adds a new key to the secret storage. + /// + /// + public void AddKey(string inputPath) + { + // Ensure output file does not exist + var fileName = Path.GetFileName(inputPath); + _secretStorage.ThrowIfSecretExists(fileName); + + var password = ConsoleUtils.PasswordPrompt("Password"); + var cert = new X509Certificate2(File.ReadAllBytes(inputPath), password, X509KeyStorageFlags.Exportable); + + var code = _passwordGenerator.Generate(); + _secretStorage.SaveSecret(fileName, cert, code); + Console.WriteLine(); + Console.WriteLine($"Saved {fileName} ({cert.FriendlyName})"); + Console.WriteLine($"Subject: {cert.SubjectName.Format(false)}"); + Console.WriteLine($"Issuer: {cert.IssuerName.Format(false)}"); + Console.WriteLine($"Valid from {cert.NotBefore} until {cert.NotAfter}"); + Console.WriteLine(); + Console.WriteLine($"Secret Code: {code}"); + } + + /// + /// Creates a new access token to use the specified key + /// + /// Encryption code for the key + /// Name of the key + /// Access token and its config + public (AccessToken accessToken, AccessTokenConfig accessTokenConfig) CreateAccessToken( + string code, + string name + ) + { + var comment = ConsoleUtils.Prompt("Comment (optional)"); + + Console.WriteLine(); + Console.WriteLine("Signing settings:"); + var desc = ConsoleUtils.Prompt("Description"); + var url = ConsoleUtils.Prompt("Product/Application URL"); + + var accessToken = new AccessToken + { + Id = Guid.NewGuid().ToShortGuid(), + Code = code, + IssuedAt = DateTime.Now, + KeyName = name, + }; + var accessTokenConfig = new AccessTokenConfig + { + Comment = comment, + IssuedAt = accessToken.IssuedAt, + Valid = true, + + SignDescription = desc, + SignUrl = url, + }; + return (accessToken, accessTokenConfig); + } + } +} diff --git a/src/SecureSign.Tools/KeyHandlers/GpgKeyHandler.cs b/src/SecureSign.Tools/KeyHandlers/GpgKeyHandler.cs new file mode 100644 index 0000000..49d55bb --- /dev/null +++ b/src/SecureSign.Tools/KeyHandlers/GpgKeyHandler.cs @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Libgpgme; +using SecureSign.Core; +using SecureSign.Core.Extensions; +using SecureSign.Core.Models; + +namespace SecureSign.Tools.KeyHandlers +{ + /// + /// Handles storing and creating access tokens for GPG keys + /// + class GpgKeyHandler : IKeyHandler + { + private readonly ISecretStorage _secretStorage; + private readonly IPasswordGenerator _passwordGenerator; + private readonly Context _ctx; + + public GpgKeyHandler(ISecretStorage secretStorage, IPasswordGenerator passwordGenerator, Context ctx) + { + _secretStorage = secretStorage; + _passwordGenerator = passwordGenerator; + _ctx = ctx; + } + + /// + /// Gets the key type that this key handler supports + /// + public KeyType KeyType => KeyType.Gpg; + + /// + /// Adds a new key to the secret storage. + /// + /// + public void AddKey(string inputPath) + { + // Create a temporary directory to hold the GPG key while we verify that it's legit + var tempHomedir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Console.WriteLine($"Using {tempHomedir} as temp directory."); + Directory.CreateDirectory(tempHomedir); + try + { + using (var tempCtx = new Context()) + { + tempCtx.SetEngineInfo(Protocol.OpenPGP, null, tempHomedir); + tempCtx.PinentryMode = PinentryMode.Error; + + ImportResult result; + using (var keyfile = new GpgmeFileData(inputPath)) + { + result = tempCtx.KeyStore.Import(keyfile); + } + + if (result == null || result.Imported == 0 || result.SecretImported == 0 || result.Imports == null) + { + throw new Exception("No secret keys found!"); + } + + var fingerprints = result.Imports.Where(x => x.NewSecretKey).Select(x => x.Fpr); + Console.WriteLine($"Found fingerprints: {string.Join(", ", fingerprints)}"); + + var keys = GetAndVerifyKeys(fingerprints, tempCtx); + EnsureGpgKeysDoNotExist(keys); + + Console.WriteLine(); + ImportAndEncryptSecretGpgKeys(keys, tempHomedir); + ImportPublicGpgKeys(keys, tempCtx); + } + } + finally + { + Directory.Delete(tempHomedir, true); + } + } + + /// + /// Gets all the keys with the specified fingerprints, and verifies that they can be used for signing + /// + /// Fingerprints of the keys + /// GPGME context to use + /// List of all the keys + private static List GetAndVerifyKeys(IEnumerable fingerprints, Context ctx) + { + // Ensure all keys work by signing a message with them + var original = new GpgmeMemoryData { FileName = "original.txt" }; + var writer = new BinaryWriter(original, Encoding.UTF8); + writer.Write("Hello World!"); + + var keys = new List(); + + foreach (var fingerprint in fingerprints) + { + var output = new GpgmeMemoryData { FileName = "original.txt.gpg" }; + var key = ctx.KeyStore.GetKey(fingerprint, true); + ctx.Signers.Clear(); + ctx.Signers.Add(key); + try + { + var signResult = ctx.Sign(original, output, SignatureMode.Detach); + if (signResult.InvalidSigners != null) + { + var firstInvalidKey = signResult.InvalidSigners.First(); + throw new Exception( + $"Invalid signer: {firstInvalidKey.Fingerprint} ({firstInvalidKey.Reason})" + ); + } + + keys.Add(key); + } + catch (GpgmeException ex) + { + // TODO: Expose GPG error code through gpgme-sharp, to avoid string comparison + if (ex.Message.Contains("NO_PIN_ENTRY")) + { + throw new Exception( + "SecureSign does not support passphrase-protected GPG keys. " + + "Please export your key without a passphrase." + ); + } + + throw; + } + } + + return keys; + } + + /// + /// Imports the specified keys from the temporary storage, and encrypts them. + /// Outputs details to the console. + /// + /// Keys to import + /// Temporary GPG homedir + private void ImportAndEncryptSecretGpgKeys(IEnumerable keys, string tempHomedir) + { + foreach (var key in keys) + { + foreach (var subkey in key.Subkeys) + { + if (!subkey.CanSign) + { + continue; + } + + var inputFilename = subkey.Keygrip + ".key"; + var outputFilename = subkey.Keygrip + ".gpg"; + var keygripPath = Path.Join( + tempHomedir, + "private-keys-v1.d", + inputFilename + ); + var code = _passwordGenerator.Generate(); + _secretStorage.SaveSecret(outputFilename, File.ReadAllBytes(keygripPath), code); + File.Delete(keygripPath); + + Console.WriteLine($"- {subkey.KeyId}"); + Console.WriteLine($" Expires: {subkey.Expires}"); + Console.WriteLine($" Key name: {outputFilename}"); + Console.WriteLine($" Secret Code: {code}"); + Console.WriteLine(); + } + } + } + + /// + /// Imports the public GPG keys into the local GPG home directory + /// + /// Keys to import + /// Temporary GPGME context + private void ImportPublicGpgKeys(IEnumerable keys, Context tempCtx) + { + foreach (var key in keys) + { + var tempFile = Path.GetTempFileName(); + try + { + // Export public key from temporary keychain and import into real keychain + tempCtx.KeyStore.Export(key.KeyId, tempFile); + using (var tempFileData = new GpgmeFileData(tempFile)) + { + _ctx.KeyStore.Import(tempFileData); + } + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Throws an exception if any of the specified keys already exist in the secret storage + /// + /// Keys to check + private void EnsureGpgKeysDoNotExist(List keys) + { + foreach (var key in keys) + { + foreach (var subkey in key.Subkeys) + { + _secretStorage.ThrowIfSecretExists(subkey.Keygrip + ".gpg"); + } + } + } + + /// + /// Creates a new access token to use the specified key + /// + /// Encryption code for the key + /// Name of the key + /// Access token and its config + public (AccessToken accessToken, AccessTokenConfig accessTokenConfig) CreateAccessToken( + string code, + string name + ) + { + var fingerprint = ConsoleUtils.Prompt("Key ID"); + // Validate keyId is legit + var key = _ctx.KeyStore.GetKey(fingerprint, secretOnly: false); + if (key == null) + { + throw new Exception($"Invalid key ID: {fingerprint}"); + } + + var comment = ConsoleUtils.Prompt("Comment (optional)"); + + var accessToken = new AccessToken + { + Id = Guid.NewGuid().ToShortGuid(), + Code = code, + IssuedAt = DateTime.Now, + KeyFingerprint = fingerprint, + KeyName = name, + }; + var accessTokenConfig = new AccessTokenConfig + { + Comment = comment, + IssuedAt = accessToken.IssuedAt, + Valid = true, + }; + return (accessToken, accessTokenConfig); + } + } +} diff --git a/src/SecureSign.Tools/KeyHandlers/IKeyHandler.cs b/src/SecureSign.Tools/KeyHandlers/IKeyHandler.cs new file mode 100644 index 0000000..09db390 --- /dev/null +++ b/src/SecureSign.Tools/KeyHandlers/IKeyHandler.cs @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using SecureSign.Core; +using SecureSign.Core.Models; + +namespace SecureSign.Tools.KeyHandlers +{ + /// + /// Handles storing new secret keys and creating access tokens for them + /// + public interface IKeyHandler + { + /// + /// Gets the key type that this key handler supports + /// + KeyType KeyType { get; } + + /// + /// Adds a new key to the secret storage. + /// + /// + void AddKey(string inputPath); + + /// + /// Creates a new access token to use the specified key + /// + /// Encryption code for the key + /// Name of the key + /// Access token and its config + (AccessToken accessToken, AccessTokenConfig accessTokenConfig) CreateAccessToken( + string code, + string name + ); + } +} diff --git a/src/SecureSign.Tools/KeyHandlers/IKeyHandlerFactory.cs b/src/SecureSign.Tools/KeyHandlers/IKeyHandlerFactory.cs new file mode 100644 index 0000000..0ea609f --- /dev/null +++ b/src/SecureSign.Tools/KeyHandlers/IKeyHandlerFactory.cs @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace SecureSign.Tools.KeyHandlers +{ + /// + /// Handles getting the key factory to handle the given file + /// + public interface IKeyHandlerFactory + { + /// + /// Gets the handler for the specified file. If no handler supports this file, throws an exception. + /// + /// Name of the key file + /// The handler for this key file + IKeyHandler GetHandler(string filename); + } +} diff --git a/src/SecureSign.Tools/KeyHandlers/KeyHandlerFactory.cs b/src/SecureSign.Tools/KeyHandlers/KeyHandlerFactory.cs new file mode 100644 index 0000000..52f667a --- /dev/null +++ b/src/SecureSign.Tools/KeyHandlers/KeyHandlerFactory.cs @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using SecureSign.Core; + +namespace SecureSign.Tools.KeyHandlers +{ + /// + /// Handles getting the key factory to handle the given file + /// + class KeyHandlerFactory : IKeyHandlerFactory + { + private readonly IEnumerable _handlers; + + public KeyHandlerFactory(IEnumerable handlers) + { + _handlers = handlers; + } + + /// + /// Gets the handler for the specified file. If no handler supports this file, throws an exception. + /// + /// Name of the key file + /// The handler for this key file + public IKeyHandler GetHandler(string filename) + { + try + { + var keytype = KeyTypeUtils.FromFilename(filename); + return _handlers.First(x => x.KeyType == keytype); + } + catch (Exception ex) + { + throw new Exception($"Unrecognised file extension. Please use .pfx for Authenticode or .gpg for GPG. {ex.Message}"); + } + } + } +} diff --git a/src/SecureSign.Tools/Program.cs b/src/SecureSign.Tools/Program.cs index 2c750ae..bc620ad 100644 --- a/src/SecureSign.Tools/Program.cs +++ b/src/SecureSign.Tools/Program.cs @@ -6,18 +6,12 @@ */ using System; -using System.Collections.Generic; using System.IO; -using System.Security.Cryptography.X509Certificates; using McMaster.Extensions.CommandLineUtils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SecureSign.Core; using SecureSign.Core.Extensions; -using SecureSign.Core.Models; +using SecureSign.Tools.KeyHandlers; namespace SecureSign.Tools { @@ -28,6 +22,9 @@ static int Main(string[] args) var config = BuildConfig(); var services = new ServiceCollection(); services.AddSecureSignCore(config); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); var provider = services.BuildServiceProvider(); var program = ActivatorUtilities.CreateInstance(provider); @@ -41,17 +38,13 @@ private static IConfiguration BuildConfig() .Build(); } - private readonly ISecretStorage _secretStorage; - private readonly IAccessTokenSerializer _accessTokenSerializer; - private readonly IPasswordGenerator _passwordGenerator; - private readonly PathConfig _pathConfig; + private readonly IKeyHandlerFactory _keyHandlerFactory; + private readonly IServiceProvider _provider; - public Program(ISecretStorage secretStorage, IAccessTokenSerializer accessTokenSerializer, IPasswordGenerator passwordGenerator, IOptions pathConfig) + public Program(IKeyHandlerFactory keyHandlerFactory, IServiceProvider provider) { - _secretStorage = secretStorage; - _accessTokenSerializer = accessTokenSerializer; - _passwordGenerator = passwordGenerator; - _pathConfig = pathConfig.Value; + _keyHandlerFactory = keyHandlerFactory; + _provider = provider; } private int Run(string[] args) @@ -74,36 +67,19 @@ private int Run(string[] args) return 1; } - // Ensure input file exists if (!File.Exists(inputPath)) { throw new Exception("File does not exist: " + inputPath); } - // Ensure output file does not exist - var fileName = Path.GetFileName(inputPath); - var outputPath = _secretStorage.GetPathForSecret(fileName); - if (File.Exists(outputPath)) - { - throw new Exception(outputPath + " already exists! I'm not going to overwrite it."); - } + var handler = _keyHandlerFactory.GetHandler(inputPath); + handler.AddKey(inputPath); - var password = ConsoleUtils.PasswordPrompt("Password"); - var cert = new X509Certificate2(File.ReadAllBytes(inputPath), password, X509KeyStorageFlags.Exportable); - - var code = _passwordGenerator.Generate(); - _secretStorage.SaveSecret(fileName, cert, code); - Console.WriteLine(); - Console.WriteLine($"Saved {fileName} ({cert.FriendlyName})"); - Console.WriteLine($"Subject: {cert.SubjectName.Format(false)}"); - Console.WriteLine($"Issuer: {cert.IssuerName.Format(false)}"); - Console.WriteLine($"Valid from {cert.NotBefore} until {cert.NotAfter}"); - Console.WriteLine(); - Console.WriteLine($"Secret Code: {code}"); Console.WriteLine(); Console.WriteLine("This secret code is required whenever you create an access token that uses this key."); Console.WriteLine("Store this secret code in a SECURE PLACE! The code is not stored anywhere, "); Console.WriteLine("so if you lose it, you will need to re-install the key."); + Console.ReadKey(); return 0; }); }); @@ -111,69 +87,7 @@ private int Run(string[] args) app.Command("addtoken", command => { command.Description = "Add a new access token"; - command.OnExecute(() => - { - var name = ConsoleUtils.Prompt("Key name"); - var code = ConsoleUtils.Prompt("Secret code"); - - try - { - _secretStorage.LoadSecret(name, code); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Could not load key: {ex.Message}"); - Console.Error.WriteLine("Please check that the name and secret code are valid."); - return 1; - } - - // If we got here, the key is valid - var comment = ConsoleUtils.Prompt("Comment (optional)"); - - Console.WriteLine(); - Console.WriteLine("Signing settings:"); - var desc = ConsoleUtils.Prompt("Description"); - var url = ConsoleUtils.Prompt("Product/Application URL"); - - var accessToken = new AccessToken - { - Id = Guid.NewGuid().ToShortGuid(), - Code = code, - IssuedAt = DateTime.Now, - KeyName = name, - }; - var accessTokenConfig = new AccessTokenConfig - { - Comment = comment, - IssuedAt = accessToken.IssuedAt, - Valid = true, - - SignDescription = desc, - SignUrl = url, - }; - - - // If this is the first time an access token is being added, we need to create the config file - if (!File.Exists(_pathConfig.AccessTokenConfig)) - { - File.WriteAllText(_pathConfig.AccessTokenConfig, JsonConvert.SerializeObject(new - { - AccessTokens = new Dictionary() - })); - } - - // Save access token config to config file - dynamic configFile = JObject.Parse(File.ReadAllText(_pathConfig.AccessTokenConfig)); - configFile.AccessTokens[accessToken.Id] = JToken.FromObject(accessTokenConfig); - File.WriteAllText(_pathConfig.AccessTokenConfig, JsonConvert.SerializeObject(configFile, Formatting.Indented)); - - var encodedAccessToken = _accessTokenSerializer.Serialize(accessToken); - - Console.WriteLine(); - Console.WriteLine("Created new access token:"); - Console.WriteLine(encodedAccessToken); - return 0; - }); + command.OnExecute(() => ActivatorUtilities.CreateInstance(_provider).Run()); }); try diff --git a/src/SecureSign.Tools/SecureSign.Tools.csproj b/src/SecureSign.Tools/SecureSign.Tools.csproj index 6620e63..b3f4bf3 100644 --- a/src/SecureSign.Tools/SecureSign.Tools.csproj +++ b/src/SecureSign.Tools/SecureSign.Tools.csproj @@ -5,6 +5,10 @@ netcoreapp2.2 + + x86 + + diff --git a/src/SecureSign.Web/Controllers/AuthenticodeSigningController.cs b/src/SecureSign.Web/Controllers/AuthenticodeSigningController.cs new file mode 100644 index 0000000..efa944c --- /dev/null +++ b/src/SecureSign.Web/Controllers/AuthenticodeSigningController.cs @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2017 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using SecureSign.Core; +using SecureSign.Core.Signers; +using SecureSign.Web.Models; + +namespace SecureSign.Web.Controllers +{ + /// + /// Handles requests to sign files + /// + [Produces("application/json")] + public class AuthenticodeSigningController : Controller + { + private readonly ISecretStorage _secretStorage; + private readonly IAuthenticodeSigner _signer; + private readonly SigningControllerUtils _utils; + + + public AuthenticodeSigningController( + ISecretStorage secretStorage, + IAuthenticodeSigner signer, + SigningControllerUtils utils + ) + { + _secretStorage = secretStorage; + _signer = signer; + _utils = utils; + } + + /// + /// Handles requests to sign files with an Authenticode signature. + /// + /// Details of the request + /// Authenticode-signed file + [Route("sign/authenticode")] + public async Task SignUsingAuthenticode(SignRequest request) + { + var (token, tokenConfig, tokenError) = _utils.TryGetAccessToken(request); + if (tokenError != null) + { + return tokenError; + } + + var cert = _secretStorage.LoadAuthenticodeCertificate(token.KeyName, token.Code); + var (artifact, artifactError) = await _utils.GetFileFromPayloadAsync(token, tokenConfig, request); + if (artifactError != null) + { + return artifactError; + } + + var signed = await _signer.SignAsync(artifact, cert, tokenConfig.SignDescription, tokenConfig.SignUrl); + return File(signed, "application/octet-stream"); + } + } +} diff --git a/src/SecureSign.Web/Controllers/CertificateStatusController.cs b/src/SecureSign.Web/Controllers/CertificateStatusController.cs index d31a8e0..5376d87 100644 --- a/src/SecureSign.Web/Controllers/CertificateStatusController.cs +++ b/src/SecureSign.Web/Controllers/CertificateStatusController.cs @@ -6,6 +6,8 @@ */ using System; +using System.Linq; +using Libgpgme; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using SecureSign.Core; @@ -21,12 +23,19 @@ public class CertificateStatusController : Controller private readonly IAccessTokenSerializer _accessTokenSerializer; private readonly ILogger _logger; private readonly ISecretStorage _secretStorage; + private readonly Context _ctx; - public CertificateStatusController(ILogger logger, IAccessTokenSerializer accessTokenSerializer, ISecretStorage secretStorage) + public CertificateStatusController( + ILogger logger, + IAccessTokenSerializer accessTokenSerializer, + ISecretStorage secretStorage, + Context ctx + ) { _logger = logger; _accessTokenSerializer = accessTokenSerializer; _secretStorage = secretStorage; + _ctx = ctx; } [Route("")] @@ -43,17 +52,37 @@ public IActionResult Index(CertificateStatusRequest request) return Unauthorized(); } - var cert = _secretStorage.LoadAuthenticodeCertificate(token.KeyName, token.Code); - return Ok(new CertificateStatusResponse + switch (KeyTypeUtils.FromFilename(token.KeyName)) { - CreationDate = cert.NotBefore, - ExpiryDate = cert.NotAfter, - Issuer = cert.IssuerName.Format(false), - Name = cert.FriendlyName, - SerialNumber = cert.SerialNumber, - Subject = cert.SubjectName.Format(false), - Thumbprint = cert.Thumbprint, - }); + case KeyType.Authenticode: + var cert = _secretStorage.LoadAuthenticodeCertificate(token.KeyName, token.Code); + return Ok(new CertificateStatusResponse + { + CreationDate = cert.NotBefore, + ExpiryDate = cert.NotAfter, + Issuer = cert.IssuerName.Format(false), + Name = cert.FriendlyName, + SerialNumber = cert.SerialNumber, + Subject = cert.SubjectName.Format(false), + Thumbprint = cert.Thumbprint, + }); + + case KeyType.Gpg: + var key = _ctx.KeyStore.GetKey(token.KeyFingerprint, secretOnly: false); + var subkey = key.Subkeys.First(x => x.KeyId == token.KeyFingerprint); + return Ok(new CertificateStatusResponse + { + CreationDate = subkey.Timestamp, + ExpiryDate = subkey.Expires, + Issuer = key.IssuerName, + Name = token.KeyName, + Subject = key.Uid.Uid, + Thumbprint = subkey.KeyId, + }); + + default: + return NotFound("Unknown key type"); + } } } } \ No newline at end of file diff --git a/src/SecureSign.Web/Controllers/GpgSigningController.cs b/src/SecureSign.Web/Controllers/GpgSigningController.cs new file mode 100644 index 0000000..5f681ca --- /dev/null +++ b/src/SecureSign.Web/Controllers/GpgSigningController.cs @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using SecureSign.Core; +using SecureSign.Core.Signers; +using SecureSign.Web.Models; + +namespace SecureSign.Web.Controllers +{ + /// + /// Handles requests to sign files + /// + [Produces("application/json")] + public class GpgSigningController : Controller + { + private readonly ISecretStorage _secretStorage; + private readonly IGpgSigner _signer; + private readonly SigningControllerUtils _utils; + + public GpgSigningController( + ISecretStorage secretStorage, + IGpgSigner signer, + SigningControllerUtils utils + ) + { + _secretStorage = secretStorage; + _signer = signer; + _utils = utils; + } + + /// + /// Handles requests to sign files with an Authenticode signature. + /// + /// Details of the request + /// Authenticode-signed file + [Route("sign/gpg")] + public async Task SignUsingAuthenticode(SignRequest request) + { + var (token, tokenConfig, tokenError) = _utils.TryGetAccessToken(request); + if (tokenError != null) + { + return tokenError; + } + + var secretKey = _secretStorage.LoadSecret(token.KeyName, token.Code); + var (artifact, artifactError) = await _utils.GetFileFromPayloadAsync(token, tokenConfig, request); + if (artifactError != null) + { + return artifactError; + } + + var signed = _signer.Sign( + artifact, + Path.GetFileNameWithoutExtension(token.KeyName), + secretKey, + token.KeyFingerprint + ); + return File(signed, "application/pgp-signature"); + } + } +} diff --git a/src/SecureSign.Web/Controllers/SigningController.cs b/src/SecureSign.Web/Controllers/SigningControllerUtils.cs similarity index 52% rename from src/SecureSign.Web/Controllers/SigningController.cs rename to src/SecureSign.Web/Controllers/SigningControllerUtils.cs index e42d09b..7e6d659 100644 --- a/src/SecureSign.Web/Controllers/SigningController.cs +++ b/src/SecureSign.Web/Controllers/SigningControllerUtils.cs @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 Daniel Lo Nigro (Daniel15) + * Copyright (c) 2019 Daniel Lo Nigro (Daniel15) * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -17,47 +17,32 @@ using Microsoft.Extensions.Options; using SecureSign.Core; using SecureSign.Core.Models; -using SecureSign.Core.Signers; using SecureSign.Web.Models; namespace SecureSign.Web.Controllers { /// - /// Handles requests to sign files + /// Utilities specific to signing controllers. /// - [Produces("application/json")] - [Route("sign")] - public class SigningController : Controller + public class SigningControllerUtils { - private readonly ISecretStorage _secretStorage; + private readonly ILogger _logger; private readonly IAccessTokenSerializer _accessTokenSerializer; - private readonly IAuthenticodeSigner _signer; - private readonly ILogger _logger; - private readonly IDictionary _accessTokenConfig; + private readonly Dictionary _accessTokenConfig; private readonly HttpClient _httpClient = new HttpClient(); - public SigningController( - ISecretStorage secretStorage, + public SigningControllerUtils( IAccessTokenSerializer accessTokenSerializer, - IAuthenticodeSigner signer, IOptions> accessTokenConfig, - ILogger logger + ILogger logger ) { - _secretStorage = secretStorage; - _accessTokenConfig = accessTokenConfig.Value; - _accessTokenSerializer = accessTokenSerializer; - _signer = signer; _logger = logger; + _accessTokenSerializer = accessTokenSerializer; + _accessTokenConfig = accessTokenConfig.Value; } - /// - /// Handles requests to sign files with an Authenticode signature. - /// - /// Details of the request - /// Authenticode-signed file - [Route("authenticode")] - public async Task SignUsingAuthenticode(AuthenticodeSignRequest request) + public (AccessToken, AccessTokenConfig, IActionResult) TryGetAccessToken(SignRequest request) { AccessToken token; try @@ -67,29 +52,16 @@ public async Task SignUsingAuthenticode(AuthenticodeSignRequest r catch (Exception ex) { _logger.LogInformation(ex, "Access token could not be decrypted"); - return Unauthorized(); + return (null, null, new UnauthorizedResult()); } if (!_accessTokenConfig.TryGetValue(token.Id, out var tokenConfig) || !tokenConfig.Valid) { _logger.LogWarning("Access token not in config file, or marked as invalid: {Id}", token.Id); - return Unauthorized(); + return (null, null, new UnauthorizedResult()); } - var cert = _secretStorage.LoadAuthenticodeCertificate(token.KeyName, token.Code); - byte[] artifact; - try - { - artifact = await GetFileFromPayloadAsync(token, tokenConfig, request); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not retrieve artifact to sign"); - return BadRequest(ex.Message); - } - - var signed = await _signer.SignAsync(artifact, cert, tokenConfig.SignDescription, tokenConfig.SignUrl); - return File(signed, "application/octet-stream"); + return (token, tokenConfig, null); } /// @@ -99,33 +71,44 @@ public async Task SignUsingAuthenticode(AuthenticodeSignRequest r /// Configuration for this access token /// The request /// The file contents - private async Task GetFileFromPayloadAsync(AccessToken token, AccessTokenConfig tokenConfig, AuthenticodeSignRequest request) + public async Task<(byte[], IActionResult)> GetFileFromPayloadAsync(AccessToken token, AccessTokenConfig tokenConfig, SignRequest request) { - if (!CheckIfRequestIsWhitelisted(token, tokenConfig, request)) - { - throw new InvalidOperationException("Upload request is not allowed."); - } - if (request.ArtifactUrl != null) + try { - _logger.LogInformation("Signing request received: {Id} is signing {ArtifactUrl}", token.Id, request.ArtifactUrl); - return await _httpClient.GetByteArrayAsync(request.ArtifactUrl); - } + if (!CheckIfRequestIsWhitelisted(token, tokenConfig, request)) + { + throw new InvalidOperationException("Upload request is not allowed."); + } - if (request.Artifact != null) - { - _logger.LogInformation("Signing request received: {Id} is signing {Filename}", token.Id, request.Artifact.FileName); - using (var stream = new MemoryStream()) + if (request.ArtifactUrl != null) { - await request.Artifact.CopyToAsync(stream); - return stream.ToArray(); + _logger.LogInformation("Signing request received: {Id} is signing {ArtifactUrl}", token.Id, + request.ArtifactUrl); + var artifact = await _httpClient.GetByteArrayAsync(request.ArtifactUrl); + return (artifact, null); + } + + if (request.Artifact != null) + { + _logger.LogInformation("Signing request received: {Id} is signing {Filename}", token.Id, + request.Artifact.FileName); + using (var stream = new MemoryStream()) + { + await request.Artifact.CopyToAsync(stream); + return (stream.ToArray(), null); + } } - } - // TODO: This should likely throw instead - return new byte[0]; + return (null, new BadRequestObjectResult("No artifact found to sign")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not retrieve artifact to sign"); + return (null, new BadRequestObjectResult(ex.Message)); + } } - private bool CheckIfRequestIsWhitelisted(AccessToken token, AccessTokenConfig tokenConfig, AuthenticodeSignRequest request) + private bool CheckIfRequestIsWhitelisted(AccessToken token, AccessTokenConfig tokenConfig, SignRequest request) { if ( request.ArtifactUrl != null && diff --git a/src/SecureSign.Web/Models/AuthenticodeSignRequest.cs b/src/SecureSign.Web/Models/SignRequest.cs similarity index 95% rename from src/SecureSign.Web/Models/AuthenticodeSignRequest.cs rename to src/SecureSign.Web/Models/SignRequest.cs index c679f37..5a11e8c 100644 --- a/src/SecureSign.Web/Models/AuthenticodeSignRequest.cs +++ b/src/SecureSign.Web/Models/SignRequest.cs @@ -13,7 +13,7 @@ namespace SecureSign.Web.Models /// /// Represents a request to sign a file using Authenticode /// - public class AuthenticodeSignRequest + public class SignRequest { /// /// Gets or sets the access token for the request. Will decode to an instance of . diff --git a/src/SecureSign.Web/SecureSign.Web.csproj b/src/SecureSign.Web/SecureSign.Web.csproj index ba13074..dcb4987 100644 --- a/src/SecureSign.Web/SecureSign.Web.csproj +++ b/src/SecureSign.Web/SecureSign.Web.csproj @@ -4,6 +4,10 @@ netcoreapp2.2 + + x86 + + diff --git a/src/SecureSign.Web/Startup.cs b/src/SecureSign.Web/Startup.cs index f8657a3..31a8e3d 100644 --- a/src/SecureSign.Web/Startup.cs +++ b/src/SecureSign.Web/Startup.cs @@ -10,7 +10,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using SecureSign.Core.Extensions; +using SecureSign.Web.Controllers; using SecureSign.Web.Middleware; namespace SecureSign.Web @@ -28,6 +30,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddSecureSignCore(Configuration); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + services.TryAddSingleton(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env)