Skip to content
Open
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
72 changes: 72 additions & 0 deletions CycloneDX.Tests/XmlBomSignerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using Xunit;
using System.Threading.Tasks;
using System.Xml;
using CycloneDX.Services;

namespace CycloneDX.Tests
{
public class XmlBomSignerTests
{

private const string TestBom = """
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6"
serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
version="1">
<components>
<component type="library">
<publisher>Apache</publisher>
<group>org.apache.tomcat</group>
<name>tomcat-catalina</name>
<version>9.0.14</version>
<hashes>
<hash alg="MD5">3942447fac867ae5cdb3229b658f4d48</hash>
<hash alg="SHA-1">e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a</hash>
<hash alg="SHA-256">f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b</hash>
<hash alg="SHA-512">e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282</hash>
</hashes>
<licenses>
<license>
<id>Apache-2.0</id>
</license>
</licenses>
<purl>pkg:maven/org.apache.tomcat/tomcat-catalina@9.0.14</purl>
</component>
</components>
</bom>
""";

[Fact]
public async Task SignAsyncShouldAddXmlSignatureElement()
{
var signer = new XmlBomSigner();
var tempKeyFile = Path.GetTempFileName();

using var rsa = RSA.Create();
var privateKeyPem = rsa.ExportRSAPrivateKeyPem();
await File.WriteAllTextAsync(tempKeyFile, privateKeyPem);

var signedXml = await signer.SignAsync(tempKeyFile, TestBom);

var doc = new XmlDocument();
doc.LoadXml(signedXml);

var signatureNode = doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
Assert.True(signatureNode.Count == 1, "Expected a Signature Element in BOM after Signing.");
}

[Fact]
public async Task SignAsyncThrowsOnMalformedPrivateKes()
{
var signer = new XmlBomSigner();
var tempKeyFile = Path.GetTempFileName();
await File.WriteAllTextAsync(tempKeyFile, "Not a valid PEM Key");

await Assert.ThrowsAsync<InvalidSigningKeyException>(() =>
signer.SignAsync(tempKeyFile, TestBom));
}
}
}
1 change: 1 addition & 0 deletions CycloneDX/CycloneDX.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageReference Include="NuGet.ProjectModel" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Security.Cryptography.Xml" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion CycloneDX/ExitCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum ExitCode
LocalPackageCacheError,
DotnetRestoreFailed,
GitHubLicenseResolutionFailed,
UnableToLocateDependencyBomRef
UnableToLocateDependencyBomRef,
UnsupportedSignatureFormat
}
}
26 changes: 26 additions & 0 deletions CycloneDX/Interfaces/IBomSigner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This file is part of CycloneDX Tool for .NET
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

using System.Threading.Tasks;

namespace CycloneDX.Interfaces
{
public interface IBomSigner
{
public Task<string> SignAsync(string keyFile, string bomContent);
}
}
2 changes: 1 addition & 1 deletion CycloneDX/Models/RunOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace CycloneDX.Models
{
public class RunOptions
{
public string SigningKeyPath { get; set; }
public string SolutionOrProjectFile { get; set; }
public string runtime { get; set; }
public string framework { get; set; }
Expand Down Expand Up @@ -52,6 +53,5 @@ public class RunOptions
public string DependencyExcludeFilter { get; set; }
public OutputFileFormat outputFormat { get; set; }
public SpecificationVersion? specVersion { get; set; }

}
}
6 changes: 5 additions & 1 deletion CycloneDX/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public static Task<int> Main(string[] args)
aliases: new[] { "--spec-version", "-spv" },
description: $"Which version of CycloneDX spec to use. [default: {SpecificationVersionHelpers.VersionString(SpecificationVersionHelpers.CurrentVersion)}]");
specVersion.FromAmong(Enum.GetValues<SpecificationVersion>().Select(SpecificationVersionHelpers.VersionString).ToArray());
var signingKey = new Option<string>(new[] { "--signing-key", "-sk" },
description: "Sign the generated BOM with the Key at the given path.");

//Deprecated args
var disableGithubLicenses = new Option<bool>(new[] { "--disable-github-licenses", "-dgl" }, "(Deprecated, this is the default setting now");
Expand Down Expand Up @@ -106,6 +108,7 @@ public static Task<int> Main(string[] args)
setVersion,
setType,
setNugetPurl,
signingKey,
specVersion,
outputFilenameDeprecated,
excludeDevDeprecated,
Expand Down Expand Up @@ -152,7 +155,8 @@ public static Task<int> Main(string[] args)
outputFormat = context.ParseResult.GetValueForOption(outputFormat),
specVersion = context.ParseResult.HasOption(specVersion)
? SpecificationVersionHelpers.Version(context.ParseResult.GetValueForOption(specVersion))
: null
: null,
SigningKeyPath = context.ParseResult.GetValueForOption(signingKey)
};

