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

Command Line Validation of Indirect Signatures #78

Merged
merged 9 commits into from
Feb 14, 2024
Merged
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
40 changes: 26 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [v1.1.6-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.6-pre1) (2024-02-07)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.7...v1.1.6-pre1)

## [v1.1.7](https://github.com/microsoft/CoseSignTool/tree/v1.1.7) (2024-02-07)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.6...v1.1.7)

**Merged pull requests:**

- Action Permission Changes [\#76](https://github.com/microsoft/CoseSignTool/pull/76) ([elantiguamsft](https://github.com/elantiguamsft))

## [v1.1.6](https://github.com/microsoft/CoseSignTool/tree/v1.1.6) (2024-02-07)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.5...v1.1.6)
Expand Down Expand Up @@ -58,19 +70,19 @@

## [v1.1.1-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1-pre1) (2024-01-17)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1...v1.1.1-pre1)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre7...v1.1.1-pre1)

**Merged pull requests:**

- Move CreateChangelog to after build in PR build [\#70](https://github.com/microsoft/CoseSignTool/pull/70) ([lemccomb](https://github.com/lemccomb))

## [v1.1.1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1) (2024-01-12)
## [v1.1.0-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre7) (2024-01-12)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre7...v1.1.1)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.1...v1.1.0-pre7)

## [v1.1.0-pre7](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre7) (2024-01-12)
## [v1.1.1](https://github.com/microsoft/CoseSignTool/tree/v1.1.1) (2024-01-12)

[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre6...v1.1.0-pre7)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v1.1.0-pre6...v1.1.1)

**Closed issues:**

Expand Down Expand Up @@ -127,7 +139,7 @@

## [v1.1.0-pre1](https://github.com/microsoft/CoseSignTool/tree/v1.1.0-pre1) (2023-11-03)

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

**Merged pull requests:**

Expand All @@ -137,13 +149,13 @@
- DetachedSignatureFactory accepts pre-hashed content as payload [\#53](https://github.com/microsoft/CoseSignTool/pull/53) ([elantiguamsft](https://github.com/elantiguamsft))
- Add password support for certificate files [\#52](https://github.com/microsoft/CoseSignTool/pull/52) ([lemccomb](https://github.com/lemccomb))

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

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

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

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

**Merged pull requests:**

Expand All @@ -153,13 +165,13 @@
- 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.9](https://github.com/microsoft/CoseSignTool/tree/v0.3.1-pre.9) (2023-09-28)
## [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.2...v0.3.1-pre.9)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.9...v0.3.2)

## [v0.3.2](https://github.com/microsoft/CoseSignTool/tree/v0.3.2) (2023-09-28)
## [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.2)
[Full Changelog](https://github.com/microsoft/CoseSignTool/compare/v0.3.1-pre.8...v0.3.1-pre.9)

**Merged pull requests:**

Expand Down
65 changes: 65 additions & 0 deletions CoseHandler.Tests/CoseSignValidateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace CoseSignUnitTests;

using CoseDetachedSignature;
using System.Net.Mime;
using System.Runtime.ConstrainedExecution;

[TestClass]
public class CoseHandlerSignValidateTests
{
Expand Down Expand Up @@ -332,6 +336,67 @@ public void DetachedValidateModifiedPayload()
result.Success.Should().Be(false);
result.Errors.Should().Contain(e => e.ErrorCode.Equals(ValidationFailureCode.PayloadMismatch));
}

[TestMethod]
public void IndirectSignatureValidation()
{
var msgFac = new DetachedSignatureFactory();
byte[] signedBytes = msgFac.CreateDetachedSignatureBytes(
payload: Payload1Bytes,
contentType: "application/spdx+json",
signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray();

// Try to validate byte[]
var result = CoseHandler.Validate(signedBytes, Payload1Bytes, ValidRootSetPriv, RevMode);
result.Success.Should().Be(true);

// Try to validate stream
var result2 = CoseHandler.Validate(signedBytes, new MemoryStream(Payload1Bytes), ValidRootSetPriv, RevMode);
Dismissed Show dismissed Hide dismissed
result2.Success.Should().Be(true);
}

[TestMethod]
public void IndirectSignatureModifiedPayload()
{
var msgFac = new DetachedSignatureFactory();
byte[] signedBytes = msgFac.CreateDetachedSignatureBytes(
payload: Payload1Bytes,
contentType: "application/spdx+json",
signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray();

// Now change one character in the payload
var modifiedPayload = Encoding.ASCII.GetBytes("Payload2!");

// Try to validate byte[]
var result = CoseHandler.Validate(signedBytes, modifiedPayload, ValidRootSetPriv, RevMode);
result.Success.Should().Be(false);
result.Errors.Should().Contain(e => e.ErrorCode.Equals(ValidationFailureCode.PayloadMismatch));

// Try to validate stream
var result2 = CoseHandler.Validate(signedBytes, new MemoryStream(modifiedPayload), ValidRootSetPriv, RevMode);
Dismissed Show dismissed Hide dismissed
result2.Success.Should().Be(false);
result2.Errors.Should().Contain(e => e.ErrorCode.Equals(ValidationFailureCode.PayloadMismatch));
}

[TestMethod]
public void IndirectSignatureUntrustedSignature()
{
var msgFac = new DetachedSignatureFactory();
byte[] signedBytes = msgFac.CreateDetachedSignatureBytes(
payload: Payload1Bytes,
contentType: "application/spdx+json",
signingKeyProvider: new X509Certificate2CoseSigningKeyProvider(Leaf1Priv)).ToArray();

// Try to validate byte[]
var result = CoseHandler.Validate(signedBytes, Payload1Bytes, null, RevMode);
result.Success.Should().Be(false);
result.Errors.Should().Contain(e => e.ErrorCode.Equals(ValidationFailureCode.TrustValidationFailed));

// Try to validate stream
var result2 = CoseHandler.Validate(signedBytes, new MemoryStream(Payload1Bytes), null, RevMode);
Dismissed Show dismissed Hide dismissed
result2.Success.Should().Be(false);
result2.Errors.Should().Contain(e => e.ErrorCode.Equals(ValidationFailureCode.TrustValidationFailed));
}
#endregion

#region TryX wrappers
Expand Down
32 changes: 32 additions & 0 deletions CoseHandler/ContentValidationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace CoseX509;

/// <summary>
/// A set of COSE content validation types. Content validation refers to the method used to validate
/// that the payload has not been modified.
/// </summary>
public enum ContentValidationType
{
/// <summary>
/// Indicates that validation on the content was not performed.
/// </summary>
ContentValidationNotPerformed = 0,

/// <summary>
/// Indicates validation using a detached payload. The payload is not included in the message.
/// </summary>
Detached = 1,

/// <summary>
/// Indicates validation using an embedded payload. The payload is included in the message.
/// </summary>
Embedded = 2,

/// <summary>
/// Indicates validation using an indirect payload. The payload is hashed using the algorithm in the COSE message
/// and then the payload hash is compared to the embedded content.
/// </summary>
Indirect = 3
}
60 changes: 41 additions & 19 deletions CoseHandler/CoseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace CoseX509;
using CoseSign1.Certificates.Local.Validators;
using CoseSign1.Extensions;
using CoseSign1.Interfaces;
using CoseDetachedSignature.Extensions;

/// <summary>
/// Contains static methods to generate and validate Cose X509 signatures.
Expand Down Expand Up @@ -557,14 +558,14 @@ internal static ValidationResult ValidateInternal(
{
errorCodes.Add(ValidationFailureCode.SigningCertificateUnreadable);
content = null;
return new ValidationResult(false, errorCodes);
return new ValidationResult(false, errorCodes, validationType: ContentValidationType.ContentValidationNotPerformed);
elantiguamsft marked this conversation as resolved.
Show resolved Hide resolved
}

if (!msg.TryGetCertificateChain(out List<X509Certificate2>? chain, true))
{
errorCodes.Add(ValidationFailureCode.CertificateChainUnreadable);
content = null;
return new ValidationResult(false, errorCodes);
return new ValidationResult(false, errorCodes, validationType: ContentValidationType.ContentValidationNotPerformed);
}

// Populate the output parameter
Expand All @@ -574,14 +575,14 @@ internal static ValidationResult ValidateInternal(
if (!validator.TryValidate(msg, out List<CoseSign1ValidationResult> certValidationResults))
{
errorCodes.Add(ValidationFailureCode.TrustValidationFailed);
return new ValidationResult(false, errorCodes, certValidationResults, chain);
return new ValidationResult(false, errorCodes, certValidationResults, chain, validationType: ContentValidationType.ContentValidationNotPerformed);
}

// Get the signing certificate
if (!msg.TryGetSigningCertificate(out X509Certificate2? signingCertificate, true) || signingCertificate is null)
{
errorCodes.Add(ValidationFailureCode.CertificateChainUnreadable); // Is this always correct? Can there be certs found with none of them being the signing cert?
return new ValidationResult(false, errorCodes, certValidationResults, chain);
return new ValidationResult(false, errorCodes, certValidationResults, chain, validationType: ContentValidationType.ContentValidationNotPerformed);
}

// Get the public key
Expand All @@ -591,27 +592,46 @@ internal static ValidationResult ValidateInternal(
if (publicKey is null)
{
errorCodes.Add(ValidationFailureCode.NoPublicKey);
return new ValidationResult(false, errorCodes, certValidationResults, chain);
return new ValidationResult(false, errorCodes, certValidationResults, chain, validationType: ContentValidationType.ContentValidationNotPerformed);
}

// Validate that the COSE header is formatted correctly and that the payload and hash are consistent.
bool messageVerified = false;

// check for external payload
bool hasBytes = !payloadBytes.IsNullOrEmpty();
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
bool hasStream = payloadStream is not null;

// Determine the type of content validation to perform.
// Check for an indirect signature, where the content header contains the hash of the payload, and the algorithm is stored in the message.
// If this is the case and external content is provided, we can validate an external payload hash against the hash stored in the cose message content.
ContentValidationType cvt = msg.IsDetachedSignature() ? ContentValidationType.Indirect :
(hasBytes || hasStream) ? ContentValidationType.Detached : ContentValidationType.Embedded;

try
{
if (!payloadBytes.IsNullOrEmpty())
{
// Detached payload received as byte array
messageVerified = msg.VerifyDetached(publicKey, new ReadOnlySpan<byte>(payloadBytes));
}
else if (payloadStream is not null)
{
// Detached payload received as a stream
messageVerified = Task.Run(() => msg.VerifyDetachedAsync(publicKey, payloadStream)).GetAwaiter().GetResult();
}
else
switch (cvt)
{
// Embedded payload
messageVerified = msg.VerifyEmbedded(publicKey);
// Indirect signature validation. Validate external payload hash against embedded hash + Embedded signature validation.
case ContentValidationType.Indirect:
messageVerified = hasBytes ?
(msg.VerifyEmbedded(publicKey) && msg.SignatureMatches(payloadBytes)) :
hasStream ?
(msg.VerifyEmbedded(publicKey) && msg.SignatureMatches(payloadStream)) :
throw new InvalidOperationException();
break;
elantiguamsft marked this conversation as resolved.
Show resolved Hide resolved

// Detached signature validation. Validate external payload against the signature.
case ContentValidationType.Detached:
messageVerified = hasBytes ?
msg.VerifyDetached(publicKey, new ReadOnlySpan<byte>(payloadBytes)) :
Task.Run(() => msg.VerifyDetachedAsync(publicKey, payloadStream)).GetAwaiter().GetResult();
break;

// Embedded signature validation. Validate the embedded content against the signature.
case ContentValidationType.Embedded:
messageVerified = msg.VerifyEmbedded(publicKey);
break;
}

if (!messageVerified)
Expand All @@ -630,9 +650,11 @@ internal static ValidationResult ValidateInternal(
errorCodes.Add(
payloadBytes is null && payloadStream is null ? ValidationFailureCode.PayloadMissing :
ValidationFailureCode.RedundantPayload);

messageVerified = false;
}

return new ValidationResult(messageVerified, errorCodes, certValidationResults, chain);
return new ValidationResult(messageVerified, errorCodes, certValidationResults, chain, cvt);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions CoseHandler/CoseHandler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CoseDetachedSignature\CoseDetachedSignature.csproj" />
<ProjectReference Include="..\CoseSign1.Abstractions\CoseSign1.Abstractions.csproj" />
<ProjectReference Include="..\CoseSign1.Certificates\CoseSign1.Certificates.csproj" />
<ProjectReference Include="..\CoseSign1\CoseSign1.csproj" />
Expand Down
2 changes: 1 addition & 1 deletion CoseHandler/CoseValidationError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public CoseValidationError(ValidationFailureCode errorCode)
{ ValidationFailureCode.CertificateChainInvalid, "Certificate chain validation failed." },
{ ValidationFailureCode.TrustValidationFailed, "The signature failed to validate against the trust validator." },
{ ValidationFailureCode.PayloadMismatch, "The supplied or embedded payload does not match the hash of the payload that was signed." },
{ ValidationFailureCode.PayloadMissing, "The detached signature could not be validated because the original payload was not supplied."},
{ ValidationFailureCode.PayloadMissing, "The detached or indirect signature could not be validated because the external payload was not supplied."},
{ ValidationFailureCode.PayloadUnreadable, "The payload content could not be read."},
{ ValidationFailureCode.RedundantPayload, "The embedded signature was not validated because external payload was also specified."},
{ ValidationFailureCode.CoseHeadersInvalid, "The COSE headers in the signature could not be read." },
Expand Down
2 changes: 1 addition & 1 deletion CoseHandler/ValidationFailureCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public enum ValidationFailureCode
PayloadUnreadable,

/// <summary>
/// Required payload was not supplied for detached signature.
/// Required payload was not supplied for a detached or indirect signature.
/// </summary>
PayloadMissing,

Expand Down
Loading
Loading