Skip to content

Commit

Permalink
Merge pull request #3 from Daniel15/gpg
Browse files Browse the repository at this point in the history
Add support for GPG
  • Loading branch information
Daniel15 authored Mar 24, 2019
2 parents 3b0ec37 + 0b4bb16 commit c2fe7fa
Show file tree
Hide file tree
Showing 30 changed files with 1,081 additions and 182 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,5 @@ __pycache__/

# OpenCover UI analysis results
OpenCover/
.gnupg/
secrets/
Empty file added .gnupg/.gitkeep
Empty file.
44 changes: 35 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
========
Expand All @@ -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
Expand All @@ -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:
```
Expand All @@ -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 \
Expand All @@ -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": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
}
```

Restricting Usage of Access Tokens
Expand Down
11 changes: 11 additions & 0 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Workaround for https://github.com/dotnet/cli/issues/7532 -->
<Project>
<PropertyGroup
Condition="'$(OS)' == 'Windows_NT' and
'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and
'$(SelfContained)' != 'true'"
>
<RunCommand Condition="'$(PlatformTarget)' == 'x86'">$(MSBuildProgramFiles32)\dotnet\dotnet</RunCommand>
<RunCommand Condition="'$(PlatformTarget)' == 'x64'">$(ProgramW6432)\dotnet\dotnet</RunCommand>
</PropertyGroup>
</Project>
30 changes: 30 additions & 0 deletions src/SecureSign.Core/Extensions/SecureStringExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
13 changes: 13 additions & 0 deletions src/SecureSign.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,11 +36,22 @@ public static IServiceCollection AddSecureSignCore(this IServiceCollection servi
services.AddSingleton<ISecretStorage, SecretStorage>();
services.AddSingleton<IAccessTokenSerializer, AccessTokenSerializer>();
services.AddSingleton<IAuthenticodeSigner, AuthenticodeSigner>();
services.AddScoped<IGpgSigner, GpgSigner>();

// Configuration
services.Configure<PathConfig>(config.GetSection("Paths"));
services.Configure<Dictionary<string, AccessTokenConfig>>(config.GetSection("AccessTokens"));

// GPG
services.AddScoped<Context>(provider =>
{
var pathConfig = provider.GetRequiredService<IOptions<PathConfig>>();
var ctx = new Context();
ctx.SetEngineInfo(Protocol.OpenPGP, null, pathConfig.Value.GpgHome);
ctx.PinentryMode = PinentryMode.Error;
return ctx;
});

return services;
}

Expand Down
6 changes: 6 additions & 0 deletions src/SecureSign.Core/ISecretStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@ public interface ISecretStorage
/// <param name="name">Name of the secret file</param>
/// <returns>Full file path for the secret file</returns>
string GetPathForSecret(string name);

/// <summary>
/// Throws an exception if a secret with the specified name already exists.
/// </summary>
/// <param name="name">Name of the secret file</param>
void ThrowIfSecretExists(string name);
}
}
25 changes: 25 additions & 0 deletions src/SecureSign.Core/KeyType.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents a type of signing key supported by SecureSign
/// </summary>
public enum KeyType
{
/// <summary>
/// Authenticode (eg. to sign Windows installers)
/// </summary>
Authenticode,

/// <summary>
/// GnuPG
/// </summary>
Gpg,
}
}
33 changes: 33 additions & 0 deletions src/SecureSign.Core/KeyTypeUtils.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Gets the <see cref="KeyType"/> that this file could contain.
/// </summary>
/// <param name="filename">Name of the file</param>
/// <returns>The key type</returns>
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");
}
}
}
}
8 changes: 7 additions & 1 deletion src/SecureSign.Core/Models/AccessToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ public class AccessToken
public DateTime IssuedAt { get; set; }

/// <summary>
/// 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.
/// </summary>
public string KeyName { get; set; }

/// <summary>
/// Fingerprint of the key to use for signing. Only used for GPG.
/// </summary>
public string KeyFingerprint { get; set; }

/// <summary>
/// Decryption key for the key
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/SecureSign.Core/Models/PathConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class PathConfig
/// </summary>
public string AccessTokenConfig => Path.Combine(Root, ACCESS_TOKEN_FILENAME);

/// <summary>
/// Gets the full path to SecureSign's GnuPG home directory
/// </summary>
public string GpgHome => Path.Combine(Root, ".gnupg");

/// <summary>
/// Gets or sets the path to signtool.exe
/// </summary>
Expand Down
16 changes: 15 additions & 1 deletion src/SecureSign.Core/SecretStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,7 +95,20 @@ public string GetPathForSecret(string name)
return Path.Combine(_pathConfig.Certificates, name);
}

/// <summary>
/// <summary>
/// Throws an exception if a secret with the specified name already exists.
/// </summary>
/// <param name="name">Name of the secret file</param>
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.");
}
}

/// <summary>
/// Creates an <see cref="IDataProtector"/> to encrypt files with the specified key.
/// </summary>
/// <param name="encryptionKey">Key to use</param>
Expand Down
1 change: 1 addition & 0 deletions src/SecureSign.Core/SecureSign.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="gpgme-sharp" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
Expand Down
82 changes: 82 additions & 0 deletions src/SecureSign.Core/Signers/GpgSigner.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Handles signing artifacts using GPG.
/// </summary>
public class GpgSigner : IGpgSigner
{
private readonly Context _ctx;
private readonly PathConfig _pathConfig;
private static readonly object _signLock = new object();

public GpgSigner(IOptions<PathConfig> pathConfig, Context ctx)
{
_ctx = ctx;
_pathConfig = pathConfig.Value;
}

/// <summary>
/// Signs the specified artifact using GPG
/// </summary>
/// <param name="input">Bytes to sign</param>
/// <param name="keygrip">Hex representation of keygrip</param>
/// <param name="secretKeyFile">Contents of the secret key file</param>
/// <param name="fingerprint">Fingerprint of the key</param>
/// <returns>ASCII-armored signature</returns>
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);
}
}
}
}
}
Loading

0 comments on commit c2fe7fa

Please sign in to comment.