Runner runner = new Runner();
Expand Down
20 changes: 18 additions & 2 deletions CycloneDX/Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public async Task<int> HandleCommandAsync(RunOptions options)
bool setNugetPurl = options.setNugetPurl;
Models.OutputFileFormat outputFormat = options.outputFormat;
SpecificationVersion specVersion = options.specVersion ?? SpecificationVersionHelpers.CurrentVersion;
string signingKeyPath = options.SigningKeyPath;


Console.WriteLine();
Expand Down Expand Up @@ -168,8 +169,8 @@ public async Task<int> HandleCommandAsync(RunOptions options)

try
{
if (SolutionOrProjectFile.ToLowerInvariant().EndsWith(".sln", StringComparison.OrdinalIgnoreCase) ||
SolutionOrProjectFile.ToLowerInvariant().EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) ||
if (SolutionOrProjectFile.ToLowerInvariant().EndsWith(".sln", StringComparison.OrdinalIgnoreCase) ||
SolutionOrProjectFile.ToLowerInvariant().EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) ||
SolutionOrProjectFile.ToLowerInvariant().EndsWith(".slnf", StringComparison.OrdinalIgnoreCase))
{
if (!fileSystem.File.Exists(SolutionOrProjectFile))
Expand Down Expand Up @@ -442,6 +443,21 @@ public async Task<int> HandleCommandAsync(RunOptions options)
var (format, filename) = DetermineOutputFileFormatAndFilename(outputFormat, outputFilename, json);
var bomContents = BomService.CreateDocument(bom, format);

if (!string.IsNullOrEmpty(signingKeyPath))
{
if (format == OutputFileFormat.Xml)
{
IBomSigner signer = new XmlBomSigner();
bomContents = await signer.SignAsync(signingKeyPath, bomContents);
}
else
{
Console.WriteLine("Signing the BOM is only supported with XML BOMs at the moment.");
return (int)ExitCode.UnsupportedSignatureFormat;
}
}


// check if the output directory exists and create it if needed
var bomPath = this.fileSystem.Path.GetFullPath(outputDirectory);
if (!this.fileSystem.Directory.Exists(bomPath))
Expand Down
92 changes: 92 additions & 0 deletions CycloneDX/Services/XmlBomSigner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// This file is part of CycloneDX Tool for .NET
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Text;
using CycloneDX.Interfaces;
using System.Threading.Tasks;
using System.Xml;

namespace CycloneDX.Services
{
public class InvalidSigningKeyException(string message, Exception innerException)
: Exception(message, innerException);

public class XmlBomSigner : IBomSigner
{
public async Task<string> SignAsync(string keyFile, string bomContent)
{
var privateKey = await File.ReadAllTextAsync(keyFile).ConfigureAwait(false);
using var rsa = RSA.Create();

try
{
rsa.ImportFromPem(privateKey);
}
catch (ArgumentException e)
{
throw new InvalidSigningKeyException("The provided Signing Key is malformed", e);
}

var bom = new XmlDocument();
bom.PreserveWhitespace = true;
bomContent = bomContent.TrimStart('\uFEFF', '\u200B');
bom.LoadXml(bomContent);

var signedBom = new SignedXml(bom);
signedBom.SigningKey = rsa;
var reference = new Reference("");
var envelope = new XmlDsigEnvelopedSignatureTransform();

reference.AddTransform(envelope);
reference.AddTransform(new XmlDsigC14NTransform());
signedBom.AddReference(reference);

var keyInfo = new KeyInfo();
keyInfo.AddClause(new RSAKeyValue(rsa));
signedBom.KeyInfo = keyInfo;

signedBom.ComputeSignature();

var signature = signedBom.GetXml();
bom.DocumentElement!.AppendChild(bom.ImportNode(signature, true));

using var memoryStream = new MemoryStream();
var xmlWriterSettings = new XmlWriterSettings
{
Indent = true,
IndentChars = " ",
NewLineChars = "\r\n",
OmitXmlDeclaration = false,
Encoding = new UTF8Encoding(false),
Async = true
};

using (var xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))
{
bom.Save(xmlWriter);
await xmlWriter.FlushAsync();
}

var utf8String = Encoding.UTF8.GetString(memoryStream.ToArray());
return utf8String;
}
}
}
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
<PackageVersion Include="NuGet.Protocol" Version="6.9.1" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.9" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.IO.Abstractions" Version="21.0.2" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
</ItemGroup>
</Project>
</Project>