Skip to content

Commit

Permalink
Merge pull request #52 from microsoft/user/lemccomb/passwordsupport
Browse files Browse the repository at this point in the history
Add password support for certificate files
  • Loading branch information
lemccomb authored Oct 11, 2023
2 parents ed8919e + 5a24f79 commit 16a81fa
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 72 deletions.
38 changes: 11 additions & 27 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
# Changelog

## [Unreleased](https://github.com/microsoft/CoseSignTool/tree/HEAD)
## [v1.1.0](https://github.com/microsoft/CoseSignTool/tree/v1.1.0) (2023-10-10)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.12...HEAD)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.10...v1.1.0)

**Merged pull requests:**

- implement detached signature factory, tests and helper extension methods. [\#47](https://github.com/microsoft/CoseSignTool/pull/47) ([JeromySt](https://github.com/JeromySt))
## [v0.3.1-pre.10](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.10) (2023-10-10)

## [v0.3.1-pre.12](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.12) (2023-10-02)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.11...v0.3.1-pre.12)

**Merged pull requests:**

- Re-enable CodeQL [\#45](https://github.com/microsoft/CoseSignTool/pull/45) ([lemccomb](https://github.com/lemccomb))

## [v0.3.1-pre.11](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.11) (2023-10-02)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.10...v0.3.1-pre.11)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.1-pre.10)

**Merged pull requests:**

- Correct folder mapping, update Nuget packages, fix for ADO compatibility [\#51](https://github.com/microsoft/CoseSignTool/pull/51) ([lemccomb](https://github.com/lemccomb))
- Port CoseHandler to .NET Standard 2.0 [\#48](https://github.com/microsoft/CoseSignTool/pull/48) ([lemccomb](https://github.com/lemccomb))
- implement detached signature factory, tests and helper extension methods. [\#47](https://github.com/microsoft/CoseSignTool/pull/47) ([JeromySt](https://github.com/JeromySt))
- Port changes from ADO repo to GitHub repo [\#46](https://github.com/microsoft/CoseSignTool/pull/46) ([lemccomb](https://github.com/lemccomb))
- Re-enable CodeQL [\#45](https://github.com/microsoft/CoseSignTool/pull/45) ([lemccomb](https://github.com/lemccomb))

## [v0.3.1-pre.10](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.10) (2023-10-02)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.10)

**Merged pull requests:**
## [v0.3.1-pre.9](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.9) (2023-09-28)

- Port CoseHandler to .NET Standard 2.0 [\#48](https://github.com/microsoft/CoseSignTool/pull/48) ([lemccomb](https://github.com/lemccomb))
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.2...v0.3.1-pre.9)

## [v0.3.2](https://github.com/microsoft/CoseSignTool/tree/v0.3.2) (2023-09-28)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.2)

## [v0.3.1-pre.9](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.9) (2023-09-28)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.1-pre.9)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.2)

**Merged pull requests:**

Expand Down
2 changes: 1 addition & 1 deletion CoseHandler/CoseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static class CoseHandler
// static instance of the factory for creating new CoseSign1Messages
private static readonly ICoseSign1MessageFactory Factory = new CoseSign1MessageFactory();

#region Sign
#region Sign Overloads
/// <summary>
/// Signs the payload content with the supplied certificate and returns a ReadOnlyMemory object containing the COSE signatureFile.
/// </summary>
Expand Down
55 changes: 41 additions & 14 deletions CoseSignTool.tests/MainTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

namespace CoseSignUnitTests;

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using CST = CoseSignTool.CoseSignTool;

[TestClass]
Expand All @@ -21,12 +24,15 @@ public class MainTests
// File paths to export them to
private static readonly string PrivateKeyCertFileSelfSigned = Path.GetTempFileName() + "_SelfSigned.pfx";
private static readonly string PublicKeyCertFileSelfSigned = Path.GetTempFileName() + "_SelfSigned.cer";
private static string PrivateKeyRootCertFile;
private static string PublicKeyIntermediateCertFile;
private static string PublicKeyRootCertFile;
private static string PrivateKeyCertFileChained;
private static string PayloadFile;
private static readonly string PrivateKeyRootCertFile = Path.GetTempFileName() + ".pfx";
private static readonly string PublicKeyIntermediateCertFile = Path.GetTempFileName() + ".cer";
private static readonly string PublicKeyRootCertFile = Path.GetTempFileName() + ".cer";
private static readonly string PrivateKeyCertFileChained = Path.GetTempFileName() + ".pfx";
private static readonly string PrivateKeyCertFileChainedWithPassword = Path.GetTempFileName() + ".pfx";
private static readonly string CertPassword = Guid.NewGuid().ToString();


private static string PayloadFile;
private static readonly byte[] Payload1Bytes = Encoding.ASCII.GetBytes("Payload1!");

public MainTests()
Expand All @@ -38,14 +44,11 @@ public MainTests()
// export generated certs to files
File.WriteAllBytes(PrivateKeyCertFileSelfSigned, SelfSignedCert.Export(X509ContentType.Pkcs12));
File.WriteAllBytes(PublicKeyCertFileSelfSigned, SelfSignedCert.Export(X509ContentType.Cert));
PrivateKeyRootCertFile = Path.GetTempFileName() + ".pfx";
File.WriteAllBytes(PrivateKeyRootCertFile, Root1Priv.Export(X509ContentType.Pkcs12));
PublicKeyRootCertFile = Path.GetTempFileName() + ".cer";
File.WriteAllBytes(PublicKeyRootCertFile, Root1Priv.Export(X509ContentType.Cert));
PublicKeyIntermediateCertFile = Path.GetTempFileName() + ".cer";
File.WriteAllBytes(PublicKeyIntermediateCertFile, Int1Priv.Export(X509ContentType.Cert));
PrivateKeyCertFileChained = Path.GetTempFileName() + ".pfx";
File.WriteAllBytes(PublicKeyIntermediateCertFile, Int1Priv.Export(X509ContentType.Cert));
File.WriteAllBytes(PrivateKeyCertFileChained, Leaf1Priv.Export(X509ContentType.Pkcs12));
File.WriteAllBytes(PrivateKeyCertFileChainedWithPassword, Leaf1Priv.Export(X509ContentType.Pkcs12, CertPassword));
}

[TestMethod]
Expand All @@ -55,21 +58,21 @@ public void FromMainValid()

// sign detached
string[] args1 = { "sign", @"/p", PayloadFile, @"/pfx", PrivateKeyCertFileChained };
CST.Main(args1).Should().Be(0, "Detach sign failed.");
CST.Main(args1).Should().Be((int)ExitCode.Success, "Detach sign failed.");

// sign embedded
string[] args2 = { "sign", @"/pfx", PrivateKeyCertFileChained, @"/p", PayloadFile, @"/ep" };
CST.Main(args2).Should().Be(0, "Embed sign failed.");
CST.Main(args2).Should().Be((int)ExitCode.Success, "Embed sign failed.");

// validate detached
string sigFile = PayloadFile + ".cose";
string[] args3 = { "validate", @"/rt", certPair, @"/sf", sigFile, @"/p", PayloadFile, "/rm", "NoCheck" };
CST.Main(args3).Should().Be(0, "Detach validation failed.");
CST.Main(args3).Should().Be((int)ExitCode.Success, "Detach validation failed.");

// validate embedded
sigFile = PayloadFile + ".csm";
string[] args4 = { "validate", @"/rt", certPair, @"/sf", sigFile, "/rm", "NoCheck" };
CST.Main(args4).Should().Be(0, "Embed validation failed.");
CST.Main(args4).Should().Be((int)ExitCode.Success, "Embed validation failed.");

// get content
string saveFile = PayloadFile + ".saved";
Expand All @@ -78,6 +81,30 @@ public void FromMainValid()
File.ReadAllText(PayloadFile).Should().Be(File.ReadAllText(saveFile), "Saved content did not match payload.");
}

[TestMethod]
public void SignWithPasswordProtectedCertSuccess()
{
// sign detached with password protected cert
string[] args1 = { "sign", @"/p", PayloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", CertPassword };
CST.Main(args1).Should().Be((int)ExitCode.Success, "Detach sign with password protected cert failed.");
}

[TestMethod]
public void SignWithPasswordProtectedCertNoPassword()
{
// sign detached with password protected cert
string[] args1 = { "sign", @"/p", PayloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword };
CST.Main(args1).Should().Be((int)ExitCode.CertificateLoadFailure, "Detach sign did not fail in the expected way.");
}

[TestMethod]
public void SignWithPasswordProtectedCertWrongPassword()
{
// sign detached with password protected cert
string[] args1 = { "sign", @"/p", PayloadFile, @"/pfx", PrivateKeyCertFileChainedWithPassword, @"/pw", "NotThePassword" };
CST.Main(args1).Should().Be((int)ExitCode.CertificateLoadFailure, "Detach sign did not fail in the expected way.");
}

[TestMethod]
public void FromMainInvalid()
{
Expand Down
56 changes: 40 additions & 16 deletions CoseSignTool/SignCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ public sealed class SignCommand : CoseCommand
["-PipeOutput"] = "PipeOutput",
["-po"] = "PipeOutput",
["-PfxCertificate"] = "PfxCertificate",
["-Password"] = "Password",
["-pw"] = "Password",
["-Thumbprint"] = "Thumbprint",
["-th"] = "Thumbprint",
["-StoreName"] = "StoreName",
["-sn"] = "StoreName",
["-StoreLocation"] = "StoreLocation",
["-sl"] = "StoreLocation",
["-ContentType"] = "ContentType",
["-cty"] = "ContentType",
["-pfx"] = "PfxCertificate",
["-Thumbprint"] = "Thumbprint",
["-th"] = "Thumbprint",
Expand Down Expand Up @@ -51,10 +61,15 @@ public sealed class SignCommand : CoseCommand
public bool PipeOutput { get; set; }

/// <summary>
/// Optional. Gets or set the path to a .pfx file containing the private key certificate to sign with.
/// Optional. Gets or sets the path to a .pfx file containing the private key certificate to sign with.
/// </summary>
public string? PfxCertificate { get; set; }

/// <summary>
/// Optional. Gets or sets the password for the .pfx file if it requires one.
/// </summary>
public string? Password { get; set; }

/// <summary>
/// Optional. Gets or sets the SHA1 thumbprint of a certificate in the Certificate Store to sign the file with.
/// </summary>
Expand Down Expand Up @@ -111,7 +126,7 @@ public override ExitCode Run()
{
cert = LoadCert();
}
catch (Exception ex) when (ex is CoseSign1CertificateException or FileNotFoundException)
catch (Exception ex) when (ex is CoseSign1CertificateException or FileNotFoundException or CryptographicException)
{
ExitCode exitCode = ex is CoseSign1CertificateException ? ExitCode.StoreCertificateNotFound : ExitCode.CertificateLoadFailure;
return CoseSignTool.Fail(exitCode, ex);
Expand Down Expand Up @@ -166,6 +181,7 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p
PipeOutput = GetOptionBool(provider, nameof (PipeOutput));
Thumbprint = GetOptionString(provider, nameof(Thumbprint));
PfxCertificate = GetOptionString(provider, nameof(PfxCertificate));
Password = GetOptionString(provider, nameof(Password));
ContentType = GetOptionString(provider, nameof(ContentType), CoseSign1MessageFactory.DEFAULT_CONTENT_TYPE);
StoreName = GetOptionString(provider, nameof(StoreName), DefaultStoreName);
string? sl = GetOptionString(provider, nameof(StoreLocation), DefaultStoreLocation);
Expand All @@ -179,15 +195,17 @@ protected internal override void ApplyOptions(CommandLineConfigurationProvider p
/// <returns>The certificate if found.</returns>
/// <exception cref="ArgumentOutOfRangeException">User passed in a thumbprint instead of a file path on a non-Windows OS.</exception>
/// <exception cref="ArgumentNullException">No certificate filepath or thumbprint was given.</exception>
/// <exception cref="CryptographicException">The certificate was found but could not be loaded.</exception>
/// <exception cref="CryptographicException">The certificate was found but could not be loaded
/// -- OR --
/// The certificate required a password and the user did not supply one, or the user-supplied password was wrong.</exception>
internal X509Certificate2 LoadCert()
{
X509Certificate2 cert;
if (PfxCertificate is not null)
{
// Load the PFX certificate.
// Load the PFX certificate. This will throw a CryptographicException if the password is wrong or missing.
ThrowIfMissing(PfxCertificate, "Could not find the certificate file");
cert = new X509Certificate2(PfxCertificate);
cert = new X509Certificate2(PfxCertificate, Password);
}
else
{
Expand Down Expand Up @@ -217,20 +235,26 @@ A detached signature resides in a separate file and validates against the origin
Default value is [payload file].cose for detached signatures or [payload file].csm for embedded.
Required if neither PayloadFile or PipeOutput are set.
PipeOutput /po: Optional. If set, outputs the detached or embedded COSE signature to Standard Out instead of writing
to file.
A signing certificate as either:
PfxCertificate / pfx: A path to a private key certificate file (.pfx) to sign with.
Password / pw: Optional. The password for the .pfx file if it has one.
PfxCertificate / pfx: A path to a private key certificate file (.pfx) to sign with.
--OR--
Thumbprint / th: The SHA1 thumbprint of a certificate in the local certificate store to sign the file with.
Use the optional StoreName and StoreLocation parameters to tell CoseSignTool where to find the matching
certificate.
--OR--
StoreName / sn: Optional. The name of the local certificate store to find the signing certificate in.
Default value is 'My'.
Thumbprint / th: The SHA1 thumbprint of a certificate in the local certificate store to sign the file with.
Use the optional StoreName and StoreLocation parameters to tell CoseSignTool where to find the matching
certificate.
StoreLocation / sl: Optional. The location of the local certificate store to find the signing certificate in.
Default value is 'CurrentUser'.
StoreName / sn: Optional. The name of the local certificate store to find the signing certificate in.
Default value is 'My'.
StoreLocation / sl: Optional. The location of the local certificate store to find the signing certificate in.
Default value is 'CurrentUser'.
PipeOutput /po: Optional. If set, outputs the detached or embedded COSE signature to Standard Out instead of writing
to file.
EmbedPayload / ep: Optional. If true, encrypts and embeds a copy of the payload in the in COSE signature file.
Default behavior is 'detached signing', where the signature is in a separate file from the payload.
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ This is an alpha release, so there are some planned features that are not yet in
The planned work is currently tracked only in an internal Microsoft ADO instance but will be moved to Github Issues soon. In the meantime, here is some of the work currently planned.

#### New features
* Add /Password option for signing with locked .pfx certificate files
* Investigate adding suport for RFC3161 timestamp counter signatures
* Enable specifying a mandatory cert chain root for validation
* Implement digest signing
* Simplify digest signing scenario
* Support batch operations in CoseSignTool to reduce file and cert store reads
* Publish single file version of CoseSignTool

Expand All @@ -50,8 +49,6 @@ The planned work is currently tracked only in an internal Microsoft ADO instance
* Expand code coverage in unit and integration tests

#### Other
* Downgrade the CoseSign1 libraries from .NET Standard 2.1 to 2.0 (Compatibility)
* Convert CoseHandler from .NET 7 to .NET Standard 2.0 to match other libs
* Move work item tracking to public Github repo
* Re-organize the CoseSignTool unit tests for better readability

Expand Down
22 changes: 13 additions & 9 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ See [Stye.md](./STYLE.md) for details.
## Testing
All unit tests in the repo must pass in Windows, Linux, and MacOS environments to ensure compatitility.

## Pull Request Process
_Note: There is a bug in the pull request process which causes Github to lose track of running workflows when the CreateChangelog job completes. The work around is to close and re-open the pull request on the pull request page <span>(https<nolink>://github.com/microsoft/CoseSignTool/pull/[pull-request-number])</span>_
1. Clone the [repo](https://github.com/microsoft/CoseSignTool).
1. Create a user or feature branch off of main. Do not use the keyword "hotfix" or "develope" in your branch names as these will trigger incorrect release behavior.
1. Make your changes, including adding or updating unit tests to ensure your changes work as intended.
1. Make sure the solution still builds and all unit tests still pass locally.
1. Update any documentation, user and contributor, that is impacted by your changes. See [CoseSignTool.md](./CoseSignTool.md) for the CoseSignTool project, [CoseHandler.md](./CoseHandler.md) for the CoseHandler project, and [Advanced.md](./Advanced.md) for the CoseSign1 projects.
1. Push your changes to origin and create a pull request into main from your branch. The pull request automation will re-run the unit tests in Windows, MacOS, and Linux environments.
1. Fix any build or test failures or CodeQL warnings caught by the pull request automation and push the fixes to your branch.
1. Address any code review comments.
1. You may merge the pull request in once you have the sign-off of at least two Microsoft full-time employees, including at least one other developer.
Do not modify CHANGELOG.md, as it is auto-generated.

## Releases
Releases are created automatically on completion of a pull request into main, and have the pre-release flag set. Official releases are created manually by the repo owners and do not use the pre-release flag.
In both cases, the built binaries and other assets for the release are made available in .zip files.
Expand All @@ -28,14 +41,5 @@ In both cases, the built binaries and other assets for the release are made avai
1. Make sure the _Set as a pre-release_ box is _not_ checked.
1. Click _Publish release_.

## Pull Request Process
1. Clone the [repo](https://github.com/microsoft/CoseSignTool).
1. Create a user or feature branch off of main. Do not use the keyword "hotfix" or "develope" in your branch names as these will trigger incorrect release behavior.
1. Submit pull requests into main from your branch.
1. Ensure builds are still successful and tests, including any added or updated tests, pass locally prior to submitting the pull request. The pull request automation will re-run the unit tests in Windows, MacOS, and Linux environments.
1. Update any documentation, user and contributor, that is impacted by your changes. See [CoseSignTool.md](./CoseSignTool.md) for the CoseSignTool project, [CoseHandler.md](./CoseHandler.md) for the CoseHandler project, and [Advanced.md](./Advanced.md) for the CoseSign1 projects.
1. You may merge the pull request in once you have the sign-off of at least two Microsoft full-time employees, including at least one other developer.
Do not modify CHANGELOG.md is it is generated by the pull request process.

## License Information
[MIT License](https://github.com/microsoft/CoseSignTool/blob/main/LICENSE)
Loading

0 comments on commit 16a81fa

Please sign in to comment.