diff --git a/Directory.Packages.props b/Directory.Packages.props
index 19ae67d..ec62838 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,8 +6,12 @@
+
+
+
+
@@ -15,7 +19,10 @@
+
+
+
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/AzureSign.Core.Opc.csproj b/src/AzureSign.Core.Opc/AzureSign.Core.Opc.csproj
new file mode 100644
index 0000000..bda844e
--- /dev/null
+++ b/src/AzureSign.Core.Opc/AzureSign.Core.Opc.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+ link
+
+ v
+ true
+ true
+ MIT
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Containers/HlkxContainer.cs b/src/AzureSign.Core.Opc/Containers/HlkxContainer.cs
new file mode 100644
index 0000000..4b3f022
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Containers/HlkxContainer.cs
@@ -0,0 +1,310 @@
+using System.IO.Compression;
+using System.Text;
+using System.Xml;
+using AzureSign.Core.Opc.Models;
+
+namespace AzureSign.Core.Opc.Containers;
+
+///
+/// ZIP-based implementation of HLKX OPC container.
+///
+public class HlkxContainer : IHlkxContainer
+{
+ private readonly string _filePath;
+ private readonly ZipArchive _archive;
+ private readonly Dictionary _parts;
+ private readonly List _relationships;
+ private bool _modified;
+ private bool _disposed;
+
+ private HlkxContainer(string filePath, ZipArchive archive)
+ {
+ _filePath = filePath;
+ _archive = archive;
+ _parts = new Dictionary();
+ _relationships = new List();
+ _modified = false;
+
+ LoadContainer();
+ }
+
+ ///
+ /// Opens an existing HLKX file for reading and modification.
+ ///
+ public static HlkxContainer Open(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException($"HLKX file not found: {filePath}");
+
+ var archive = ZipFile.Open(filePath, ZipArchiveMode.Update);
+ return new HlkxContainer(filePath, archive);
+ }
+
+ public IEnumerable GetParts() => _parts.Values;
+
+ public OpcPart? GetPart(string path) => _parts.TryGetValue(path, out var part) ? part : null;
+
+ public void AddPart(string path, byte[] content, string contentType)
+ {
+ var part = new OpcPart(path, contentType, content) { IsModified = true };
+ _parts[path] = part;
+ _modified = true;
+ }
+
+ public void UpdatePart(string path, byte[] content)
+ {
+ if (_parts.TryGetValue(path, out var part))
+ {
+ part.UpdateContent(content);
+ _modified = true;
+ }
+ else
+ {
+ throw new InvalidOperationException($"Part not found: {path}");
+ }
+ }
+
+ public IEnumerable GetRelationships() => _relationships;
+
+ public void AddRelationship(string target, string relationshipType, string? id = null)
+ {
+ id ??= GenerateRelationshipId();
+ var relationship = new OpcRelationship(id, relationshipType, target);
+ _relationships.Add(relationship);
+ _modified = true;
+ }
+
+ public void AddSignatureParts(Dictionary signatureParts)
+ {
+ foreach (var kvp in signatureParts)
+ {
+ var contentType = GetSignaturePartContentType(kvp.Key);
+ AddPart(kvp.Key, kvp.Value, contentType);
+ }
+ }
+
+ public void RemoveAllSignatures()
+ {
+ // Remove signature-related parts
+ var signatureParts = _parts.Keys
+ .Where(path => path.StartsWith("/package/services/digital-signature/"))
+ .ToList();
+
+ foreach (var partPath in signatureParts)
+ {
+ _parts.Remove(partPath);
+ }
+
+ // Remove signature relationships
+ var signatureRelationships = _relationships
+ .Where(r => r.RelationshipType.Contains("digital-signature"))
+ .ToList();
+
+ foreach (var rel in signatureRelationships)
+ {
+ _relationships.Remove(rel);
+ }
+
+ if (signatureParts.Any() || signatureRelationships.Any())
+ {
+ _modified = true;
+ }
+ }
+
+ public bool HasSignatures => _parts.Keys.Any(path => path.StartsWith("/package/services/digital-signature/"));
+
+ public void UpdateContentTypes()
+ {
+ var contentTypesXml = BuildContentTypesXml();
+ AddPart("/[Content_Types].xml", Encoding.UTF8.GetBytes(contentTypesXml),
+ "application/vnd.openxmlformats-package.content-types+xml");
+ }
+
+ public void Save()
+ {
+ if (!_modified) return;
+
+ // Update relationships
+ UpdateRelationshipsFile();
+
+ // Update content types
+ UpdateContentTypes();
+
+ // Write all modified parts to ZIP
+ foreach (var part in _parts.Values.Where(p => p.IsModified))
+ {
+ WritePartToArchive(part);
+ }
+
+ _modified = false;
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Save();
+ _archive?.Dispose();
+ _disposed = true;
+ }
+ }
+
+ private void LoadContainer()
+ {
+ // Load all parts from ZIP archive
+ foreach (var entry in _archive.Entries)
+ {
+ if (entry.FullName.EndsWith('/')) continue; // Skip directories
+
+ var path = "/" + entry.FullName.Replace('\\', '/');
+ var contentType = GetContentTypeFromPath(path);
+
+ using var stream = entry.Open();
+ using var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ var content = memoryStream.ToArray();
+
+ _parts[path] = new OpcPart(path, contentType, content);
+ }
+
+ // Load relationships from _rels/.rels
+ LoadRelationships();
+ }
+
+ private void LoadRelationships()
+ {
+ var relsPart = GetPart("/_rels/.rels");
+ if (relsPart == null) return;
+
+ try
+ {
+ var doc = relsPart.GetContentAsXml();
+ var nsManager = new XmlNamespaceManager(doc.NameTable);
+ nsManager.AddNamespace("r", "http://schemas.openxmlformats.org/package/2006/relationships");
+
+ var relationshipNodes = doc.SelectNodes("//r:Relationship", nsManager);
+ if (relationshipNodes == null) return;
+
+ foreach (XmlNode node in relationshipNodes)
+ {
+ if (node.Attributes == null) continue;
+
+ var id = node.Attributes["Id"]?.Value;
+ var type = node.Attributes["Type"]?.Value;
+ var target = node.Attributes["Target"]?.Value;
+
+ if (id != null && type != null && target != null)
+ {
+ _relationships.Add(new OpcRelationship(id, type, target));
+ }
+ }
+ }
+ catch (XmlException)
+ {
+ // Handle invalid XML gracefully
+ }
+ }
+
+ private void UpdateRelationshipsFile()
+ {
+ var xml = BuildRelationshipsXml();
+ var content = Encoding.UTF8.GetBytes(xml);
+
+ if (_parts.ContainsKey("/_rels/.rels"))
+ {
+ UpdatePart("/_rels/.rels", content);
+ }
+ else
+ {
+ AddPart("/_rels/.rels", content, "application/vnd.openxmlformats-package.relationships+xml");
+ }
+ }
+
+ private string BuildRelationshipsXml()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("");
+ sb.AppendLine("");
+
+ foreach (var rel in _relationships)
+ {
+ sb.AppendLine($" ");
+ }
+
+ sb.AppendLine("");
+ return sb.ToString();
+ }
+
+ private string BuildContentTypesXml()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("");
+ sb.AppendLine("");
+
+ // Default extensions
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+
+ // Signature-specific content types
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+
+ // Override for specific parts
+ foreach (var part in _parts.Values)
+ {
+ if (part.Path.StartsWith("/hck/data/") && !part.Path.EndsWith(".xml"))
+ {
+ sb.AppendLine($" ");
+ }
+ }
+
+ sb.AppendLine("");
+ return sb.ToString();
+ }
+
+ private void WritePartToArchive(OpcPart part)
+ {
+ var entryName = part.Path.TrimStart('/');
+
+ // Remove existing entry if it exists
+ var existingEntry = _archive.GetEntry(entryName);
+ existingEntry?.Delete();
+
+ // Create new entry
+ var entry = _archive.CreateEntry(entryName);
+ using var stream = entry.Open();
+ stream.Write(part.Content);
+ }
+
+ private string GetContentTypeFromPath(string path)
+ {
+ return path switch
+ {
+ "/_rels/.rels" => "application/vnd.openxmlformats-package.relationships+xml",
+ "/[Content_Types].xml" => "application/vnd.openxmlformats-package.content-types+xml",
+ var p when p.EndsWith(".psdsor") => "application/vnd.openxmlformats-package.digital-signature-origin",
+ var p when p.EndsWith(".psdsxs") => "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml",
+ var p when p.EndsWith(".cer") => "application/vnd.openxmlformats-package.digital-signature-certificate",
+ _ => "application/octet"
+ };
+ }
+
+ private string GetSignaturePartContentType(string path)
+ {
+ return path switch
+ {
+ var p when p.EndsWith("origin.psdsor") => "application/vnd.openxmlformats-package.digital-signature-origin",
+ var p when p.EndsWith(".psdsxs") => "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml",
+ var p when p.EndsWith(".cer") => "application/vnd.openxmlformats-package.digital-signature-certificate",
+ var p when p.Contains("/_rels/") => "application/vnd.openxmlformats-package.relationships+xml",
+ _ => "application/octet"
+ };
+ }
+
+ private string GenerateRelationshipId()
+ {
+ return "R" + Guid.NewGuid().ToString("N")[..8].ToUpperInvariant();
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Containers/IHlkxContainer.cs b/src/AzureSign.Core.Opc/Containers/IHlkxContainer.cs
new file mode 100644
index 0000000..fcb77df
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Containers/IHlkxContainer.cs
@@ -0,0 +1,64 @@
+using AzureSign.Core.Opc.Models;
+
+namespace AzureSign.Core.Opc.Containers;
+
+///
+/// Interface for HLKX OPC container operations.
+///
+public interface IHlkxContainer : IDisposable
+{
+ ///
+ /// Gets all parts in the container.
+ ///
+ IEnumerable GetParts();
+
+ ///
+ /// Gets a specific part by path.
+ ///
+ OpcPart? GetPart(string path);
+
+ ///
+ /// Adds a new part to the container.
+ ///
+ void AddPart(string path, byte[] content, string contentType);
+
+ ///
+ /// Updates an existing part's content.
+ ///
+ void UpdatePart(string path, byte[] content);
+
+ ///
+ /// Gets all relationships in the container.
+ ///
+ IEnumerable GetRelationships();
+
+ ///
+ /// Adds a relationship to the container.
+ ///
+ void AddRelationship(string target, string relationshipType, string? id = null);
+
+ ///
+ /// Adds signature-related parts to the container.
+ ///
+ void AddSignatureParts(Dictionary signatureParts);
+
+ ///
+ /// Removes all existing signatures from the container.
+ ///
+ void RemoveAllSignatures();
+
+ ///
+ /// Gets whether the container has any signatures.
+ ///
+ bool HasSignatures { get; }
+
+ ///
+ /// Updates the [Content_Types].xml file.
+ ///
+ void UpdateContentTypes();
+
+ ///
+ /// Saves all changes to the container.
+ ///
+ void Save();
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Models/OpcPart.cs b/src/AzureSign.Core.Opc/Models/OpcPart.cs
new file mode 100644
index 0000000..3eab731
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Models/OpcPart.cs
@@ -0,0 +1,59 @@
+using System.Text;
+using System.Xml;
+
+namespace AzureSign.Core.Opc.Models;
+
+///
+/// Represents a part (file) within an OPC package.
+///
+public class OpcPart
+{
+ public string Path { get; }
+ public string ContentType { get; set; }
+ public byte[] Content { get; set; }
+ public bool IsModified { get; set; }
+
+ public OpcPart(string path, string contentType, byte[] content)
+ {
+ Path = path ?? throw new ArgumentNullException(nameof(path));
+ ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType));
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ IsModified = false;
+ }
+
+ ///
+ /// Gets the content as a UTF-8 string.
+ ///
+ public string GetContentAsString()
+ {
+ return Encoding.UTF8.GetString(Content);
+ }
+
+ ///
+ /// Gets the content as an XML document.
+ ///
+ public XmlDocument GetContentAsXml()
+ {
+ var doc = new XmlDocument();
+ doc.LoadXml(GetContentAsString());
+ return doc;
+ }
+
+ ///
+ /// Updates the content from a string, marking the part as modified.
+ ///
+ public void UpdateContent(string content)
+ {
+ Content = Encoding.UTF8.GetBytes(content);
+ IsModified = true;
+ }
+
+ ///
+ /// Updates the content from bytes, marking the part as modified.
+ ///
+ public void UpdateContent(byte[] content)
+ {
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ IsModified = true;
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Models/OpcRelationship.cs b/src/AzureSign.Core.Opc/Models/OpcRelationship.cs
new file mode 100644
index 0000000..f4074a2
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Models/OpcRelationship.cs
@@ -0,0 +1,28 @@
+namespace AzureSign.Core.Opc.Models;
+
+///
+/// Represents a relationship within an OPC package.
+///
+public class OpcRelationship
+{
+ public string Id { get; }
+ public string RelationshipType { get; }
+ public string Target { get; }
+ public string SourceUri { get; }
+
+ public OpcRelationship(string id, string relationshipType, string target, string sourceUri = "/")
+ {
+ Id = id ?? throw new ArgumentNullException(nameof(id));
+ RelationshipType = relationshipType ?? throw new ArgumentNullException(nameof(relationshipType));
+ Target = target ?? throw new ArgumentNullException(nameof(target));
+ SourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri));
+ }
+
+ ///
+ /// Creates a relationship XML element.
+ ///
+ public string ToXmlElement()
+ {
+ return $"";
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Signatures/HlkxSigner.cs b/src/AzureSign.Core.Opc/Signatures/HlkxSigner.cs
new file mode 100644
index 0000000..2a8b5d1
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Signatures/HlkxSigner.cs
@@ -0,0 +1,291 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using AzureSign.Core.Opc.Containers;
+using AzureSign.Core.Opc.Models;
+using Microsoft.Extensions.Logging;
+
+namespace AzureSign.Core.Opc.Signatures;
+
+///
+/// HLKX package signer implementation that works with Azure Key Vault HSM certificates.
+///
+public class HlkxSigner : IHlkxSigner
+{
+ private const int S_OK = 0;
+ private const int E_FAIL = unchecked((int)0x80004005);
+ private const int E_INVALIDARG = unchecked((int)0x80070057);
+
+ public async Task SignFileAsync(
+ string filePath,
+ AsymmetricAlgorithm signingAlgorithm,
+ X509Certificate2 certificate,
+ HashAlgorithmName hashAlgorithm,
+ ILogger? logger = null)
+ {
+ try
+ {
+ logger?.LogInformation("Starting HLKX signing process for: {FilePath}", filePath);
+
+ if (!File.Exists(filePath))
+ {
+ logger?.LogError("HLKX file not found: {FilePath}", filePath);
+ return E_INVALIDARG;
+ }
+
+ using var container = HlkxContainer.Open(filePath);
+
+ // Remove any existing signatures
+ container.RemoveAllSignatures();
+
+ // Get parts and relationships that need to be signed
+ var partsToSign = GetPartsRequiringSignature(container).ToList();
+ var relationshipsToSign = GetRelationshipsRequiringSignature(container).ToList();
+
+ logger?.LogDebug("Found {PartCount} parts and {RelationshipCount} relationships to sign",
+ partsToSign.Count, relationshipsToSign.Count);
+
+ // Create manifest
+ var manifestBuilder = new OpcManifestBuilder(hashAlgorithm);
+ var manifestXml = manifestBuilder.CreateManifest(partsToSign, relationshipsToSign);
+
+ // Create temporary certificate for the signing structure
+ // This follows the fork's proven approach for HSM integration
+ using var tempCert = CreateTemporaryCertificate(signingAlgorithm, hashAlgorithm, logger);
+
+ // Prepare SignedInfo for signing
+ var signedInfoData = PrepareSignedInfo(manifestXml, hashAlgorithm);
+
+ // Sign with Azure Key Vault (remote operation)
+ logger?.LogDebug("Performing remote signing operation with Azure Key Vault");
+ var signatureValue = await PerformRemoteSigningAsync(signingAlgorithm, signedInfoData, hashAlgorithm);
+
+ // Create final digital signature
+ var signatureId = Guid.NewGuid().ToString("N");
+ var signature = new OpcDigitalSignature(
+ signatureId,
+ signatureValue,
+ certificate,
+ hashAlgorithm,
+ manifestXml);
+
+ // Add signature parts to container
+ var signatureParts = signature.GenerateSignatureParts();
+ container.AddSignatureParts(signatureParts);
+
+ // Add signature relationship to root
+ container.AddRelationship(
+ "/package/services/digital-signature/origin.psdsor",
+ "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin");
+
+ // Save the signed container
+ container.Save();
+
+ logger?.LogInformation("HLKX file signed successfully");
+ return S_OK;
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, "Failed to sign HLKX file: {FilePath}", filePath);
+ return E_FAIL;
+ }
+ }
+
+ public async Task VerifyFileAsync(string filePath, ILogger? logger = null)
+ {
+ try
+ {
+ logger?.LogInformation("Verifying HLKX signature for: {FilePath}", filePath);
+
+ if (!File.Exists(filePath))
+ {
+ logger?.LogError("HLKX file not found: {FilePath}", filePath);
+ return false;
+ }
+
+ using var container = HlkxContainer.Open(filePath);
+
+ if (!container.HasSignatures)
+ {
+ logger?.LogWarning("HLKX file has no signatures: {FilePath}", filePath);
+ return false;
+ }
+
+ // Basic verification - check if signature parts exist and are well-formed
+ // For full cryptographic verification, you would need to:
+ // 1. Parse the XML signature
+ // 2. Recalculate part digests
+ // 3. Verify the signature against the certificate
+ // 4. Check certificate chain validity
+
+ var signatureParts = container.GetParts()
+ .Where(p => p.Path.StartsWith("/package/services/digital-signature/"))
+ .ToList();
+
+ logger?.LogDebug("Found {SignaturePartCount} signature parts", signatureParts.Count);
+
+ // Minimal validation - ensure we have the required signature components
+ var hasOrigin = signatureParts.Any(p => p.Path.EndsWith("origin.psdsor"));
+ var hasXmlSignature = signatureParts.Any(p => p.Path.Contains("xml-signature") && p.Path.EndsWith(".psdsxs"));
+ var hasCertificate = signatureParts.Any(p => p.Path.Contains("certificate") && p.Path.EndsWith(".cer"));
+
+ var isValid = hasOrigin && hasXmlSignature && hasCertificate;
+
+ logger?.LogInformation("HLKX signature verification result: {IsValid}", isValid);
+ return isValid;
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, "Failed to verify HLKX file: {FilePath}", filePath);
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the parts that require signing based on HLKX specification.
+ ///
+ private IEnumerable GetPartsRequiringSignature(IHlkxContainer container)
+ {
+ foreach (var part in container.GetParts())
+ {
+ // Sign all data parts (critical for HLKX validation)
+ if (part.Path.StartsWith("/hck/data/") ||
+ part.Path == "/_rels/.rels")
+ {
+ yield return part;
+ }
+ }
+ }
+
+ ///
+ /// Gets the relationships that require signing based on HLKX specification.
+ ///
+ private IEnumerable GetRelationshipsRequiringSignature(IHlkxContainer container)
+ {
+ // Sign specific relationship types found in the analysis
+ var targetTypes = new HashSet
+ {
+ "http://microsoft.com/schemas/windows/kits/hardware/2010/streamdata",
+ "http://microsoft.com/schemas/windows/kits/hardware/2010/coredata",
+ "http://microsoft.com/schemas/windows/kits/hardware/2010/packageinfo",
+ "http://microsoft.com/shcemas/windows/kits/hardware/2010/packageinfo" // Note: typo in original spec
+ };
+
+ return container.GetRelationships()
+ .Where(r => targetTypes.Contains(r.RelationshipType));
+ }
+
+ ///
+ /// Creates a temporary self-signed certificate for the signing process.
+ /// This follows the fork's proven approach for HSM integration.
+ ///
+ private X509Certificate2 CreateTemporaryCertificate(
+ AsymmetricAlgorithm signingAlgorithm,
+ HashAlgorithmName hashAlgorithm,
+ ILogger? logger)
+ {
+ logger?.LogDebug("Creating temporary certificate for signing structure");
+
+ var rsa = signingAlgorithm as RSA ?? throw new ArgumentException("Only RSA algorithms are supported");
+
+ var request = new CertificateRequest(
+ "CN=TemporarySelfSignedHlkxCertificate",
+ rsa,
+ hashAlgorithm,
+ RSASignaturePadding.Pkcs1);
+
+ var certificate = request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(7));
+
+ logger?.LogDebug("Created temporary certificate with thumbprint: {Thumbprint}", certificate.Thumbprint);
+ return certificate;
+ }
+
+ ///
+ /// Prepares the SignedInfo data for signing.
+ ///
+ private byte[] PrepareSignedInfo(string manifestXml, HashAlgorithmName hashAlgorithm)
+ {
+ // Create SignedInfo element
+ var digestAlgorithm = GetDigestAlgorithmUrl(hashAlgorithm);
+ var signatureMethod = GetSignatureMethodUrl(hashAlgorithm);
+
+ // Compute manifest digest
+ var manifestBytes = System.Text.Encoding.UTF8.GetBytes(manifestXml);
+ using var hasher = CreateHasher(hashAlgorithm);
+ var manifestDigest = Convert.ToBase64String(hasher.ComputeHash(manifestBytes));
+
+ var signedInfo = $@"
+
+
+
+
+ {manifestDigest}
+
+";
+
+ return System.Text.Encoding.UTF8.GetBytes(signedInfo);
+ }
+
+ ///
+ /// Performs the actual signing operation using the Azure Key Vault RSA instance.
+ ///
+ private async Task PerformRemoteSigningAsync(
+ AsymmetricAlgorithm signingAlgorithm,
+ byte[] dataToSign,
+ HashAlgorithmName hashAlgorithm)
+ {
+ if (signingAlgorithm is RSA rsa)
+ {
+ // For RSA, we can use SignData directly
+ return rsa.SignData(dataToSign, hashAlgorithm, RSASignaturePadding.Pkcs1);
+ }
+ else if (signingAlgorithm is ECDsa ecdsa)
+ {
+ // For ECDSA, we need to hash first then sign
+ using var hasher = CreateHasher(hashAlgorithm);
+ var hash = hasher.ComputeHash(dataToSign);
+ return ecdsa.SignHash(hash);
+ }
+ else
+ {
+ throw new NotSupportedException($"Signing algorithm {signingAlgorithm.GetType().Name} is not supported");
+ }
+ }
+
+ private HashAlgorithm CreateHasher(HashAlgorithmName hashAlgorithm)
+ {
+ return hashAlgorithm.Name switch
+ {
+ "SHA1" => SHA1.Create(),
+ "SHA256" => SHA256.Create(),
+ "SHA384" => SHA384.Create(),
+ "SHA512" => SHA512.Create(),
+ _ => SHA256.Create()
+ };
+ }
+
+ private string GetDigestAlgorithmUrl(HashAlgorithmName hashAlgorithm)
+ {
+ return hashAlgorithm.Name switch
+ {
+ "SHA1" => "http://www.w3.org/2000/09/xmldsig#sha1",
+ "SHA256" => "http://www.w3.org/2001/04/xmlenc#sha256",
+ "SHA384" => "http://www.w3.org/2001/04/xmldsig-more#sha384",
+ "SHA512" => "http://www.w3.org/2001/04/xmlenc#sha512",
+ _ => "http://www.w3.org/2001/04/xmlenc#sha256"
+ };
+ }
+
+ private string GetSignatureMethodUrl(HashAlgorithmName hashAlgorithm)
+ {
+ return hashAlgorithm.Name switch
+ {
+ "SHA1" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "SHA256" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ "SHA384" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
+ "SHA512" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
+ _ => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Signatures/IHlkxSigner.cs b/src/AzureSign.Core.Opc/Signatures/IHlkxSigner.cs
new file mode 100644
index 0000000..09c164b
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Signatures/IHlkxSigner.cs
@@ -0,0 +1,35 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
+
+namespace AzureSign.Core.Opc.Signatures;
+
+///
+/// Interface for HLKX package signing operations.
+///
+public interface IHlkxSigner
+{
+ ///
+ /// Signs an HLKX file using the provided certificate and signing algorithm.
+ ///
+ /// Path to the HLKX file to sign
+ /// The asymmetric algorithm for signing (typically RSA from Azure Key Vault)
+ /// The X.509 certificate (public key only)
+ /// The hash algorithm to use for signing
+ /// Optional logger for diagnostic information
+ /// HRESULT indicating success (0) or failure
+ Task SignFileAsync(
+ string filePath,
+ AsymmetricAlgorithm signingAlgorithm,
+ X509Certificate2 certificate,
+ HashAlgorithmName hashAlgorithm,
+ ILogger? logger = null);
+
+ ///
+ /// Verifies the signature of an HLKX file.
+ ///
+ /// Path to the HLKX file to verify
+ /// Optional logger for diagnostic information
+ /// True if the signature is valid, false otherwise
+ Task VerifyFileAsync(string filePath, ILogger? logger = null);
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Signatures/OpcDigitalSignature.cs b/src/AzureSign.Core.Opc/Signatures/OpcDigitalSignature.cs
new file mode 100644
index 0000000..9c062d3
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Signatures/OpcDigitalSignature.cs
@@ -0,0 +1,163 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace AzureSign.Core.Opc.Signatures;
+
+///
+/// Represents an OPC digital signature with all its components.
+///
+public class OpcDigitalSignature
+{
+ public string SignatureId { get; }
+ public byte[] SignatureValue { get; }
+ public X509Certificate2 Certificate { get; }
+ public DateTime SignatureTime { get; }
+ public HashAlgorithmName HashAlgorithm { get; }
+ public string ManifestXml { get; }
+
+ public OpcDigitalSignature(
+ string signatureId,
+ byte[] signatureValue,
+ X509Certificate2 certificate,
+ HashAlgorithmName hashAlgorithm,
+ string manifestXml)
+ {
+ SignatureId = signatureId ?? throw new ArgumentNullException(nameof(signatureId));
+ SignatureValue = signatureValue ?? throw new ArgumentNullException(nameof(signatureValue));
+ Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
+ HashAlgorithm = hashAlgorithm;
+ ManifestXml = manifestXml ?? throw new ArgumentNullException(nameof(manifestXml));
+ SignatureTime = DateTime.UtcNow;
+ }
+
+ ///
+ /// Generates all OPC signature parts required for the package.
+ ///
+ public Dictionary GenerateSignatureParts()
+ {
+ var parts = new Dictionary();
+
+ // Certificate hash for filename
+ var certHash = GetCertificateHash();
+
+ // 1. Origin marker (empty file)
+ parts["/package/services/digital-signature/origin.psdsor"] = Array.Empty();
+
+ // 2. XML signature
+ var xmlSignature = CreateXmlSignature();
+ parts[$"/package/services/digital-signature/xml-signature/{SignatureId}.psdsxs"] = xmlSignature;
+
+ // 3. Certificate
+ parts[$"/package/services/digital-signature/certificate/{certHash}.cer"] = Certificate.RawData;
+
+ // 4. Origin relationships
+ var originRels = CreateOriginRelationships();
+ parts["/package/services/digital-signature/_rels/origin.psdsor.rels"] = originRels;
+
+ // 5. Signature relationships
+ var sigRels = CreateSignatureRelationships(certHash);
+ parts[$"/package/services/digital-signature/xml-signature/_rels/{SignatureId}.psdsxs.rels"] = sigRels;
+
+ return parts;
+ }
+
+ ///
+ /// Creates the XML signature document based on the HLKX reference structure.
+ ///
+ private byte[] CreateXmlSignature()
+ {
+ var xml = $@"
+
+
+
+
+
+
+ {ComputeManifestDigest()}
+
+
+ {Convert.ToBase64String(SignatureValue)}
+
+";
+
+ return System.Text.Encoding.UTF8.GetBytes(xml);
+ }
+
+ private byte[] CreateOriginRelationships()
+ {
+ var xml = $@"
+
+
+";
+
+ return System.Text.Encoding.UTF8.GetBytes(xml);
+ }
+
+ private byte[] CreateSignatureRelationships(string certHash)
+ {
+ var xml = $@"
+
+
+";
+
+ return System.Text.Encoding.UTF8.GetBytes(xml);
+ }
+
+ private string GetCertificateHash()
+ {
+ using var sha1 = SHA1.Create();
+ var hash = sha1.ComputeHash(Certificate.RawData);
+ return Convert.ToHexString(hash);
+ }
+
+ private string GetSignatureMethodAlgorithm()
+ {
+ return HashAlgorithm.Name switch
+ {
+ "SHA1" => "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "SHA256" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ "SHA384" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
+ "SHA512" => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
+ _ => "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ };
+ }
+
+ private string GetDigestAlgorithm()
+ {
+ return HashAlgorithm.Name switch
+ {
+ "SHA1" => "http://www.w3.org/2000/09/xmldsig#sha1",
+ "SHA256" => "http://www.w3.org/2001/04/xmlenc#sha256",
+ "SHA384" => "http://www.w3.org/2001/04/xmldsig-more#sha384",
+ "SHA512" => "http://www.w3.org/2001/04/xmlenc#sha512",
+ _ => "http://www.w3.org/2000/09/xmldsig#sha1"
+ };
+ }
+
+ private string ComputeManifestDigest()
+ {
+ var manifestBytes = System.Text.Encoding.UTF8.GetBytes(ManifestXml);
+
+ using System.Security.Cryptography.HashAlgorithm hasher = HashAlgorithm.Name switch
+ {
+ "SHA1" => SHA1.Create(),
+ "SHA256" => SHA256.Create(),
+ "SHA384" => SHA384.Create(),
+ "SHA512" => SHA512.Create(),
+ _ => SHA1.Create()
+ };
+
+ var hash = hasher.ComputeHash(manifestBytes);
+ return Convert.ToBase64String(hash);
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core.Opc/Signatures/OpcManifestBuilder.cs b/src/AzureSign.Core.Opc/Signatures/OpcManifestBuilder.cs
new file mode 100644
index 0000000..065955d
--- /dev/null
+++ b/src/AzureSign.Core.Opc/Signatures/OpcManifestBuilder.cs
@@ -0,0 +1,171 @@
+using System.Security.Cryptography;
+using System.Text;
+using AzureSign.Core.Opc.Models;
+
+namespace AzureSign.Core.Opc.Signatures;
+
+///
+/// Builds OPC manifest XML for digital signatures.
+///
+public class OpcManifestBuilder
+{
+ private readonly HashAlgorithmName _hashAlgorithm;
+
+ public OpcManifestBuilder(HashAlgorithmName hashAlgorithm)
+ {
+ _hashAlgorithm = hashAlgorithm;
+ }
+
+ ///
+ /// Creates the manifest XML containing references to parts and relationships.
+ ///
+ public string CreateManifest(IEnumerable partsToSign, IEnumerable relationshipsToSign)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("");
+
+ // Add part references
+ foreach (var part in partsToSign)
+ {
+ var digest = ComputePartDigest(part);
+ var contentType = part.ContentType;
+
+ if (part.Path == "/_rels/.rels")
+ {
+ // Special handling for relationships file with canonicalization
+ sb.AppendLine($" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine($" ");
+ sb.AppendLine($" {digest}");
+ sb.AppendLine(" ");
+ }
+ else
+ {
+ // Regular part reference
+ sb.AppendLine($" ");
+ sb.AppendLine($" ");
+ sb.AppendLine($" {digest}");
+ sb.AppendLine(" ");
+ }
+ }
+
+ // Add relationship references with transforms
+ if (relationshipsToSign.Any())
+ {
+ var relationshipDigest = ComputeRelationshipDigest(relationshipsToSign);
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+
+ // Add relationship group references based on HLKX analysis
+ foreach (var relType in GetUniqueRelationshipTypes(relationshipsToSign))
+ {
+ sb.AppendLine($" ");
+ }
+
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine($" ");
+ sb.AppendLine($" {relationshipDigest}");
+ sb.AppendLine(" ");
+ }
+
+ sb.AppendLine("");
+ return sb.ToString();
+ }
+
+ private string ComputePartDigest(OpcPart part)
+ {
+ byte[] contentToHash = part.Content;
+
+ // Apply canonicalization for XML content if needed
+ if (part.Path == "/_rels/.rels" || part.ContentType.Contains("xml"))
+ {
+ contentToHash = ApplyXmlCanonicalization(part.Content);
+ }
+
+ using var hasher = CreateHasher();
+ var hash = hasher.ComputeHash(contentToHash);
+ return Convert.ToBase64String(hash);
+ }
+
+ private string ComputeRelationshipDigest(IEnumerable relationships)
+ {
+ // Create filtered relationships XML for signing
+ var filteredXml = CreateFilteredRelationshipsXml(relationships);
+ var canonicalizedXml = ApplyXmlCanonicalization(Encoding.UTF8.GetBytes(filteredXml));
+
+ using var hasher = CreateHasher();
+ var hash = hasher.ComputeHash(canonicalizedXml);
+ return Convert.ToBase64String(hash);
+ }
+
+ private string CreateFilteredRelationshipsXml(IEnumerable relationships)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("");
+ sb.AppendLine("");
+
+ foreach (var rel in relationships.OrderBy(r => r.Id))
+ {
+ sb.AppendLine($" ");
+ }
+
+ sb.AppendLine("");
+ return sb.ToString();
+ }
+
+ private byte[] ApplyXmlCanonicalization(byte[] xmlContent)
+ {
+ try
+ {
+ var xml = Encoding.UTF8.GetString(xmlContent);
+
+ // Simple canonicalization - normalize whitespace and ensure consistent formatting
+ // For production, you might want to use XmlDsigC14NTransform for full C14N compliance
+ var normalized = xml
+ .Replace("\r\n", "\n")
+ .Replace("\r", "\n")
+ .Trim();
+
+ return Encoding.UTF8.GetBytes(normalized);
+ }
+ catch
+ {
+ // If XML parsing fails, return original content
+ return xmlContent;
+ }
+ }
+
+ private HashSet GetUniqueRelationshipTypes(IEnumerable relationships)
+ {
+ return relationships.Select(r => r.RelationshipType).ToHashSet();
+ }
+
+ private HashAlgorithm CreateHasher()
+ {
+ return _hashAlgorithm.Name switch
+ {
+ "SHA1" => SHA1.Create(),
+ "SHA256" => SHA256.Create(),
+ "SHA384" => SHA384.Create(),
+ "SHA512" => SHA512.Create(),
+ _ => SHA256.Create()
+ };
+ }
+
+ private string GetDigestAlgorithm()
+ {
+ return _hashAlgorithm.Name switch
+ {
+ "SHA1" => "http://www.w3.org/2000/09/xmldsig#sha1",
+ "SHA256" => "http://www.w3.org/2001/04/xmlenc#sha256",
+ "SHA384" => "http://www.w3.org/2001/04/xmldsig-more#sha384",
+ "SHA512" => "http://www.w3.org/2001/04/xmlenc#sha512",
+ _ => "http://www.w3.org/2001/04/xmlenc#sha256"
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs b/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs
index 7b2626e..15522e9 100644
--- a/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs
+++ b/src/AzureSign.Core/AuthenticodeKeyVaultSigner.cs
@@ -79,6 +79,13 @@ public AuthenticodeKeyVaultSigner(AsymmetricAlgorithm signingAlgorithm, X509Cert
/// was set to however the current operating system does not support appending signatures.
public unsafe int SignFile(ReadOnlySpan path, ReadOnlySpan description, ReadOnlySpan descriptionUrl, bool? pageHashing, ILogger? logger = null, bool appendSignature = false)
{
+ // Check for HLKX files and handle them with OPC signing
+ var sipKind = SipExtensionFactory.GetSipKind(path);
+ if (sipKind == SipKind.Hlkx)
+ {
+ return SignHlkxFile(path, description, descriptionUrl, logger);
+ }
+
static char[] NullTerminate(ReadOnlySpan str)
{
char[] result = new char[str.Length + 1];
@@ -163,11 +170,11 @@ static char[] NullTerminate(ReadOnlySpan str)
var signCallbackInfo = new SIGN_INFO(callbackPtr);
logger?.LogTrace("Getting SIP Data");
- var sipKind = SipExtensionFactory.GetSipKind(path);
+ var sipKindForSipData = SipExtensionFactory.GetSipKind(path);
void* sipData = (void*)0;
IntPtr context = IntPtr.Zero;
- switch (sipKind)
+ switch (sipKindForSipData)
{
case SipKind.Appx:
APPX_SIP_CLIENT_DATA clientData;
@@ -202,7 +209,7 @@ static char[] NullTerminate(ReadOnlySpan str)
{
Debug.Assert(mssign32.SignerFreeSignerContext(context) == 0);
}
- if (result == 0 && sipKind == SipKind.Appx)
+ if (result == 0 && sipKindForSipData == SipKind.Appx)
{
var state = ((APPX_SIP_CLIENT_DATA*)sipData)->pAppxSipState;
if (state != IntPtr.Zero)
@@ -276,5 +283,48 @@ private static unsafe void FillAppxExtension(
clientData.pSignerParams->pSignCallBack = signInfo;
}
+
+#if NET8_0_OR_GREATER
+ ///
+ /// Signs an HLKX file using OPC digital signatures.
+ ///
+ private int SignHlkxFile(ReadOnlySpan path, ReadOnlySpan description, ReadOnlySpan descriptionUrl, ILogger? logger)
+ {
+ try
+ {
+ var pathString = path.ToString();
+ logger?.LogInformation("Signing HLKX file: {Path}", pathString);
+
+ // Use the new OPC signer
+ var hlkxSigner = new AzureSign.Core.Opc.Signatures.HlkxSigner();
+ var result = hlkxSigner.SignFileAsync(pathString, _signingAlgorithm, _signingCertificate, _fileDigestAlgorithm, logger).GetAwaiter().GetResult();
+
+ if (result == 0)
+ {
+ logger?.LogInformation("HLKX file signed successfully: {Path}", pathString);
+ }
+ else
+ {
+ logger?.LogError("Failed to sign HLKX file: {Path}, HRESULT: {Result:X8}", pathString, result);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, "Exception occurred while signing HLKX file: {Path}", path.ToString());
+ return unchecked((int)0x80004005); // E_FAIL
+ }
+ }
+#else
+ ///
+ /// Signs an HLKX file using OPC digital signatures.
+ ///
+ private int SignHlkxFile(ReadOnlySpan path, ReadOnlySpan description, ReadOnlySpan descriptionUrl, ILogger? logger)
+ {
+ logger?.LogError("HLKX signing is only supported on .NET 8.0 or later");
+ return unchecked((int)0x80004005); // E_FAIL
+ }
+#endif
}
}
diff --git a/src/AzureSign.Core/AzureSign.Core.csproj b/src/AzureSign.Core/AzureSign.Core.csproj
index 172fb40..695d5c5 100644
--- a/src/AzureSign.Core/AzureSign.Core.csproj
+++ b/src/AzureSign.Core/AzureSign.Core.csproj
@@ -22,4 +22,8 @@
+
+
+
+
diff --git a/src/AzureSign.Core/SipExtensionFactory.cs b/src/AzureSign.Core/SipExtensionFactory.cs
index 68a6280..139af4f 100644
--- a/src/AzureSign.Core/SipExtensionFactory.cs
+++ b/src/AzureSign.Core/SipExtensionFactory.cs
@@ -6,7 +6,8 @@ namespace AzureSign.Core
internal enum SipKind
{
None,
- Appx
+ Appx,
+ Hlkx
}
internal class SipExtensionFactory
@@ -46,6 +47,10 @@ public static SipKind GetSipKind(ReadOnlySpan filePath)
{
return SipKind.Appx;
}
+ if (extension.Equals(".hlkx", StringComparison.OrdinalIgnoreCase))
+ {
+ return SipKind.Hlkx;
+ }
return SipKind.None;
}
}
diff --git a/src/AzureSignTool/AzureSignTool.csproj b/src/AzureSignTool/AzureSignTool.csproj
index 6e5b588..88ea3f7 100644
--- a/src/AzureSignTool/AzureSignTool.csproj
+++ b/src/AzureSignTool/AzureSignTool.csproj
@@ -32,5 +32,6 @@
+
diff --git a/test/AzureSign.Core.Opc.Tests/AzureSign.Core.Opc.Tests.csproj b/test/AzureSign.Core.Opc.Tests/AzureSign.Core.Opc.Tests.csproj
new file mode 100644
index 0000000..25e4d3e
--- /dev/null
+++ b/test/AzureSign.Core.Opc.Tests/AzureSign.Core.Opc.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/AzureSign.Core.Opc.Tests/Containers/HlkxContainerTests.cs b/test/AzureSign.Core.Opc.Tests/Containers/HlkxContainerTests.cs
new file mode 100644
index 0000000..22c517f
--- /dev/null
+++ b/test/AzureSign.Core.Opc.Tests/Containers/HlkxContainerTests.cs
@@ -0,0 +1,135 @@
+using AzureSign.Core.Opc.Containers;
+using AzureSign.Core.Opc.Tests.TestData;
+using FluentAssertions;
+using Xunit;
+
+namespace AzureSign.Core.Opc.Tests.Containers;
+
+public class HlkxContainerTests : IDisposable
+{
+ private readonly string _tempFile;
+
+ public HlkxContainerTests()
+ {
+ _tempFile = Path.GetTempFileName();
+
+ // Create synthetic HLKX file for testing
+ SyntheticHlkxGenerator.CreateMinimalHlkx(_tempFile);
+ }
+
+ [Fact]
+ public void Open_ValidHlkxFile_ShouldLoadParts()
+ {
+ // Arrange & Act
+ using var container = HlkxContainer.Open(_tempFile);
+
+ // Assert
+ var parts = container.GetParts().ToList();
+ parts.Should().NotBeEmpty();
+
+ // Should have at least the basic OPC parts
+ parts.Should().Contain(p => p.Path == "/[Content_Types].xml");
+ parts.Should().Contain(p => p.Path == "/_rels/.rels");
+
+ // Should have synthetic HCK data parts
+ parts.Should().Contain(p => p.Path == "/hck/data/PackageInfo.xml");
+ parts.Should().Contain(p => p.Path == "/hck/data/synthetic-data-1");
+ parts.Should().Contain(p => p.Path == "/hck/data/synthetic-data-2");
+ }
+
+ [Fact]
+ public void GetPart_ExistingPart_ShouldReturnPart()
+ {
+ // Arrange
+ using var container = HlkxContainer.Open(_tempFile);
+
+ // Act
+ var part = container.GetPart("/_rels/.rels");
+
+ // Assert
+ part.Should().NotBeNull();
+ part!.Path.Should().Be("/_rels/.rels");
+ part.ContentType.Should().Be("application/vnd.openxmlformats-package.relationships+xml");
+ }
+
+ [Fact]
+ public void AddPart_NewPart_ShouldAddToContainer()
+ {
+ // Arrange
+ using var container = HlkxContainer.Open(_tempFile);
+ var testContent = "test content"u8.ToArray();
+
+ // Act
+ container.AddPart("/test/part.txt", testContent, "text/plain");
+
+ // Assert
+ var part = container.GetPart("/test/part.txt");
+ part.Should().NotBeNull();
+ part!.Content.Should().BeEquivalentTo(testContent);
+ part.ContentType.Should().Be("text/plain");
+ }
+
+ [Fact]
+ public void HasSignatures_UnsignedFile_ShouldReturnFalse()
+ {
+ // Arrange
+ using var container = HlkxContainer.Open(_tempFile);
+
+ // Act & Assert
+ container.HasSignatures.Should().BeFalse();
+ }
+
+ [Fact]
+ public void AddSignatureParts_ValidParts_ShouldAddToContainer()
+ {
+ // Arrange
+ using var container = HlkxContainer.Open(_tempFile);
+ var signatureParts = new Dictionary
+ {
+ ["/package/services/digital-signature/origin.psdsor"] = Array.Empty(),
+ ["/package/services/digital-signature/xml-signature/test.psdsxs"] = "test"u8.ToArray()
+ };
+
+ // Act
+ container.AddSignatureParts(signatureParts);
+
+ // Assert
+ container.HasSignatures.Should().BeTrue();
+ container.GetPart("/package/services/digital-signature/origin.psdsor").Should().NotBeNull();
+ container.GetPart("/package/services/digital-signature/xml-signature/test.psdsxs").Should().NotBeNull();
+ }
+
+ [Fact]
+ public void HasSignatures_PreSignedFile_ShouldReturnTrue()
+ {
+ // Arrange
+ var preSignedFile = Path.GetTempFileName();
+ SyntheticHlkxGenerator.CreatePreSignedHlkx(preSignedFile);
+
+ try
+ {
+ using var container = HlkxContainer.Open(preSignedFile);
+
+ // Act & Assert
+ container.HasSignatures.Should().BeTrue();
+
+ // Should have signature parts
+ container.GetPart("/package/services/digital-signature/origin.psdsor").Should().NotBeNull();
+ var xmlSigParts = container.GetParts().Where(p => p.Path.Contains("xml-signature") && p.Path.EndsWith(".psdsxs"));
+ xmlSigParts.Should().NotBeEmpty();
+ }
+ finally
+ {
+ if (File.Exists(preSignedFile))
+ File.Delete(preSignedFile);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (File.Exists(_tempFile))
+ {
+ File.Delete(_tempFile);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/AzureSign.Core.Opc.Tests/Signatures/HlkxSignerTests.cs b/test/AzureSign.Core.Opc.Tests/Signatures/HlkxSignerTests.cs
new file mode 100644
index 0000000..d946ea6
--- /dev/null
+++ b/test/AzureSign.Core.Opc.Tests/Signatures/HlkxSignerTests.cs
@@ -0,0 +1,222 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using AzureSign.Core.Opc.Containers;
+using AzureSign.Core.Opc.Signatures;
+using AzureSign.Core.Opc.Tests.TestData;
+using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace AzureSign.Core.Opc.Tests.Signatures;
+
+public class HlkxSignerTests : IDisposable
+{
+ private readonly string _tempFile;
+ private readonly X509Certificate2 _testCertificate;
+ private readonly RSA _testRsa;
+
+ public HlkxSignerTests()
+ {
+ _tempFile = Path.GetTempFileName();
+ SyntheticHlkxGenerator.CreateMinimalHlkx(_tempFile);
+
+ // Create a test certificate and RSA key pair
+ _testRsa = RSA.Create(2048);
+ var request = new CertificateRequest(
+ "CN=Test Certificate for HLKX Signing",
+ _testRsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ _testCertificate = request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30));
+ }
+
+ [Fact]
+ public async Task SignFileAsync_ValidHlkxFile_ShouldSucceed()
+ {
+ // Arrange
+ var signer = new HlkxSigner();
+
+ // Act
+ var result = await signer.SignFileAsync(
+ _tempFile,
+ _testRsa,
+ _testCertificate,
+ HashAlgorithmName.SHA256,
+ NullLogger.Instance);
+
+ // Assert
+ result.Should().Be(0); // S_OK
+ }
+
+ [Fact]
+ public void Container_CanLoadSyntheticFile()
+ {
+ // Test if the basic container loading works
+ using var container = HlkxContainer.Open(_tempFile);
+ var parts = container.GetParts().ToList();
+
+ parts.Should().NotBeEmpty();
+ parts.Should().Contain(p => p.Path == "/[Content_Types].xml");
+ parts.Should().Contain(p => p.Path == "/_rels/.rels");
+ }
+
+ [Fact]
+ public void ManifestBuilder_CanCreateBasicManifest()
+ {
+ // Test if manifest creation works with synthetic data
+ using var container = HlkxContainer.Open(_tempFile);
+ var parts = container.GetParts()
+ .Where(p => p.Path.StartsWith("/hck/data/"))
+ .ToList();
+ var relationships = container.GetRelationships().ToList();
+
+ var manifestBuilder = new OpcManifestBuilder(HashAlgorithmName.SHA256);
+
+ // This should not throw an exception
+ var manifest = manifestBuilder.CreateManifest(parts, relationships);
+
+ manifest.Should().NotBeEmpty();
+ manifest.Should().Contain("Manifest");
+ }
+
+ [Fact]
+ public async Task SignFileAsync_NonExistentFile_ShouldFail()
+ {
+ // Arrange
+ var signer = new HlkxSigner();
+ var nonExistentFile = Path.Combine(Path.GetTempPath(), "nonexistent.hlkx");
+
+ // Act
+ var result = await signer.SignFileAsync(
+ nonExistentFile,
+ _testRsa,
+ _testCertificate,
+ HashAlgorithmName.SHA256,
+ NullLogger.Instance);
+
+ // Assert
+ result.Should().NotBe(0); // Should fail
+ }
+
+ [Fact]
+ public async Task VerifyFileAsync_UnsignedFile_ShouldReturnFalse()
+ {
+ // Arrange
+ var signer = new HlkxSigner();
+
+ // Act
+ var result = await signer.VerifyFileAsync(_tempFile, NullLogger.Instance);
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task VerifyFileAsync_SignedFile_ShouldReturnTrue()
+ {
+ // Arrange
+ var signer = new HlkxSigner();
+
+ // First sign the file
+ await signer.SignFileAsync(
+ _tempFile,
+ _testRsa,
+ _testCertificate,
+ HashAlgorithmName.SHA256,
+ NullLogger.Instance);
+
+ // Act
+ var result = await signer.VerifyFileAsync(_tempFile, NullLogger.Instance);
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task VerifyFileAsync_PreSignedSyntheticFile_ShouldReturnTrue()
+ {
+ // Arrange
+ var preSignedFile = Path.GetTempFileName();
+ SyntheticHlkxGenerator.CreatePreSignedHlkx(preSignedFile);
+ var signer = new HlkxSigner();
+
+ try
+ {
+ // Act
+ var result = await signer.VerifyFileAsync(preSignedFile, NullLogger.Instance);
+
+ // Assert
+ result.Should().BeTrue();
+ }
+ finally
+ {
+ if (File.Exists(preSignedFile))
+ File.Delete(preSignedFile);
+ }
+ }
+
+ [Fact]
+ public async Task SignFileAsync_InvalidHlkxFile_ShouldFail()
+ {
+ // Arrange
+ var invalidFile = Path.GetTempFileName();
+ SyntheticHlkxGenerator.CreateInvalidHlkx(invalidFile);
+ var signer = new HlkxSigner();
+
+ try
+ {
+ // Act
+ var result = await signer.SignFileAsync(
+ invalidFile,
+ _testRsa,
+ _testCertificate,
+ HashAlgorithmName.SHA256,
+ NullLogger.Instance);
+
+ // Assert
+ result.Should().NotBe(0); // Should fail
+ }
+ finally
+ {
+ if (File.Exists(invalidFile))
+ File.Delete(invalidFile);
+ }
+ }
+
+ [Theory]
+ [InlineData("SHA1")]
+ [InlineData("SHA256")]
+ [InlineData("SHA384")]
+ [InlineData("SHA512")]
+ public async Task SignFileAsync_DifferentHashAlgorithms_ShouldSucceed(string algorithmName)
+ {
+ // Arrange
+ var signer = new HlkxSigner();
+ var hashAlgorithm = new HashAlgorithmName(algorithmName);
+
+ // Act
+ var result = await signer.SignFileAsync(
+ _tempFile,
+ _testRsa,
+ _testCertificate,
+ hashAlgorithm,
+ NullLogger.Instance);
+
+ // Assert
+ result.Should().Be(0); // S_OK
+ }
+
+ public void Dispose()
+ {
+ if (File.Exists(_tempFile))
+ {
+ File.Delete(_tempFile);
+ }
+
+ _testCertificate?.Dispose();
+ _testRsa?.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/test/AzureSign.Core.Opc.Tests/TestContext.cs b/test/AzureSign.Core.Opc.Tests/TestContext.cs
new file mode 100644
index 0000000..18ed889
--- /dev/null
+++ b/test/AzureSign.Core.Opc.Tests/TestContext.cs
@@ -0,0 +1,69 @@
+using AzureSign.Core.Opc.Tests.TestData;
+using System.Reflection;
+
+namespace AzureSign.Core.Opc.Tests;
+
+///
+/// Provides context for test execution and synthetic test data management.
+///
+public static class TestContext
+{
+ ///
+ /// Gets the directory containing test assets.
+ ///
+ public static string TestAssetsDirectory
+ {
+ get
+ {
+ var assemblyLocation = Assembly.GetExecutingAssembly().Location;
+ var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)!;
+ return Path.Combine(assemblyDirectory, "TestAssets");
+ }
+ }
+
+ ///
+ /// Creates a temporary HLKX file for testing purposes.
+ ///
+ /// The type of HLKX file to create
+ /// Path to the temporary file
+ public static string CreateTempHlkxFile(HlkxTestFileType type = HlkxTestFileType.Minimal)
+ {
+ var tempFile = Path.GetTempFileName();
+
+ switch (type)
+ {
+ case HlkxTestFileType.Minimal:
+ SyntheticHlkxGenerator.CreateMinimalHlkx(tempFile);
+ break;
+ case HlkxTestFileType.PreSigned:
+ SyntheticHlkxGenerator.CreatePreSignedHlkx(tempFile);
+ break;
+ case HlkxTestFileType.Invalid:
+ SyntheticHlkxGenerator.CreateInvalidHlkx(tempFile);
+ break;
+ }
+
+ return tempFile;
+ }
+}
+
+///
+/// Types of synthetic HLKX test files that can be created.
+///
+public enum HlkxTestFileType
+{
+ ///
+ /// A minimal but valid unsigned HLKX file
+ ///
+ Minimal,
+
+ ///
+ /// A pre-signed HLKX file with synthetic signature data
+ ///
+ PreSigned,
+
+ ///
+ /// An invalid HLKX file for error testing
+ ///
+ Invalid
+}
\ No newline at end of file
diff --git a/test/AzureSign.Core.Opc.Tests/TestData/SyntheticHlkxGenerator.cs b/test/AzureSign.Core.Opc.Tests/TestData/SyntheticHlkxGenerator.cs
new file mode 100644
index 0000000..38fdbe3
--- /dev/null
+++ b/test/AzureSign.Core.Opc.Tests/TestData/SyntheticHlkxGenerator.cs
@@ -0,0 +1,294 @@
+using System.IO.Compression;
+using System.Text;
+
+namespace AzureSign.Core.Opc.Tests.TestData;
+
+///
+/// Generates synthetic HLKX files for testing without using actual internal data.
+///
+public static class SyntheticHlkxGenerator
+{
+ ///
+ /// Creates a minimal but valid HLKX structure for testing.
+ ///
+ public static void CreateMinimalHlkx(string filePath)
+ {
+ // Use temporary file approach to avoid ZIP entry length issues
+ var tempFilePath = Path.GetTempFileName();
+ try
+ {
+ using (var fileStream = File.Create(tempFilePath))
+ using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Create))
+ {
+ // Create [Content_Types].xml
+ CreateContentTypesFile(zip);
+
+ // Create _rels/.rels
+ CreateRootRelationshipsFile(zip);
+
+ // Create synthetic HCK data parts
+ CreateSyntheticHckData(zip);
+ }
+
+ // Copy to final destination
+ File.Copy(tempFilePath, filePath, true);
+ }
+ finally
+ {
+ if (File.Exists(tempFilePath))
+ File.Delete(tempFilePath);
+ }
+ }
+
+ ///
+ /// Creates a pre-signed HLKX for testing signature verification.
+ ///
+ public static void CreatePreSignedHlkx(string filePath)
+ {
+ // Use temporary file approach to avoid ZIP entry length issues
+ var tempFilePath = Path.GetTempFileName();
+ try
+ {
+ using (var fileStream = File.Create(tempFilePath))
+ using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Create))
+ {
+ // Create basic structure
+ CreateContentTypesFileWithSignatures(zip);
+ CreateRootRelationshipsFileWithSignatures(zip);
+ CreateSyntheticHckData(zip);
+
+ // Create synthetic signature parts
+ CreateSyntheticSignatureParts(zip);
+ }
+
+ // Copy to final destination
+ File.Copy(tempFilePath, filePath, true);
+ }
+ finally
+ {
+ if (File.Exists(tempFilePath))
+ File.Delete(tempFilePath);
+ }
+ }
+
+ private static void CreateContentTypesFile(ZipArchive zip)
+ {
+ var entry = zip.CreateEntry("[Content_Types].xml");
+ using (var stream = entry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write(@"
+
+
+
+
+
+
+");
+ }
+ }
+
+ private static void CreateContentTypesFileWithSignatures(ZipArchive zip)
+ {
+ var entry = zip.CreateEntry("[Content_Types].xml");
+ using (var stream = entry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write(@"
+
+
+
+
+
+
+
+
+
+");
+ }
+ }
+
+ private static void CreateRootRelationshipsFile(ZipArchive zip)
+ {
+ var entry = zip.CreateEntry("_rels/.rels");
+ using (var stream = entry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write(@"
+
+
+
+
+
+");
+ }
+ }
+
+ private static void CreateRootRelationshipsFileWithSignatures(ZipArchive zip)
+ {
+ var entry = zip.CreateEntry("_rels/.rels");
+ using (var stream = entry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write(@"
+
+
+
+
+
+
+");
+ }
+ }
+
+ private static void CreateSyntheticHckData(ZipArchive zip)
+ {
+ // Create PackageInfo.xml with synthetic hardware certification data
+ var packageInfoEntry = zip.CreateEntry("hck/data/PackageInfo.xml");
+ using (var stream = packageInfoEntry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write(@"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+");
+ }
+
+ // Create synthetic binary data files
+ var dataEntry1 = zip.CreateEntry("hck/data/synthetic-data-1");
+ using (var stream = dataEntry1.Open())
+ {
+ var syntheticData = Encoding.UTF8.GetBytes("This is synthetic test data for stream data testing. It represents hardware certification test results but contains no actual internal data.");
+ stream.Write(syntheticData);
+ }
+
+ var dataEntry2 = zip.CreateEntry("hck/data/synthetic-data-2");
+ using (var stream = dataEntry2.Open())
+ {
+ var syntheticData = Encoding.UTF8.GetBytes("This is synthetic core data for testing. It simulates the structure of hardware lab kit data without containing any real certification information.");
+ stream.Write(syntheticData);
+ }
+
+ // Create synthetic telemetry data
+ var telemetryEntry = zip.CreateEntry("hck/telemetry/SyntheticTelemetry.txt");
+ using (var stream = telemetryEntry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write("Synthetic telemetry data for testing purposes.\nTimestamp: 2024-01-01T12:00:00Z\nTest: SyntheticTest\nResult: Pass");
+ }
+ }
+
+ private static void CreateSyntheticSignatureParts(ZipArchive zip)
+ {
+ // Create origin marker
+ var originEntry = zip.CreateEntry("package/services/digital-signature/origin.psdsor");
+ using (var stream = originEntry.Open())
+ {
+ // Empty file as per OPC spec
+ }
+
+ // Create synthetic XML signature
+ var signatureId = "synthetic12345";
+ var xmlSigEntry = zip.CreateEntry($"package/services/digital-signature/xml-signature/{signatureId}.psdsxs");
+ using (var stream = xmlSigEntry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write($@"
+
+
+
+
+
+
+ SyntheticDigestValueForTesting==
+
+
+ SyntheticSignatureValueForTestingPurposesOnly==
+
+");
+ }
+
+ // Create synthetic certificate
+ var certHash = "SYNTHETICCERTHASH123456789ABCDEF";
+ var certEntry = zip.CreateEntry($"package/services/digital-signature/certificate/{certHash}.cer");
+ using (var stream = certEntry.Open())
+ {
+ // Create a minimal synthetic certificate-like structure
+ var syntheticCert = Encoding.UTF8.GetBytes("SYNTHETIC-CERT-DATA-FOR-TESTING-NOT-A-REAL-CERTIFICATE");
+ stream.Write(syntheticCert);
+ }
+
+ // Create origin relationships
+ var originRelsEntry = zip.CreateEntry("package/services/digital-signature/_rels/origin.psdsor.rels");
+ using (var stream = originRelsEntry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write($@"
+
+
+");
+ }
+
+ // Create signature relationships
+ var sigRelsEntry = zip.CreateEntry($"package/services/digital-signature/xml-signature/_rels/{signatureId}.psdsxs.rels");
+ using (var stream = sigRelsEntry.Open())
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ writer.Write($@"
+
+
+");
+ }
+ }
+
+ ///
+ /// Creates an invalid HLKX file for testing error handling.
+ ///
+ public static void CreateInvalidHlkx(string filePath)
+ {
+ // Create a file that looks like HLKX but is corrupted
+ File.WriteAllText(filePath, "This is not a valid HLKX file - it's just text pretending to be a ZIP archive");
+ }
+}
\ No newline at end of file