Skip to content

Commit

Permalink
Merge pull request #78 from microsoft/users/edwinlantigua/cmd-cds-val…
Browse files Browse the repository at this point in the history
…idation

Command Line Validation of Indirect Signatures
  • Loading branch information
elantiguamsft authored Feb 14, 2024
2 parents ff15e17 + 46c5044 commit 817cff7
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 62 deletions.
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);
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);
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);
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);
}

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();
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;

// 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

0 comments on commit 817cff7

Please sign in to comment.