├── NuGetLogo.png ├── .editorconfig ├── Resources ├── Certificates │ ├── Cert1.cer │ ├── Cert1.pfx │ ├── Cert2.cer │ ├── Cert2.pfx │ ├── Cert3.cer │ ├── Cert3.pfx │ ├── Cert4.cer │ ├── Cert4.pfx │ ├── Passwords.txt │ ├── WeakCert_Sha1.cer │ ├── WeakCert_Sha1.pfx │ ├── WeakCert_SmallKey.cer │ └── WeakCert_SmallKey.pfx ├── Resources.csproj ├── SolutionAssemblyInfo.cs ├── Nuspec │ └── Cpix.nuspec └── Schema │ ├── xenc-schema.xsd │ └── cpix.xsd ├── Tests ├── AssemblyInfo.cs ├── WeakCertificateTests.cs ├── Tests.csproj ├── RootAttributeTests.cs ├── SmokeTests.cs ├── ContentKeyEncryptionTests.cs ├── TestHelpers.cs └── ContentKeyCrudTests.cs ├── Cpix ├── ContentKeyContextType.cs ├── HlsPlaylistType.cs ├── ContentKeyResolveException.cs ├── InvalidCpixDataException.cs ├── KeyPeriodFilter.cs ├── ContentKeyResolveImpossibleException.cs ├── WeakCertificateException.cs ├── ContentKeyResolveAmbiguityException.cs ├── HlsSignalingData.cs ├── Entity.cs ├── LabelFilter.cs ├── BitrateFilter.cs ├── AudioFilter.cs ├── Recipient.cs ├── ContentKeyPeriodCollection.cs ├── Cpix.csproj ├── ContentKeyPeriod.cs ├── VideoFilter.cs ├── Constants.cs ├── UsageRule.cs ├── ContentKeyContext.cs ├── ContentKey.cs ├── MultiEndianBinaryWriter.cs ├── UsageRuleCollection.cs ├── DrmSystemCollection.cs ├── CryptographyHelpers.cs ├── WidevinePsshData.cs ├── DrmSystem.cs ├── ContentKeyCollection.cs ├── RecipientCollection.cs ├── EntityCollection.cs ├── EntityCollectionBase.cs ├── XmlHelpers.cs └── DrmSignalingHelpers.cs ├── TestVectorGenerator ├── EmptyDocument.cs ├── ITestVector.cs ├── RecipientsWithoutContentKeys.cs ├── ClearContentKeysOnly.cs ├── Invalid_BadDocumentSignature.cs ├── EncryptedContentKeysWithMultipleRecipients.cs ├── EncryptedContentKeys.cs ├── Invalid_BadContentKeysSignature.cs ├── Invalid_WrongMac.cs ├── TestVectorGenerator.csproj ├── UsageRulesBasedOnLabels.cs ├── Program.cs └── Complex.cs ├── License.txt ├── ReadmeQuickStartExamples ├── ReadmeQuickStartExamples.csproj └── Program.cs ├── .gitattributes ├── Cpix.sln ├── .gitignore └── Readme.md /NuGetLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/NuGetLogo.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = tab 5 | indent_style = tab -------------------------------------------------------------------------------- /Resources/Certificates/Cert1.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert1.cer -------------------------------------------------------------------------------- /Resources/Certificates/Cert1.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert1.pfx -------------------------------------------------------------------------------- /Resources/Certificates/Cert2.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert2.cer -------------------------------------------------------------------------------- /Resources/Certificates/Cert2.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert2.pfx -------------------------------------------------------------------------------- /Resources/Certificates/Cert3.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert3.cer -------------------------------------------------------------------------------- /Resources/Certificates/Cert3.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert3.pfx -------------------------------------------------------------------------------- /Resources/Certificates/Cert4.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert4.cer -------------------------------------------------------------------------------- /Resources/Certificates/Cert4.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/Cert4.pfx -------------------------------------------------------------------------------- /Resources/Certificates/Passwords.txt: -------------------------------------------------------------------------------- 1 | Password for PFX file is the file name, without extension, case-sensitive. -------------------------------------------------------------------------------- /Resources/Certificates/WeakCert_Sha1.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/WeakCert_Sha1.cer -------------------------------------------------------------------------------- /Resources/Certificates/WeakCert_Sha1.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/WeakCert_Sha1.pfx -------------------------------------------------------------------------------- /Resources/Certificates/WeakCert_SmallKey.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/WeakCert_SmallKey.cer -------------------------------------------------------------------------------- /Resources/Certificates/WeakCert_SmallKey.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axinom/cpix/HEAD/Resources/Certificates/WeakCert_SmallKey.pfx -------------------------------------------------------------------------------- /Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Axinom.Cpix.TestVectorGenerator")] -------------------------------------------------------------------------------- /Cpix/ContentKeyContextType.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | public enum ContentKeyContextType 4 | { 5 | Video, 6 | Audio 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Cpix/HlsPlaylistType.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | public sealed class HlsPlaylistType 4 | { 5 | public const string Master = "master"; 6 | public const string Media = "media"; 7 | } 8 | } -------------------------------------------------------------------------------- /Resources/Resources.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Axinom.Cpix.Resources 6 | Axinom.Cpix.Resources 7 | 8 | false 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TestVectorGenerator/EmptyDocument.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Axinom.Cpix.TestVectorGenerator 4 | { 5 | sealed class EmptyDocument : ITestVector 6 | { 7 | public string Description => "An empty document. Valid, though rather useless."; 8 | public bool OutputIsValid => true; 9 | 10 | public void Generate(Stream outputStream) 11 | { 12 | new CpixDocument().Save(outputStream); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Resources/SolutionAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // This is the real version number, used in NuGet packages and for display purposes. 5 | [assembly: AssemblyFileVersion("2.7.1")] 6 | 7 | // Only use major version here, with others kept at zero, for correct assembly binding logic. 8 | [assembly: AssemblyVersion("2.0.0")] 9 | 10 | [assembly: InternalsVisibleTo("Axinom.Cpix.Tests")] 11 | [assembly: InternalsVisibleTo("Axinom.Cpix.TestVectorGenerator")] 12 | -------------------------------------------------------------------------------- /TestVectorGenerator/ITestVector.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Axinom.Cpix.TestVectorGenerator 4 | { 5 | /// 6 | /// Marker interface that must be present on all test vectors. 7 | /// 8 | interface ITestVector 9 | { 10 | /// 11 | /// Human-readable description of the test vector, for the readme file. 12 | /// 13 | string Description { get; } 14 | 15 | /// 16 | /// Generates the CPIX document output. 17 | /// 18 | void Generate(Stream outputStream); 19 | 20 | /// 21 | /// Whether the generated output is expected to be a valid CPIX document. 22 | /// 23 | bool OutputIsValid { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Cpix/ContentKeyResolveException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | /// 6 | /// Thrown when it is not possible to resolve a content key for a context key context. 7 | /// 8 | [Serializable] 9 | public class ContentKeyResolveException : Exception 10 | { 11 | public ContentKeyResolveException() { } 12 | public ContentKeyResolveException(string message) : base(message) { } 13 | public ContentKeyResolveException(string message, Exception inner) : base(message, inner) { } 14 | protected ContentKeyResolveException( 15 | System.Runtime.Serialization.SerializationInfo info, 16 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Cpix/InvalidCpixDataException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | /// 6 | /// Thrown when some loaded or supplied data is not suitable for use in a well-formed CPIX document. 7 | /// 8 | [Serializable] 9 | public class InvalidCpixDataException : Exception 10 | { 11 | public InvalidCpixDataException() { } 12 | public InvalidCpixDataException(string message) : base(message) { } 13 | public InvalidCpixDataException(string message, Exception inner) : base(message, inner) { } 14 | protected InvalidCpixDataException( 15 | System.Runtime.Serialization.SerializationInfo info, 16 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TestVectorGenerator/RecipientsWithoutContentKeys.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System.IO; 3 | 4 | namespace Axinom.Cpix.TestVectorGenerator 5 | { 6 | sealed class RecipientsWithoutContentKeys : ITestVector 7 | { 8 | public string Description => "Defines a few authorized recipients (Cert3 and Cert4) and delivery data but no actual content keys to deliver."; 9 | public bool OutputIsValid => true; 10 | 11 | public void Generate(Stream outputStream) 12 | { 13 | var document = new CpixDocument(); 14 | 15 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 16 | document.Recipients.Add(new Recipient(TestHelpers.Certificate4WithPublicKey)); 17 | 18 | document.Save(outputStream); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Cpix/KeyPeriodFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// A key period filter attached to a new content key assignment rule. 5 | /// 6 | public sealed class KeyPeriodFilter 7 | { 8 | /// 9 | /// Gets or sets the ID of the content key period that is associated with 10 | /// this filter. Null is invalid. 11 | /// 12 | public string PeriodId { get; set; } 13 | 14 | /// 15 | /// Validates the data in the object before it is accepted for use by this library. 16 | /// 17 | internal void Validate() 18 | { 19 | if (PeriodId == null) 20 | throw new InvalidCpixDataException("A key period filter that does not reference a content key period is invalid."); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Cpix/ContentKeyResolveImpossibleException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | /// 6 | /// Thrown when resolving content keys is impossible. 7 | /// 8 | [Serializable] 9 | public class ContentKeyResolveImpossibleException : ContentKeyResolveException 10 | { 11 | public ContentKeyResolveImpossibleException() { } 12 | public ContentKeyResolveImpossibleException(string message) : base(message) { } 13 | public ContentKeyResolveImpossibleException(string message, Exception inner) : base(message, inner) { } 14 | protected ContentKeyResolveImpossibleException( 15 | System.Runtime.Serialization.SerializationInfo info, 16 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Cpix/WeakCertificateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security; 3 | 4 | namespace Axinom.Cpix 5 | { 6 | /// 7 | /// Thown when an attempt is made to use a certificate that is associated with weak cryptographic parameters. 8 | /// 9 | [Serializable] 10 | public class WeakCertificateException : SecurityException 11 | { 12 | public WeakCertificateException() { } 13 | public WeakCertificateException(string message) : base(message) { } 14 | public WeakCertificateException(string message, Exception inner) : base(message, inner) { } 15 | protected WeakCertificateException( 16 | System.Runtime.Serialization.SerializationInfo info, 17 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Cpix/ContentKeyResolveAmbiguityException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | /// 6 | /// Thrown when resolving a content key for a content key context results in ambiguity. 7 | /// 8 | [Serializable] 9 | public class ContentKeyResolveAmbiguityException : ContentKeyResolveException 10 | { 11 | public ContentKeyResolveAmbiguityException() { } 12 | public ContentKeyResolveAmbiguityException(string message) : base(message) { } 13 | public ContentKeyResolveAmbiguityException(string message, Exception inner) : base(message, inner) { } 14 | protected ContentKeyResolveAmbiguityException( 15 | System.Runtime.Serialization.SerializationInfo info, 16 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Cpix/HlsSignalingData.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// Represents the DRM system signaling data to be inserted into HLS playlists. 5 | /// 6 | public class HlsSignalingData 7 | { 8 | /// 9 | /// Gets or sets the signaling data to be inserted into the HLS master 10 | /// playlist. The data includes the EXT-X-SESSION-KEY tag along with any 11 | /// proprietary tags. This is UTF-8 text without a byte order mark that may 12 | /// contain multiple lines. 13 | /// 14 | public string MasterPlaylistData; 15 | 16 | /// 17 | /// Gets or sets the signaling data to be inserted into the HLS media 18 | /// playlist. The data includes the EXT-X-KEY tag along with any proprietary 19 | /// tags. This is UTF-8 text without a byte order mark that may contain 20 | /// multiple lines. 21 | /// 22 | public string MediaPlaylistData; 23 | } 24 | } -------------------------------------------------------------------------------- /Cpix/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// Base class for all types of CPIX entities. For internal use only. 5 | /// 6 | /// 7 | /// Acts as an internal interface to the entities, exposing internal general purpose entity features. 8 | /// 9 | public abstract class Entity 10 | { 11 | /// 12 | /// Validates that the current state of the entity is valid for a newly created/added entity. 13 | /// The document is supplied to facilitate cross-checking with other parts of the document. 14 | /// 15 | internal abstract void ValidateNewEntity(CpixDocument document); 16 | 17 | /// 18 | /// Validates that the current state of the entity is valid for a loaded entity. 19 | /// The document is supplied to facilitate cross-checking with other parts of the document. 20 | /// 21 | internal abstract void ValidateLoadedEntity(CpixDocument document); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Cpix/LabelFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// A label filter attached to a new content key assignment rule. 5 | /// 6 | public sealed class LabelFilter 7 | { 8 | /// 9 | /// The label that must exist on a content key context that matches this filter. 10 | /// The meaning of labels is implementation-defined - they are just arbitrary freeform strings. 11 | /// 12 | /// A null value is a syntax error. 13 | /// 14 | public string Label { get; set; } 15 | 16 | public LabelFilter() 17 | { 18 | } 19 | 20 | public LabelFilter(string label) 21 | { 22 | Label = label; 23 | } 24 | 25 | /// 26 | /// Validates the data in the object before it is accepted for use by this library. 27 | /// 28 | internal void Validate() 29 | { 30 | if (Label == null) 31 | throw new InvalidCpixDataException("A label filter that does not reference a label is invalid."); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Cpix/BitrateFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// A read-only view of a bitrate filter attached to a content key assignment rule. 5 | /// 6 | public sealed class BitrateFilter 7 | { 8 | /// 9 | /// The minimum nominal bitrate of the content key context (inclusive). 10 | /// 11 | /// If null, the minimum bitrate is zero. 12 | /// 13 | public long? MinBitrate { get; set; } 14 | 15 | /// 16 | /// The maximum nominal bitrate of the content key context (inclusive). 17 | /// 18 | /// If null, the maximum bitrate is infinity. 19 | /// 20 | public long? MaxBitrate { get; set; } 21 | 22 | /// 23 | /// Validates the data in the object before it is accepted for use by this library. 24 | /// 25 | internal void Validate() 26 | { 27 | if (MinBitrate > MaxBitrate) 28 | throw new InvalidCpixDataException("Bitrate filter minimum bitrate cannot be greater than its maximum bitrate."); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Resources/Nuspec/Cpix.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Axinom.Cpix 5 | Axinom 6 | Library for processing CPIX documents. 7 | https://github.com/Axinom/cpix 8 | https://raw.githubusercontent.com/Axinom/cpix/master/License.txt 9 | https://raw.githubusercontent.com/Axinom/cpix/master/NuGetLogo.png 10 | 11 | 12 | __NUGETPACKAGEVERSION__ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Axinom 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ReadmeQuickStartExamples/ReadmeQuickStartExamples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | Axinom.Cpix.ReadmeQuickStartExamples 7 | Axinom.Cpix.ReadmeQuickStartExamples 8 | 1.0.0 9 | Axinom 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Cpix/AudioFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// An audio filter attached to a new content key assignment rule. 5 | /// Only audio can match this filter - any other type of content key context is never a match. 6 | /// 7 | public sealed class AudioFilter 8 | { 9 | /// 10 | /// The minimum number of channels that must be present in the content key context (inclusive). 11 | /// 12 | /// If null, the minimum channel count is zero. 13 | /// 14 | public int? MinChannels { get; set; } 15 | 16 | /// 17 | /// The maximum number of channels that must be present in the content key context (inclusive). 18 | /// 19 | /// If null, the maximum channel count is infinity. 20 | /// 21 | public int? MaxChannels { get; set; } 22 | 23 | /// 24 | /// Validates the data in the object before it is accepted for use by this library. 25 | /// 26 | internal void Validate() 27 | { 28 | if (MinChannels > MaxChannels) 29 | throw new InvalidCpixDataException("Audio filter minimum channel count cannot be greater than its maximum channel count."); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Cpix/Recipient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography.X509Certificates; 3 | 4 | namespace Axinom.Cpix 5 | { 6 | /// 7 | /// An identity that is authorized to access the content keys of a CPIX document. 8 | /// 9 | public sealed class Recipient : Entity 10 | { 11 | /// 12 | /// A certificate identifying the recipient and the asymmetric key used to secure communications. 13 | /// 14 | public X509Certificate2 Certificate { get; } 15 | 16 | public Recipient(X509Certificate2 certificate) 17 | { 18 | if (certificate == null) 19 | throw new ArgumentNullException(nameof(certificate)); 20 | 21 | Certificate = certificate; 22 | } 23 | 24 | internal override void ValidateNewEntity(CpixDocument document) 25 | { 26 | CryptographyHelpers.ValidateRecipientCertificateAndPublicKey(Certificate); 27 | } 28 | 29 | internal override void ValidateLoadedEntity(CpixDocument document) 30 | { 31 | // We do not particularly care if someone has, with highly questionable intent, used weak certificates 32 | // in their CPIX documented generated via other mechanisms. We won't use them ourselves but we can ignore them here. 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TestVectorGenerator/ClearContentKeysOnly.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Axinom.Cpix.TestVectorGenerator 5 | { 6 | sealed class ClearContentKeysOnly : ITestVector 7 | { 8 | public string Description => "Content keys with values in the clear (without encryption)."; 9 | public bool OutputIsValid => true; 10 | 11 | public void Generate(Stream outputStream) 12 | { 13 | var document = new CpixDocument(); 14 | 15 | document.ContentKeys.Add(new ContentKey 16 | { 17 | Id = new Guid("40d02dd1-61a3-4787-a155-572325d47b80"), 18 | Value = Convert.FromBase64String("gPxt0PMwrHM4TdjwdQmhhQ==") 19 | }); 20 | document.ContentKeys.Add(new ContentKey 21 | { 22 | Id = new Guid("0a30ea4f-539d-4b02-94b2-2b3fba2576d3"), 23 | Value = Convert.FromBase64String("x/gaoS/fDi8BqGNIhkixwQ==") 24 | }); 25 | document.ContentKeys.Add(new ContentKey 26 | { 27 | Id = new Guid("9f7908fa-5d5c-4097-ba53-50edc2235fbc"), 28 | Value = Convert.FromBase64String("3iv9lYwafpe0uEmxDc6PSw==") 29 | }); 30 | document.ContentKeys.Add(new ContentKey 31 | { 32 | Id = new Guid("fac2cbf5-889c-412b-a385-04a29d409bdc"), 33 | Value = Convert.FromBase64String("1OZVZZoYFSU2X/7qT3sHwg==") 34 | }); 35 | 36 | document.Save(outputStream); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Cpix/ContentKeyPeriodCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Xml; 2 | using Axinom.Cpix.Internal; 3 | 4 | namespace Axinom.Cpix 5 | { 6 | sealed class ContentKeyPeriodCollection : EntityCollection 7 | { 8 | public const string ContainerXmlElementName = "ContentKeyPeriodList"; 9 | 10 | public ContentKeyPeriodCollection(CpixDocument document) : base(document) 11 | { 12 | } 13 | 14 | internal override string ContainerName => ContainerXmlElementName; 15 | 16 | protected override XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, ContentKeyPeriod entity) 17 | { 18 | var contentKeyPeriodElement = new ContentKeyPeriodElement 19 | { 20 | Id = entity.Id, 21 | Index = entity.Index, 22 | Start = entity.Start, 23 | End = entity.End 24 | }; 25 | 26 | return XmlHelpers.AppendChildAndReuseNamespaces(contentKeyPeriodElement, container); 27 | } 28 | 29 | protected override ContentKeyPeriod DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces) 30 | { 31 | var contentKeyPeriodElement = XmlHelpers.Deserialize(element); 32 | 33 | var contentKeyPeriod = new ContentKeyPeriod 34 | { 35 | Id = contentKeyPeriodElement.Id, 36 | Index = contentKeyPeriodElement.Index, 37 | Start = contentKeyPeriodElement.Start, 38 | End = contentKeyPeriodElement.End 39 | }; 40 | 41 | return contentKeyPeriod; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /TestVectorGenerator/Invalid_BadDocumentSignature.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix.TestVectorGenerator 8 | { 9 | sealed class Invalid_BadDocumentSignature : ITestVector 10 | { 11 | public string Description => @"The document signature should fail validation because an extra namespace declaration attribute has been added to the document root element."; 12 | public bool OutputIsValid => false; 13 | 14 | public void Generate(Stream outputStream) 15 | { 16 | var document = new CpixDocument(); 17 | 18 | document.ContentKeys.Add(new ContentKey 19 | { 20 | Id = new Guid("bbe06060-1c26-4ed5-8ccd-ee03ddb2ffd9"), 21 | Value = Convert.FromBase64String("aTqer0SFxijnIoQLXGHYJg==") 22 | }); 23 | 24 | document.Recipients.Add(new Recipient(TestHelpers.Certificate1WithPublicKey)); 25 | document.SignedBy = TestHelpers.Certificate2WithPrivateKey; 26 | 27 | var buffer = new MemoryStream(); 28 | document.Save(buffer); 29 | 30 | var xml = new XmlDocument(); 31 | buffer.Position = 0; 32 | xml.Load(buffer); 33 | 34 | XmlHelpers.DeclareNamespace(xml.DocumentElement, "xxx", "http://example.com/this/makes/the/document/signature/invalid"); 35 | 36 | using (var writer = XmlWriter.Create(outputStream, new XmlWriterSettings 37 | { 38 | Encoding = Encoding.UTF8, 39 | CloseOutput = false 40 | })) 41 | { 42 | xml.Save(writer); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Cpix/Cpix.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Axinom.Cpix 6 | Axinom.Cpix 7 | false 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | bin\Release\netstandard2.0\Axinom.Cpix.xml 16 | true 17 | CS1591 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /TestVectorGenerator/EncryptedContentKeysWithMultipleRecipients.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.IO; 4 | 5 | namespace Axinom.Cpix.TestVectorGenerator 6 | { 7 | sealed class EncryptedContentKeysWithMultipleRecipients : ITestVector 8 | { 9 | public string Description => "Content keys encrypted for delivery to four recipients (Cert1 through Cert4)."; 10 | public bool OutputIsValid => true; 11 | 12 | public void Generate(Stream outputStream) 13 | { 14 | var document = new CpixDocument(); 15 | 16 | document.ContentKeys.Add(new ContentKey 17 | { 18 | Id = new Guid("bc365b99-0667-446f-b417-ff0398c9a4c4"), 19 | Value = Convert.FromBase64String("gMMdXMudvuGpYW5k3lzf/g==") 20 | }); 21 | document.ContentKeys.Add(new ContentKey 22 | { 23 | Id = new Guid("1e25f2a7-76a9-4570-bc1a-d8181800d529"), 24 | Value = Convert.FromBase64String("WUxnvQjGw28bA3cgW/1jfg==") 25 | }); 26 | document.ContentKeys.Add(new ContentKey 27 | { 28 | Id = new Guid("2e4e6c21-c0d7-4a1c-80af-bff3d7cc5270"), 29 | Value = Convert.FromBase64String("cCudMIPMQkak1l+oCVXT2A==") 30 | }); 31 | document.ContentKeys.Add(new ContentKey 32 | { 33 | Id = new Guid("8ad35bd4-53ab-437b-8f42-6e1ea8e2f0d8"), 34 | Value = Convert.FromBase64String("zqvOfAja51IUSRV385bvoA==") 35 | }); 36 | 37 | document.Recipients.Add(new Recipient(TestHelpers.Certificate1WithPublicKey)); 38 | document.Recipients.Add(new Recipient(TestHelpers.Certificate2WithPublicKey)); 39 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 40 | document.Recipients.Add(new Recipient(TestHelpers.Certificate4WithPublicKey)); 41 | 42 | document.Save(outputStream); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TestVectorGenerator/EncryptedContentKeys.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace Axinom.Cpix.TestVectorGenerator 8 | { 9 | sealed class EncryptedContentKeys : ITestVector 10 | { 11 | private static readonly Dictionary _contentKeys = new Dictionary 12 | { 13 | { new Guid("bd5adf51-cf04-410f-aac3-ec63a69e929e"), "3rWoHYasQubO6HbJGrGtLw==" }, 14 | { new Guid("d2920429-87ab-41e6-a4c5-a8c836b6312e"), "O5w9FdZiwmQK4uIXzAziaQ==" }, 15 | { new Guid("e17ba4b8-faff-4d30-bcba-7485e3f2e884"), "Cwu/3hSBBRQ7SurBdZD5ow==" }, 16 | { new Guid("0ae6b9ad-92d2-4ebe-882b-1d07dee70715"), "FB6/Eck9Y9SXy6bY8UU/Mw==" }, 17 | }; 18 | 19 | public string Description 20 | { 21 | get 22 | { 23 | var sb = new StringBuilder(); 24 | sb.AppendLine("Content keys encrypted for delivery to a specific recipient (Cert1)."); 25 | sb.AppendLine(); 26 | sb.AppendLine("The decrypted values of the content keys (base64-encoded here) are: "); 27 | sb.AppendLine(); 28 | 29 | foreach (var pair in _contentKeys) 30 | sb.AppendLine($"* {pair.Value} with ID {pair.Key}."); 31 | 32 | return sb.ToString(); 33 | } 34 | } 35 | 36 | public bool OutputIsValid => true; 37 | 38 | public void Generate(Stream outputStream) 39 | { 40 | var document = new CpixDocument(); 41 | 42 | foreach (var pair in _contentKeys) 43 | document.ContentKeys.Add(new ContentKey 44 | { 45 | Id = pair.Key, 46 | Value = Convert.FromBase64String(pair.Value) 47 | }); 48 | 49 | document.Recipients.Add(new Recipient(TestHelpers.Certificate1WithPublicKey)); 50 | 51 | document.Save(outputStream); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Cpix/ContentKeyPeriod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | public sealed class ContentKeyPeriod : Entity 6 | { 7 | /// 8 | /// Gets or sets the ID of the content key period. This must be unique 9 | /// within the scope of this document and the value must be a valid XML 10 | /// ID. 11 | /// 12 | public string Id { get; set; } 13 | 14 | /// 15 | /// Get or sets the numerical index for the key period. Mutually exclusive 16 | /// with start and end. 17 | /// 18 | public int? Index { get; set; } 19 | 20 | /// 21 | /// Gets or sets the wall clock (Live) or media time (VOD) for the start time 22 | /// for the period. Mutually inclusive with end, and mutually exclusive with 23 | /// index. 24 | /// 25 | public DateTimeOffset? Start { get; set; } 26 | 27 | /// 28 | /// Gets or sets the wall clock (Live) or media time (VOD) for the end time 29 | /// for the period. Mutually inclusive with start, and mutually exclusive 30 | /// with index. 31 | /// 32 | public DateTimeOffset? End { get; set; } 33 | 34 | internal override void ValidateNewEntity(CpixDocument document) 35 | { 36 | ValidateEntity(); 37 | } 38 | 39 | internal override void ValidateLoadedEntity(CpixDocument document) 40 | { 41 | ValidateEntity(); 42 | } 43 | 44 | private void ValidateEntity() 45 | { 46 | var invalidIndexStartEndCombination = 47 | (Index == null && (Start == null || End == null)) || 48 | (Index != null && (Start != null || End != null)); 49 | 50 | if (invalidIndexStartEndCombination) 51 | throw new InvalidCpixDataException("A content key period must specify either only the index or both the start and end time."); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /TestVectorGenerator/Invalid_BadContentKeysSignature.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix.TestVectorGenerator 8 | { 9 | sealed class Invalid_BadContentKeysSignature : ITestVector 10 | { 11 | public string Description => @"The signature on the content key collection should fail validation because one of the content key elements was removed after applying the signature."; 12 | public bool OutputIsValid => false; 13 | 14 | public void Generate(Stream outputStream) 15 | { 16 | var document = new CpixDocument(); 17 | 18 | document.ContentKeys.Add(new ContentKey 19 | { 20 | Id = new Guid("c003b21a-fe68-4162-a809-b0add9fe49c1"), 21 | Value = Convert.FromBase64String("fV2AfUA1WnvFaySrl6I7vg==") 22 | }); 23 | document.ContentKeys.Add(new ContentKey 24 | { 25 | Id = new Guid("a3813bc3-986a-462b-84d1-c5d17d3ac0f5"), 26 | Value = Convert.FromBase64String("fs1XSl6sqULnEjX0g6UyBg==") 27 | }); 28 | 29 | document.ContentKeys.AddSignature(TestHelpers.Certificate4WithPrivateKey); 30 | 31 | var buffer = new MemoryStream(); 32 | document.Save(buffer); 33 | 34 | var xml = new XmlDocument(); 35 | buffer.Position = 0; 36 | xml.Load(buffer); 37 | 38 | var namespaces = XmlHelpers.CreateCpixNamespaceManager(xml); 39 | 40 | var firstContentKey = (XmlElement)xml.SelectSingleNode("/cpix:CPIX/cpix:ContentKeyList/cpix:ContentKey", namespaces); 41 | firstContentKey.ParentNode.RemoveChild(firstContentKey); 42 | 43 | using (var writer = XmlWriter.Create(outputStream, new XmlWriterSettings 44 | { 45 | Encoding = Encoding.UTF8, 46 | CloseOutput = false 47 | })) 48 | { 49 | xml.Save(writer); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /TestVectorGenerator/Invalid_WrongMac.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix.TestVectorGenerator 8 | { 9 | sealed class Invalid_WrongMac : ITestVector 10 | { 11 | public string Description => @"The MAC on the encrypted content key is invalid! Expected implementation behavior: 12 | 13 | * Loading should fail when the recipient (Cert1) tries to decrypt the content key. 14 | * Loading should be successful if no attempt is made to decrypt the content key, as the error can only be discovered during decryption."; 15 | public bool OutputIsValid => false; 16 | 17 | public void Generate(Stream outputStream) 18 | { 19 | var document = new CpixDocument(); 20 | 21 | document.ContentKeys.Add(new ContentKey 22 | { 23 | Id = new Guid("5ad2b739-4f46-48df-9a44-aab8c35abf71"), 24 | Value = Convert.FromBase64String("QsCLorCoPAmTd2IHz2pogg==") 25 | }); 26 | 27 | document.Recipients.Add(new Recipient(TestHelpers.Certificate1WithPublicKey)); 28 | 29 | var buffer = new MemoryStream(); 30 | document.Save(buffer); 31 | 32 | var xml = new XmlDocument(); 33 | buffer.Position = 0; 34 | xml.Load(buffer); 35 | 36 | var namespaces = XmlHelpers.CreateCpixNamespaceManager(xml); 37 | var mac = xml.SelectSingleNode("/cpix:CPIX/cpix:ContentKeyList/cpix:ContentKey/cpix:Data/pskc:Secret/pskc:ValueMAC", namespaces); 38 | 39 | // No way this will be the right MAC! 40 | mac.InnerText = "YtijEC7siGSqLg/9WrZ5Z7/TCVE9BydVO9UOv28yZr5+QCdstz8uAQvC9mFFx8hag0LKw461/OKIe5Fr7Mvo2A=="; 41 | 42 | using (var writer = XmlWriter.Create(outputStream, new XmlWriterSettings 43 | { 44 | Encoding = Encoding.UTF8, 45 | CloseOutput = false 46 | })) 47 | { 48 | xml.Save(writer); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/WeakCertificateTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Xunit; 3 | 4 | namespace Axinom.Cpix.Tests 5 | { 6 | /// 7 | /// These tests verify that we do not accept weak certificates. 8 | /// 9 | public sealed class WeakCertificateTests 10 | { 11 | [Fact] 12 | public void SignDocument_WithWeakCertificate_Fails() 13 | { 14 | var document = new CpixDocument(); 15 | 16 | Assert.Throws(() => document.SignedBy = TestHelpers.WeakSha1CertificateWithPrivateKey); 17 | Assert.Throws(() => document.SignedBy = TestHelpers.WeakSmallKeyCertificateWithPrivateKey); 18 | } 19 | 20 | [Fact] 21 | public void SignCollection_WithWeakCertificate_Fails() 22 | { 23 | var document = new CpixDocument(); 24 | 25 | Assert.Throws(() => document.Recipients.AddSignature(TestHelpers.WeakSha1CertificateWithPrivateKey)); 26 | Assert.Throws(() => document.Recipients.AddSignature(TestHelpers.WeakSmallKeyCertificateWithPrivateKey)); 27 | } 28 | 29 | [Fact] 30 | public void AddRecipient_WithWeakCertificate_Fails() 31 | { 32 | var document = new CpixDocument(); 33 | 34 | Assert.Throws(() => document.Recipients.Add(new Recipient(TestHelpers.WeakSha1CertificateWithPublicKey))); 35 | Assert.Throws(() => document.Recipients.Add(new Recipient(TestHelpers.WeakSmallKeyCertificateWithPublicKey))); 36 | } 37 | 38 | [Fact] 39 | public void LoadDocument_WithWeakCertificate_Fails() 40 | { 41 | Assert.Throws(() => CpixDocument.Load(new MemoryStream(), TestHelpers.WeakSha1CertificateWithPrivateKey)); 42 | Assert.Throws(() => CpixDocument.Load(new MemoryStream(), TestHelpers.WeakSmallKeyCertificateWithPrivateKey)); 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TestVectorGenerator/TestVectorGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | Axinom.Cpix.TestVectorGenerator 7 | Axinom.Cpix.TestVectorGenerator 8 | Axinom 9 | Axinom 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Cpix/VideoFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Axinom.Cpix 2 | { 3 | /// 4 | /// A video filter attached to a new content key assignment rule. 5 | /// Only video can match this filter - any other type of content key context is never a match. 6 | /// 7 | public sealed class VideoFilter 8 | { 9 | /// 10 | /// The minimum number of pixels that must be present in the pictures in the content key context (inclusive). 11 | /// 12 | /// If null, the minimum required pixel count is 0. 13 | /// 14 | public long? MinPixels { get; set; } 15 | 16 | /// 17 | /// The maximum number of pixels that must be present in the pictures in the content key context (inclusive). 18 | /// 19 | /// If null, the maximum required pixel count is infinity. 20 | /// 21 | public long? MaxPixels { get; set; } 22 | 23 | /// 24 | /// Whether the filter matches HDR or non-HDR data or both. 25 | /// 26 | /// If null, HDR-ness is ignored. 27 | /// 28 | public bool? HighDynamicRange { get; set; } 29 | 30 | /// 31 | /// Whether the filter matches WCG or non-WCG data or both. 32 | /// 33 | /// If null, WCG-ness is ignored. 34 | /// 35 | public bool? WideColorGamut { get; set; } 36 | 37 | /// 38 | /// The minimum (exclusive) framerate of the content key context. 39 | /// 40 | /// If null, the minimum framerate is 0. 41 | /// 42 | public long? MinFramesPerSecond { get; set; } 43 | 44 | /// 45 | /// The maximum (inclusive) framerate of the content key context. 46 | /// 47 | /// If null, the maximum framerate is infinity. 48 | /// 49 | public long? MaxFramesPerSecond { get; set; } 50 | 51 | /// 52 | /// Validates the data in the object before it is accepted for use by this library. 53 | /// 54 | internal void Validate() 55 | { 56 | if (MinPixels > MaxPixels) 57 | throw new InvalidCpixDataException("Video filter minimum pixel count cannot be greater than its maximum pixel count."); 58 | 59 | if (MinFramesPerSecond > MaxFramesPerSecond) 60 | throw new InvalidCpixDataException("Video filter minimum framerate cannot be greater than its maximum framerate."); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | Axinom.Cpix.Tests 9 | 10 | Axinom.Cpix.Tests 11 | 12 | Axinom 13 | 14 | Axinom 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | PreserveNewest 42 | 43 | 44 | PreserveNewest 45 | 46 | 47 | PreserveNewest 48 | 49 | 50 | PreserveNewest 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Cpix/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axinom.Cpix 4 | { 5 | static class Constants 6 | { 7 | public const string CpixNamespace = "urn:dashif:org:cpix"; 8 | public const string PskcNamespace = "urn:ietf:params:xml:ns:keyprov:pskc"; 9 | public const string XmlnsNamespace = "http://www.w3.org/2000/xmlns/"; 10 | 11 | public const string XmlDigitalSignatureNamespace = "http://www.w3.org/2000/09/xmldsig#"; 12 | public const string XmlEncryptionNamespace = "http://www.w3.org/2001/04/xmlenc#"; 13 | 14 | public const string Aes256CbcAlgorithm = "http://www.w3.org/2001/04/xmlenc#aes256-cbc"; 15 | 16 | public const string HmacSha512Algorithm = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"; 17 | public const string RsaOaepAlgorithm = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"; 18 | 19 | public const int MinimumRsaKeySizeInBits = 3072; 20 | 21 | public static readonly string Sha1Oid = "1.3.14.3.2.29"; 22 | 23 | public const string Sha512Algorithm = "http://www.w3.org/2001/04/xmlenc#sha512"; 24 | 25 | public static readonly int[] ValidContentKeyLengthsInBytes = new[] { 16, 32 }; 26 | public static string ValidContentKeyLengthsHumanReadable => string.Join(", ", ValidContentKeyLengthsInBytes); 27 | 28 | public const int ContentKeyExplicitIvLengthInBytes = 16; 29 | 30 | /// 31 | /// AES-256, so 256-bit key. 32 | /// 33 | public const int DocumentKeyLengthInBytes = 256 / 8; 34 | 35 | /// 36 | /// HMAC-SHA512, so 512-bit key. 37 | /// 38 | public const int MacKeyLengthInBytes = 512 / 8; 39 | 40 | /// 41 | /// The correct order of the entity collection container elements in CPIX XML structure. 42 | /// The schema specifies an ordered sequence and we need to ensure that we always generate 43 | /// the elements in the correct XML document order, regardless of their order in time. 44 | /// 45 | /// Inside the top-level elements, things are simple and life is easy. All we care about is the top layer. 46 | /// 47 | /// Values are name-namespace pairs. 48 | /// 49 | public static readonly Tuple[] TopLevelXmlElementOrder = 50 | { 51 | new Tuple(RecipientCollection.ContainerXmlElementName, CpixNamespace), 52 | new Tuple(ContentKeyCollection.ContainerXmlElementName, CpixNamespace), 53 | new Tuple(DrmSystemCollection.ContainerXmlElementName, CpixNamespace), 54 | new Tuple(ContentKeyPeriodCollection.ContainerXmlElementName, CpixNamespace), 55 | new Tuple(UsageRuleCollection.ContainerXmlElementName, CpixNamespace), 56 | new Tuple("UpdateHistoryItemList", CpixNamespace), 57 | new Tuple("Signature", XmlDigitalSignatureNamespace), 58 | }; 59 | 60 | /// 61 | /// Valid Common Encryption protection scheme identifiers. 62 | /// 63 | public static readonly string[] ValidCommonEncryptionSchemes = { "cenc", "cens", "cbc1", "cbcs" }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Cpix/UsageRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Axinom.Cpix 6 | { 7 | public sealed class UsageRule : Entity 8 | { 9 | /// 10 | /// The ID of the content key that this usage rule applies to. 11 | /// 12 | public Guid KeyId { get; set; } 13 | 14 | /// 15 | /// Specifies the type of media track which corresponds to the streams which 16 | /// match the rules defined in this usage rule. Example values: LowRes, UHD, 17 | /// UHD+HFR, etc. 18 | /// 19 | public string IntendedTrackType { get; set; } 20 | 21 | /// 22 | /// If true for a loaded usage rule, there were filters present in the CPIX document that are not supported 23 | /// by the current implementation. Presence of such filters disables usage rule resolving for the entire document. 24 | /// 25 | public bool ContainsUnsupportedFilters { get; internal set; } 26 | 27 | public ICollection KeyPeriodFilters { get; set; } = new List(); 28 | public ICollection VideoFilters { get; set; } = new List(); 29 | public ICollection AudioFilters { get; set; } = new List(); 30 | public ICollection LabelFilters { get; set; } = new List(); 31 | public ICollection BitrateFilters { get; set; } = new List(); 32 | 33 | internal override void ValidateNewEntity(CpixDocument document) 34 | { 35 | // This can happen if an entity with unsupported filters gets re-added to a document, for some misguided reason. 36 | if (ContainsUnsupportedFilters) 37 | { 38 | throw new InvalidCpixDataException("Cannot add a content key usage rule that contains unsupported filters. " + 39 | "Such usage rules can only be passed through unmodified when processing a CPIX document."); 40 | } 41 | 42 | ValidateLoadedEntity(document); 43 | } 44 | 45 | internal override void ValidateLoadedEntity(CpixDocument document) 46 | { 47 | if (!document.ContentKeys.Any(ck => ck.Id == KeyId)) 48 | throw new InvalidCpixDataException("Content key usage rule references a content key that is not present in the CPIX document."); 49 | 50 | foreach (var keyPeriodFilter in KeyPeriodFilters) 51 | { 52 | keyPeriodFilter.Validate(); 53 | 54 | if (!document.ContentKeyPeriods.Any(ckp => ckp.Id == keyPeriodFilter.PeriodId)) 55 | { 56 | throw new InvalidCpixDataException("Content key usage rule key period filter references a content key period " + 57 | "that is not present in the CPIX document."); 58 | } 59 | } 60 | 61 | foreach (var videoFilter in VideoFilters) 62 | videoFilter.Validate(); 63 | 64 | foreach (var audioFilter in AudioFilters) 65 | audioFilter.Validate(); 66 | 67 | foreach (var labelFilter in LabelFilters) 68 | labelFilter.Validate(); 69 | 70 | foreach (var bitrateFilter in BitrateFilters) 71 | bitrateFilter.Validate(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TestVectorGenerator/UsageRulesBasedOnLabels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace Axinom.Cpix.TestVectorGenerator 6 | { 7 | sealed class UsageRulesBasedOnLabels : ITestVector 8 | { 9 | public string Description => "Usage rules that map content keys using sets of labels."; 10 | public bool OutputIsValid => true; 11 | 12 | public void Generate(Stream outputStream) 13 | { 14 | var document = new CpixDocument(); 15 | 16 | document.ContentKeys.Add(new ContentKey 17 | { 18 | Id = new Guid("ba6c62d6-4a49-4aa4-8869-ce4d2727a2b5"), 19 | Value = Convert.FromBase64String("sLVGDIuvogAUW+Ay0mE9ZA==") 20 | }); 21 | document.ContentKeys.Add(new ContentKey 22 | { 23 | Id = new Guid("37e3de05-9a3b-4c69-8970-63c17a95e0b7"), 24 | Value = Convert.FromBase64String("UvL2JdZiEX2exVMwn796Tg==") 25 | }); 26 | document.ContentKeys.Add(new ContentKey 27 | { 28 | Id = new Guid("53abdba2-f210-43cb-bc90-f18f9a890a02"), 29 | Value = Convert.FromBase64String("lOgzNKBnPZlGSns+WqO8zw==") 30 | }); 31 | document.ContentKeys.Add(new ContentKey 32 | { 33 | Id = new Guid("7ae8e96f-309e-42c3-a510-24023d923373"), 34 | Value = Convert.FromBase64String("K9uQ8+GgwrNx4keBHnI4Xw==") 35 | }); 36 | 37 | document.UsageRules.Add(new UsageRule 38 | { 39 | KeyId = document.ContentKeys.ElementAt(0).Id, 40 | 41 | LabelFilters = new[] 42 | { 43 | new LabelFilter("AllAudioStreams"), 44 | new LabelFilter("Audio"), 45 | new LabelFilter("PositionalAudio"), 46 | new LabelFilter("Stereo"), 47 | } 48 | }); 49 | 50 | // Intentionally add two rules for first key ID, to emphasize that there is no limit of one. 51 | document.UsageRules.Add(new UsageRule 52 | { 53 | KeyId = document.ContentKeys.ElementAt(0).Id, 54 | 55 | LabelFilters = new[] 56 | { 57 | new LabelFilter("Commentary"), 58 | new LabelFilter("Telephony"), 59 | new LabelFilter("Speech"), 60 | } 61 | }); 62 | 63 | document.UsageRules.Add(new UsageRule 64 | { 65 | KeyId = document.ContentKeys.ElementAt(1).Id, 66 | 67 | LabelFilters = new[] 68 | { 69 | new LabelFilter("SD-Video"), 70 | } 71 | }); 72 | 73 | document.UsageRules.Add(new UsageRule 74 | { 75 | KeyId = document.ContentKeys.ElementAt(2).Id, 76 | 77 | LabelFilters = new[] 78 | { 79 | new LabelFilter("HD-Video"), 80 | } 81 | }); 82 | 83 | document.UsageRules.Add(new UsageRule 84 | { 85 | KeyId = document.ContentKeys.ElementAt(3).Id, 86 | 87 | LabelFilters = new[] 88 | { 89 | new LabelFilter("UHD-Video"), 90 | new LabelFilter("3D-Video"), 91 | new LabelFilter("WCG-Video"), 92 | new LabelFilter("HDR-Video"), 93 | } 94 | }); 95 | 96 | // Intentionally no rule for last key ID, to emphasize that not all keys need to have usage rules. 97 | 98 | document.Save(outputStream); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Cpix.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2018 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cpix", "Cpix\Cpix.csproj", "{8D2CE324-03EF-4F95-B0BE-1FAE294E8A3C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{F000D760-5E18-44D6-A90D-135B4203BB19}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resources", "Resources\Resources.csproj", "{76BA027A-9142-4D60-9AFF-421A16247513}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{34101AD0-9229-4F10-B951-B07082F850D2}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadmeQuickStartExamples", "ReadmeQuickStartExamples\ReadmeQuickStartExamples.csproj", "{EBBAF2D8-F96F-4744-A5A3-E4FADF06855E}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestVectorGenerator", "TestVectorGenerator\TestVectorGenerator.csproj", "{727197E7-8742-40ED-B8D1-96659BFD7C65}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {8D2CE324-03EF-4F95-B0BE-1FAE294E8A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {8D2CE324-03EF-4F95-B0BE-1FAE294E8A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {8D2CE324-03EF-4F95-B0BE-1FAE294E8A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {8D2CE324-03EF-4F95-B0BE-1FAE294E8A3C}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {F000D760-5E18-44D6-A90D-135B4203BB19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {F000D760-5E18-44D6-A90D-135B4203BB19}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {F000D760-5E18-44D6-A90D-135B4203BB19}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {F000D760-5E18-44D6-A90D-135B4203BB19}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {76BA027A-9142-4D60-9AFF-421A16247513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {76BA027A-9142-4D60-9AFF-421A16247513}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {76BA027A-9142-4D60-9AFF-421A16247513}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {76BA027A-9142-4D60-9AFF-421A16247513}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {EBBAF2D8-F96F-4744-A5A3-E4FADF06855E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {EBBAF2D8-F96F-4744-A5A3-E4FADF06855E}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {EBBAF2D8-F96F-4744-A5A3-E4FADF06855E}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {EBBAF2D8-F96F-4744-A5A3-E4FADF06855E}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {727197E7-8742-40ED-B8D1-96659BFD7C65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {727197E7-8742-40ED-B8D1-96659BFD7C65}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {727197E7-8742-40ED-B8D1-96659BFD7C65}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {727197E7-8742-40ED-B8D1-96659BFD7C65}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {62BEEF78-DB80-4851-AC4F-D90F29A3A5D2} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /Cpix/ContentKeyContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Axinom.Cpix 5 | { 6 | /// 7 | /// Defines the context key context that content keys can be resolved for, as defined by usage rules. 8 | /// 9 | public sealed class ContentKeyContext 10 | { 11 | /// 12 | /// Type of the data represented by the context. 13 | /// If null, the context will not match any filters that require a specific type of data. 14 | /// 15 | public ContentKeyContextType? Type { get; set; } 16 | 17 | /// 18 | /// Nominal bitrate. 19 | /// If null, the context will not match any filters that require a specific bitrate. 20 | /// 21 | public long? Bitrate { get; set; } 22 | 23 | /// 24 | /// Number of audio channels. Only valid if Type == Audio. 25 | /// If null, the context will not match any filters that require a specific audio channel count. 26 | /// 27 | public int? AudioChannelCount { get; set; } 28 | 29 | /// 30 | /// Number of pixels present in the encoded pictures. Only valid if Type == Video. 31 | /// If null, the context will not match any filters that require a specific picture pixel count. 32 | /// 33 | public long? PicturePixelCount { get; set; } 34 | 35 | /// 36 | /// Nominal video frames per second. Only valid if Type == Video. 37 | /// If null, the context will not match any filters that require a specific value; 38 | /// 39 | public long? VideoFramesPerSecond { get; set; } 40 | 41 | /// 42 | /// Whether the picture uses WCG. Only valid if Type == Video. 43 | /// If null, the context will not match any filters that require a specific value; 44 | /// 45 | public bool? WideColorGamut { get; set; } 46 | 47 | /// 48 | /// Whether the picture uses HDR. Only valid if Type == Video. 49 | /// If null, the context will not match any filters that require a specific value; 50 | /// 51 | public bool? HighDynamicRange { get; set; } 52 | 53 | /// 54 | /// All the labels associated with the content key context. 55 | /// If null, the context will not match any filters that require a specific label. 56 | /// 57 | public IReadOnlyCollection Labels { get; set; } 58 | 59 | /// 60 | /// Validates the content key context before use. 61 | /// 62 | internal void Validate() 63 | { 64 | if (AudioChannelCount != null && Type != ContentKeyContextType.Audio) 65 | throw new ArgumentException("Content key context cannot define audio channel count unless Type == Audio."); 66 | 67 | if (PicturePixelCount != null && Type != ContentKeyContextType.Video) 68 | throw new ArgumentException("Content key context cannot define picture pixel count unless Type == Video."); 69 | 70 | if (VideoFramesPerSecond != null && Type != ContentKeyContextType.Video) 71 | throw new ArgumentException("Content key context cannot define VideoFramesPerSecond unless Type == Video."); 72 | 73 | if (WideColorGamut != null && Type != ContentKeyContextType.Video) 74 | throw new ArgumentException("Content key context cannot define WideColorGamut unless Type == Video."); 75 | 76 | if (HighDynamicRange != null && Type != ContentKeyContextType.Video) 77 | throw new ArgumentException("Content key context cannot define HighDynamicRange unless Type == Video."); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Cpix/ContentKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Axinom.Cpix 5 | { 6 | public sealed class ContentKey : Entity 7 | { 8 | /// 9 | /// Unique ID of the content key. 10 | /// 11 | public Guid Id { get; set; } 12 | 13 | /// 14 | /// Gets or sets the value of the content key. Must be 128 bits (16 bytes) long. 15 | /// Null if the content key was loaded from a CPIX document and could not be decrypted. 16 | /// 17 | public byte[] Value { get; set; } 18 | 19 | /// 20 | /// Get or sets the optional IV that is to be explicitly associated with the 21 | /// content key. The IV must be 128 bits (16 bytes) long. A common use case 22 | /// involves FairPlay DRM, where the IV is expected to be transported 23 | /// together with the content key, instead of being extracted from the 24 | /// content. Otherwise, the use of this IV is not recommended and it should 25 | /// be ignored even if set. 26 | /// 27 | public byte[] ExplicitIv { get; set; } 28 | 29 | /// 30 | /// Gets or sets the Common Encryption protection scheme that the content key 31 | /// is intended to be used with. When set, the value shall be a 4-character 32 | /// protection scheme name, one of "cenc", "cens", "cbc1", "cbcs". If omitted, 33 | /// then content may be encrypted using any Common Encryption protection scheme. 34 | /// 35 | public string CommonEncryptionScheme { get; set; } 36 | 37 | /// 38 | /// Gets whether the content key is a loaded encrypted content key. 39 | /// 40 | internal bool IsLoadedEncryptedKey { get; set; } 41 | 42 | internal override void ValidateLoadedEntity(CpixDocument document) 43 | { 44 | ValidateEntity(document); 45 | 46 | // We skip length check if we do not have a value for an encrypted key (it will be read-only). 47 | if (IsLoadedEncryptedKey && Value != null) 48 | ValidateContentKeyValueAndSize(document); 49 | } 50 | 51 | internal override void ValidateNewEntity(CpixDocument document) 52 | { 53 | ValidateEntity(document); 54 | 55 | ValidateContentKeyValueAndSize(document); 56 | } 57 | 58 | private void ValidateEntity(CpixDocument document) 59 | { 60 | if (Id == Guid.Empty) 61 | throw new InvalidCpixDataException("A unique key ID must be provided for each content key."); 62 | 63 | if (ExplicitIv != null && ExplicitIv.Length != Constants.ContentKeyExplicitIvLengthInBytes) 64 | throw new InvalidCpixDataException($"The explicit IVs associated with content keys must be {Constants.ContentKeyExplicitIvLengthInBytes} byte long."); 65 | 66 | if (CommonEncryptionScheme != null && !Constants.ValidCommonEncryptionSchemes.Contains(CommonEncryptionScheme)) 67 | { 68 | throw new InvalidCpixDataException( 69 | $"The Common Encryption protection scheme associated with content keys must be one of" + 70 | $"{string.Join(", ", Constants.ValidCommonEncryptionSchemes.Select(x => $"'{x}'"))}."); 71 | } 72 | } 73 | 74 | private void ValidateContentKeyValueAndSize(CpixDocument document) 75 | { 76 | // We support content keys without a value because many packagers 77 | // create such documents for requesting keys from key services, 78 | // which then fill in the value. 79 | if (Value != null && !Constants.ValidContentKeyLengthsInBytes.Contains(Value.Length)) 80 | throw new InvalidCpixDataException($"A content key must have a key value with a byte-size from the set: {Constants.ValidContentKeyLengthsHumanReadable}."); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Cpix/MultiEndianBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Axinom.Cpix 7 | { 8 | public enum ByteOrder 9 | { 10 | BigEndian, 11 | LittleEndian 12 | } 13 | 14 | /// 15 | /// Binary writer with variable byte endianness. 16 | /// Note that text writing does not support big-endian byte order. This is for binary only. 17 | /// 18 | public class MultiEndianBinaryWriter : BinaryWriter 19 | { 20 | /// 21 | /// Gets or sets the byte order the writer uses for its operations. 22 | /// 23 | public ByteOrder ByteOrder { get; set; } 24 | 25 | public override void Write(double value) 26 | { 27 | if (ByteOrder == ByteOrder.LittleEndian) 28 | { 29 | base.Write(value); 30 | return; 31 | } 32 | 33 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 34 | } 35 | 36 | public override void Write(short value) 37 | { 38 | if (ByteOrder == ByteOrder.LittleEndian) 39 | { 40 | base.Write(value); 41 | return; 42 | } 43 | 44 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 45 | } 46 | 47 | public override void Write(ushort value) 48 | { 49 | if (ByteOrder == ByteOrder.LittleEndian) 50 | { 51 | base.Write(value); 52 | return; 53 | } 54 | 55 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 56 | } 57 | 58 | public override void Write(int value) 59 | { 60 | if (ByteOrder == ByteOrder.LittleEndian) 61 | { 62 | base.Write(value); 63 | return; 64 | } 65 | 66 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 67 | } 68 | 69 | public override void Write(uint value) 70 | { 71 | if (ByteOrder == ByteOrder.LittleEndian) 72 | { 73 | base.Write(value); 74 | return; 75 | } 76 | 77 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 78 | } 79 | 80 | public override void Write(long value) 81 | { 82 | if (ByteOrder == ByteOrder.LittleEndian) 83 | { 84 | base.Write(value); 85 | return; 86 | } 87 | 88 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 89 | } 90 | 91 | public override void Write(ulong value) 92 | { 93 | if (ByteOrder == ByteOrder.LittleEndian) 94 | { 95 | base.Write(value); 96 | return; 97 | } 98 | 99 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 100 | } 101 | 102 | public override void Write(float value) 103 | { 104 | if (ByteOrder == ByteOrder.LittleEndian) 105 | { 106 | base.Write(value); 107 | return; 108 | } 109 | 110 | Write(BitConverter.GetBytes(value).Reverse().ToArray()); 111 | } 112 | 113 | #region Initialization 114 | public MultiEndianBinaryWriter(Stream output, ByteOrder byteOrder) 115 | : base(output) 116 | { 117 | if (!BitConverter.IsLittleEndian) 118 | throw new InvalidOperationException("This class is only designed for little endian machines and will almost certainly not function correctly on big endian machines."); 119 | 120 | ByteOrder = byteOrder; 121 | } 122 | 123 | public MultiEndianBinaryWriter(Stream output, Encoding encoding, ByteOrder byteOrder) 124 | : base(output, encoding) 125 | { 126 | if (!BitConverter.IsLittleEndian) 127 | throw new InvalidOperationException("This class is only designed for little endian machines and will almost certainly not function correctly on big endian machines."); 128 | 129 | ByteOrder = byteOrder; 130 | } 131 | #endregion 132 | } 133 | } -------------------------------------------------------------------------------- /Tests/RootAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Xml; 4 | using Axinom.Cpix.Internal; 5 | using Xunit; 6 | 7 | namespace Axinom.Cpix.Tests 8 | { 9 | public sealed class RootAttributeTests 10 | { 11 | [Fact] 12 | public void ContentId_ByDefault_IsNull() 13 | { 14 | var document = new CpixDocument(); 15 | Assert.Null(document.ContentId); 16 | } 17 | 18 | [Fact] 19 | public void ContentId_WhenNull_IsNotSerialized() 20 | { 21 | var document = new CpixDocument(); 22 | document.ContentId = null; 23 | 24 | var buffer = new MemoryStream(); 25 | document.Save(buffer); 26 | buffer.Position = 0; 27 | 28 | var xmlDocument = new XmlDocument(); 29 | xmlDocument.Load(buffer); 30 | 31 | var contentIdAttribute = xmlDocument.DocumentElement.GetAttributeNode(DocumentRootElement.ContentIdAttributeName); 32 | 33 | Assert.Null(contentIdAttribute); 34 | } 35 | 36 | [Theory] 37 | [InlineData(null)] 38 | [InlineData("")] 39 | [InlineData("hello")] 40 | public void ContentId_WhenSetToVariousValues_SurvivesRoundtrip(string expectedContentId) 41 | { 42 | var document = new CpixDocument(); 43 | document.ContentId = expectedContentId; 44 | document = TestHelpers.Reload(document); 45 | 46 | Assert.NotNull(document.ContentKeys); 47 | Assert.Equal(expectedContentId, document.ContentId); 48 | } 49 | 50 | [Fact] 51 | public void ContentId_WhenSetWhileDocumentIsReadOnly_ThrowsInvalidOperationException() 52 | { 53 | var document = new CpixDocument(); 54 | document.SignedBy = TestHelpers.Certificate1WithPrivateKey; 55 | document = TestHelpers.Reload(document); 56 | 57 | Assert.Throws(() => document.ContentId = "fail"); 58 | } 59 | 60 | [Fact] 61 | public void Version_ByDefault_IsNull() 62 | { 63 | var document = new CpixDocument(); 64 | Assert.Null(document.Version); 65 | } 66 | 67 | [Fact] 68 | public void Version_WhenNull_IsNotSerialized() 69 | { 70 | var document = new CpixDocument(); 71 | document.Version = null; 72 | 73 | var buffer = new MemoryStream(); 74 | document.Save(buffer); 75 | buffer.Position = 0; 76 | 77 | var xmlDocument = new XmlDocument(); 78 | xmlDocument.Load(buffer); 79 | 80 | var versionAttribute = xmlDocument.DocumentElement.GetAttributeNode(DocumentRootElement.VersionAttributeName); 81 | 82 | Assert.Null(versionAttribute); 83 | } 84 | 85 | [Theory] 86 | [InlineData(null)] 87 | [InlineData("2.3")] 88 | [InlineData("20.30")] 89 | public void Version_WhenSetToVariousValues_SurvivesRoundtrip(string expectedVersion) 90 | { 91 | var document = new CpixDocument(); 92 | document.Version = expectedVersion; 93 | document = TestHelpers.Reload(document); 94 | 95 | Assert.NotNull(document.ContentKeys); 96 | Assert.Equal(expectedVersion, document.Version); 97 | } 98 | 99 | [Fact] 100 | public void Version_WhenSetWhileDocumentIsReadOnly_ThrowsInvalidOperationException() 101 | { 102 | var document = new CpixDocument(); 103 | document.SignedBy = TestHelpers.Certificate1WithPrivateKey; 104 | document = TestHelpers.Reload(document); 105 | 106 | Assert.Throws(() => document.Version = "2.3"); 107 | } 108 | 109 | [Theory] 110 | [InlineData("")] 111 | [InlineData("abcd")] 112 | [InlineData("2.2")] 113 | public void Version_WhenSetToVariousInvalidValues_ThrowsInvalidCpixDataException(string invalidVersion) 114 | { 115 | var document = new CpixDocument(); 116 | 117 | Assert.Throws(() => document.Version = invalidVersion); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /TestVectorGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace Axinom.Cpix.TestVectorGenerator 9 | { 10 | class Program 11 | { 12 | const string OutputDirectoryName = "Output"; 13 | 14 | static void Main(string[] args) 15 | { 16 | var workingDirectory = Path.Combine(Environment.CurrentDirectory, OutputDirectoryName); 17 | 18 | if (Directory.Exists(workingDirectory)) 19 | Directory.Delete(workingDirectory, true); 20 | 21 | Directory.CreateDirectory(workingDirectory); 22 | 23 | var implementationTypes = Assembly.GetExecutingAssembly().GetTypes() 24 | .Where(t => typeof(ITestVector).IsAssignableFrom(t)) 25 | .Where(t => t.IsClass && !t.IsAbstract && t.GetConstructor(Type.EmptyTypes) != null) 26 | .ToArray(); 27 | 28 | Console.WriteLine($"Found {implementationTypes.Length} test vector implementations."); 29 | 30 | using (var readme = new StreamWriter(Path.Combine(workingDirectory, "Readme.md"))) 31 | { 32 | readme.WriteLine($@"CPIX Test Vectors 33 | ================= 34 | 35 | Generated {DateTimeOffset.UtcNow.ToString("yyyy-MM-dd")} using [Axinom.Cpix](https://github.com/Axinom/cpix) v{FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion} 36 | 37 | Included test certificates generated as follows: `makecert.exe -pe -n ""CN=CPIX Example Entity 1"" -sky exchange -a sha512 -len 4096 -r -ss My`. The password for any included PFX files is the filename, without extension, case-sensitive. 38 | 39 | Test vector descriptions follow below. 40 | "); 41 | 42 | foreach (var implementationType in implementationTypes) 43 | { 44 | var name = implementationType.Name; 45 | var instance = (ITestVector)Activator.CreateInstance(implementationType); 46 | 47 | Console.WriteLine("Generating: " + name); 48 | 49 | readme.WriteLine(name); 50 | readme.WriteLine(new string('=', name.Length)); 51 | readme.WriteLine(); 52 | 53 | if (!instance.OutputIsValid) 54 | { 55 | readme.WriteLine("NB! This test vector intentionally contains invalid data!"); 56 | readme.WriteLine(); 57 | } 58 | 59 | readme.WriteLine(instance.Description); 60 | readme.WriteLine(); 61 | 62 | using (var file = File.Create(Path.Combine(workingDirectory, name + ".xml"))) 63 | { 64 | instance.Generate(file); 65 | 66 | if (file.Length == 0) 67 | throw new Exception("Test vector implementation failed to generate output."); 68 | 69 | if (instance.OutputIsValid != IsValidCpix(file)) 70 | throw new Exception("CPIX validity and our expectation do not match!"); 71 | } 72 | } 73 | } 74 | 75 | Console.WriteLine("Copying certificate files to output directory."); 76 | 77 | foreach (var pfx in Directory.GetFiles(Environment.CurrentDirectory, "*.pfx")) 78 | File.Copy(pfx, Path.Combine(workingDirectory, Path.GetFileName(pfx))); 79 | 80 | foreach (var cer in Directory.GetFiles(Environment.CurrentDirectory, "*.cer")) 81 | File.Copy(cer, Path.Combine(workingDirectory, Path.GetFileName(cer))); 82 | 83 | Console.WriteLine("All done. Generated output is at " + workingDirectory); 84 | } 85 | 86 | static bool IsValidCpix(Stream stream) 87 | { 88 | stream.Position = 0; 89 | 90 | try 91 | { 92 | CpixDocument.Load(stream, new[] 93 | { 94 | TestHelpers.Certificate1WithPrivateKey, 95 | TestHelpers.Certificate2WithPrivateKey, 96 | TestHelpers.Certificate3WithPrivateKey, 97 | TestHelpers.Certificate4WithPrivateKey, 98 | }); 99 | 100 | return true; 101 | } 102 | catch 103 | { 104 | return false; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Cpix/UsageRuleCollection.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Internal; 2 | using System.Linq; 3 | using System.Xml; 4 | 5 | namespace Axinom.Cpix 6 | { 7 | sealed class UsageRuleCollection : EntityCollection 8 | { 9 | public const string ContainerXmlElementName = "ContentKeyUsageRuleList"; 10 | 11 | public UsageRuleCollection(CpixDocument document) : base(document) 12 | { 13 | } 14 | 15 | internal override string ContainerName => ContainerXmlElementName; 16 | 17 | protected override XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, UsageRule entity) 18 | { 19 | var element = new UsageRuleElement 20 | { 21 | KeyId = entity.KeyId, 22 | IntendedTrackType = entity.IntendedTrackType 23 | }; 24 | 25 | if (entity.VideoFilters?.Count > 0) 26 | { 27 | element.VideoFilters = entity.VideoFilters 28 | .Select(f => new VideoFilterElement 29 | { 30 | MinPixels = f.MinPixels, 31 | MaxPixels = f.MaxPixels, 32 | MinFps = f.MinFramesPerSecond, 33 | MaxFps = f.MaxFramesPerSecond, 34 | Wcg = f.WideColorGamut, 35 | Hdr = f.HighDynamicRange 36 | }) 37 | .ToArray(); 38 | } 39 | 40 | if (entity.AudioFilters?.Count > 0) 41 | { 42 | element.AudioFilters = entity.AudioFilters 43 | .Select(f => new AudioFilterElement 44 | { 45 | MinChannels = f.MinChannels, 46 | MaxChannels = f.MaxChannels 47 | }) 48 | .ToArray(); 49 | } 50 | 51 | if (entity.BitrateFilters?.Count > 0) 52 | { 53 | element.BitrateFilters = entity.BitrateFilters 54 | .Select(f => new BitrateFilterElement 55 | { 56 | MinBitrate = f.MinBitrate, 57 | MaxBitrate = f.MaxBitrate 58 | }) 59 | .ToArray(); 60 | } 61 | 62 | if (entity.LabelFilters?.Count > 0) 63 | { 64 | element.LabelFilters = entity.LabelFilters 65 | .Select(f => new LabelFilterElement 66 | { 67 | Label = f.Label 68 | }) 69 | .ToArray(); 70 | } 71 | 72 | if (entity.KeyPeriodFilters?.Count > 0) 73 | { 74 | element.KeyPeriodFilters = entity.KeyPeriodFilters 75 | .Select(f => new KeyPeriodElement() 76 | { 77 | PeriodId = f.PeriodId 78 | }) 79 | .ToArray(); 80 | } 81 | 82 | return XmlHelpers.AppendChildAndReuseNamespaces(element, container); 83 | } 84 | 85 | protected override UsageRule DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces) 86 | { 87 | var raw = XmlHelpers.Deserialize(element); 88 | raw.LoadTimeValidate(); 89 | 90 | var rule = new UsageRule 91 | { 92 | KeyId = raw.KeyId, 93 | IntendedTrackType = raw.IntendedTrackType 94 | }; 95 | 96 | // This disables all usage rule processing, basically, and treats this particular rule as read-only. 97 | // The unknown filters will be preserved unless the rule is removed, just no rules from this document can be used. 98 | if (raw.UnknownFilters?.Any() == true) 99 | rule.ContainsUnsupportedFilters = true; 100 | 101 | if (raw.VideoFilters?.Length > 0) 102 | { 103 | rule.VideoFilters = raw.VideoFilters 104 | .Select(f => new VideoFilter 105 | { 106 | MinPixels = f.MinPixels, 107 | MaxPixels = f.MaxPixels, 108 | MinFramesPerSecond = f.MinFps, 109 | MaxFramesPerSecond = f.MaxFps, 110 | WideColorGamut = f.Wcg, 111 | HighDynamicRange = f.Hdr 112 | }) 113 | .ToList(); 114 | } 115 | 116 | if (raw.AudioFilters?.Length > 0) 117 | { 118 | rule.AudioFilters = raw.AudioFilters 119 | .Select(f => new AudioFilter 120 | { 121 | MinChannels = f.MinChannels, 122 | MaxChannels = f.MaxChannels 123 | }) 124 | .ToList(); 125 | } 126 | 127 | if (raw.BitrateFilters?.Length > 0) 128 | { 129 | rule.BitrateFilters = raw.BitrateFilters 130 | .Select(f => new BitrateFilter 131 | { 132 | MinBitrate = f.MinBitrate, 133 | MaxBitrate = f.MaxBitrate 134 | }) 135 | .ToList(); 136 | } 137 | 138 | if (raw.LabelFilters?.Length > 0) 139 | { 140 | rule.LabelFilters = raw.LabelFilters 141 | .Select(f => new LabelFilter 142 | { 143 | Label = f.Label 144 | }) 145 | .ToList(); 146 | } 147 | 148 | if (raw.KeyPeriodFilters?.Length > 0) 149 | { 150 | rule.KeyPeriodFilters = raw.KeyPeriodFilters 151 | .Select(f => new KeyPeriodFilter 152 | { 153 | PeriodId = f.PeriodId 154 | }) 155 | .ToList(); 156 | } 157 | 158 | return rule; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Tests/SmokeTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace Axinom.Cpix.Tests 6 | { 7 | public sealed class SmokeTests 8 | { 9 | [Fact] 10 | public void NewDocument_CreatesEmptyDocument() 11 | { 12 | var document = new CpixDocument(); 13 | 14 | Assert.True(document.ContentKeysAreReadable); 15 | Assert.False(document.IsReadOnly); 16 | Assert.Null(document.SignedBy); 17 | Assert.Empty(document.Recipients); 18 | Assert.Empty(document.ContentKeys); 19 | Assert.Empty(document.UsageRules); 20 | Assert.Null(document.ContentId); 21 | Assert.Null(document.Version); 22 | } 23 | 24 | [Fact] 25 | public void SaveAndLoad_EmptyDocument_Succeeds() 26 | { 27 | var document = new CpixDocument(); 28 | 29 | document = TestHelpers.Reload(document); 30 | 31 | Assert.True(document.ContentKeysAreReadable); 32 | Assert.False(document.IsReadOnly); 33 | Assert.Null(document.SignedBy); 34 | Assert.Empty(document.Recipients); 35 | Assert.Empty(document.ContentKeys); 36 | Assert.Empty(document.UsageRules); 37 | Assert.Null(document.ContentId); 38 | Assert.Null(document.Version); 39 | } 40 | 41 | [Fact] 42 | public void Save_OneClearKey_DoesNotHorriblyFail() 43 | { 44 | var keyData = TestHelpers.GenerateKeyData(); 45 | 46 | var document = new CpixDocument(); 47 | document.ContentKeys.Add(new ContentKey 48 | { 49 | Id = keyData.Item1, 50 | Value = keyData.Item2 51 | }); 52 | 53 | using (var buffer = new MemoryStream()) 54 | { 55 | document.Save(buffer); 56 | 57 | // Something got saved and there was no exception. Good enough! 58 | Assert.NotEqual(0, buffer.Length); 59 | } 60 | } 61 | 62 | [Fact] 63 | public void Save_OneEncryptedKey_DoesNotHorriblyFail() 64 | { 65 | var keyData = TestHelpers.GenerateKeyData(); 66 | 67 | var document = new CpixDocument(); 68 | document.ContentKeys.Add(new ContentKey 69 | { 70 | Id = keyData.Item1, 71 | Value = keyData.Item2 72 | }); 73 | 74 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 75 | 76 | using (var buffer = new MemoryStream()) 77 | { 78 | document.Save(buffer); 79 | 80 | // Something got saved and there was no exception. Good enough! 81 | Assert.NotEqual(0, buffer.Length); 82 | } 83 | } 84 | 85 | [Fact] 86 | public void Save_DoesNotAddBomToStream() 87 | { 88 | var expectedFirstFiveBytes = new [] { (byte)'<', (byte)'?', (byte)'x', (byte)'m', (byte)'l' } ; 89 | 90 | var document = new CpixDocument(); 91 | 92 | using (var buffer = new MemoryStream()) 93 | { 94 | document.Save(buffer); 95 | 96 | buffer.Position = 0; 97 | var firstFiveBytes = new byte[5]; 98 | buffer.Read(firstFiveBytes, 0, 5); 99 | 100 | Assert.Equal(expectedFirstFiveBytes, firstFiveBytes); 101 | } 102 | } 103 | 104 | [Fact] 105 | public void RoundTrip_WithOneClearKey_LoadsExpectedKey() 106 | { 107 | var keyData = TestHelpers.GenerateKeyData(); 108 | 109 | var document = new CpixDocument(); 110 | document.ContentKeys.Add(new ContentKey 111 | { 112 | Id = keyData.Item1, 113 | Value = keyData.Item2 114 | }); 115 | 116 | document = TestHelpers.Reload(document); 117 | 118 | Assert.NotNull(document.ContentKeys); 119 | Assert.Single(document.ContentKeys); 120 | 121 | var key = document.ContentKeys.Single(); 122 | Assert.Equal(keyData.Item1, key.Id); 123 | Assert.Equal(keyData.Item2, key.Value); 124 | } 125 | 126 | [Fact] 127 | public void RoundTrip_WithOneKeyEncryptedAndSigned_LoadsExpectedKeyAndDetectsIdentities() 128 | { 129 | var keyData = TestHelpers.GenerateKeyData(); 130 | 131 | var document = new CpixDocument(); 132 | document.ContentKeys.Add(new ContentKey 133 | { 134 | Id = keyData.Item1, 135 | Value = keyData.Item2 136 | }); 137 | 138 | document.ContentKeys.AddSignature(TestHelpers.Certificate1WithPrivateKey); 139 | document.SignedBy = TestHelpers.Certificate1WithPrivateKey; 140 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 141 | 142 | document = TestHelpers.Reload(document, new[] { TestHelpers.Certificate3WithPrivateKey }); 143 | 144 | Assert.Single(document.ContentKeys); 145 | Assert.NotNull(document.SignedBy); 146 | Assert.Single(document.ContentKeys.SignedBy); 147 | Assert.Single(document.Recipients); 148 | 149 | Assert.Equal(TestHelpers.Certificate1WithPrivateKey.Thumbprint, document.SignedBy.Thumbprint); 150 | Assert.Equal(TestHelpers.Certificate1WithPrivateKey.Thumbprint, document.ContentKeys.SignedBy.Single().Thumbprint); 151 | Assert.Equal(TestHelpers.Certificate3WithPrivateKey.Thumbprint, document.Recipients.Single().Certificate.Thumbprint); 152 | 153 | var key = document.ContentKeys.Single(); 154 | Assert.Equal(keyData.Item1, key.Id); 155 | Assert.Equal(keyData.Item2, key.Value); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Cpix/DrmSystemCollection.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Internal; 2 | using System; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix 8 | { 9 | sealed class DrmSystemCollection : EntityCollection 10 | { 11 | public const string ContainerXmlElementName = "DRMSystemList"; 12 | 13 | public DrmSystemCollection(CpixDocument document) : base(document) 14 | { 15 | } 16 | 17 | internal override string ContainerName => ContainerXmlElementName; 18 | 19 | protected override XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, DrmSystem entity) 20 | { 21 | var drmSystemElement = new DrmSystemElement 22 | { 23 | SystemId = entity.SystemId, 24 | KeyId = entity.KeyId, 25 | Pssh = entity.Pssh, 26 | SmoothStreamingProtectionHeaderData = entity.SmoothStreamingProtectionHeaderData 27 | }; 28 | 29 | if (entity.ContentProtectionData != null) 30 | drmSystemElement.ContentProtectionData = Convert.ToBase64String(Encoding.UTF8.GetBytes(entity.ContentProtectionData)); 31 | 32 | if (entity.UriExtXKey != null) 33 | drmSystemElement.UriExtXKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(entity.UriExtXKey)); 34 | 35 | if (entity.HdsSignalingData != null) 36 | drmSystemElement.HdsSignalingData = Convert.ToBase64String(Encoding.UTF8.GetBytes(entity.HdsSignalingData)); 37 | 38 | if (entity.HlsSignalingData?.MasterPlaylistData != null) 39 | { 40 | drmSystemElement.HlsSignalingData.Add(new HlsSignalingDataElement 41 | { 42 | Playlist = HlsPlaylistType.Master, 43 | Value = Convert.ToBase64String(Encoding.UTF8.GetBytes(entity.HlsSignalingData.MasterPlaylistData)) 44 | }); 45 | } 46 | 47 | if (entity.HlsSignalingData?.MediaPlaylistData != null) 48 | { 49 | drmSystemElement.HlsSignalingData.Add(new HlsSignalingDataElement 50 | { 51 | Playlist = HlsPlaylistType.Media, 52 | Value = Convert.ToBase64String(Encoding.UTF8.GetBytes(entity.HlsSignalingData.MediaPlaylistData)) 53 | }); 54 | } 55 | 56 | return XmlHelpers.AppendChildAndReuseNamespaces(drmSystemElement, container); 57 | } 58 | 59 | protected override DrmSystem DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces) 60 | { 61 | var drmSystemElement = XmlHelpers.Deserialize(element); 62 | 63 | drmSystemElement.LoadTimeValidate(); 64 | 65 | var drmSystem = new DrmSystem 66 | { 67 | SystemId = drmSystemElement.SystemId, 68 | KeyId = drmSystemElement.KeyId, 69 | Pssh = drmSystemElement.Pssh, 70 | SmoothStreamingProtectionHeaderData = drmSystemElement.SmoothStreamingProtectionHeaderData 71 | }; 72 | 73 | if (drmSystemElement.ContentProtectionData != null) 74 | drmSystem.ContentProtectionData = Encoding.UTF8.GetString(Convert.FromBase64String(drmSystemElement.ContentProtectionData)); 75 | 76 | if (drmSystemElement.UriExtXKey != null) 77 | drmSystem.UriExtXKey = Encoding.UTF8.GetString(Convert.FromBase64String(drmSystemElement.UriExtXKey)); 78 | 79 | if (drmSystemElement.HdsSignalingData != null) 80 | drmSystem.HdsSignalingData = Encoding.UTF8.GetString(Convert.FromBase64String(drmSystemElement.HdsSignalingData)); 81 | 82 | if (drmSystemElement.HlsSignalingData.Count > 0) 83 | { 84 | drmSystem.HlsSignalingData = new HlsSignalingData(); 85 | 86 | var mediaPlaylistDataAsBase64 = drmSystemElement.HlsSignalingData 87 | .SingleOrDefault(d => d.Playlist == null || string.Equals(d.Playlist, HlsPlaylistType.Media, StringComparison.InvariantCulture))?.Value; 88 | 89 | var masterPlaylistDataAsBase64 = drmSystemElement.HlsSignalingData 90 | .SingleOrDefault(d => string.Equals(d.Playlist, HlsPlaylistType.Master, StringComparison.InvariantCulture))?.Value; 91 | 92 | if (mediaPlaylistDataAsBase64 != null) 93 | drmSystem.HlsSignalingData.MediaPlaylistData = Encoding.UTF8.GetString(Convert.FromBase64String(mediaPlaylistDataAsBase64)); 94 | 95 | if (masterPlaylistDataAsBase64 != null) 96 | drmSystem.HlsSignalingData.MasterPlaylistData = Encoding.UTF8.GetString(Convert.FromBase64String(masterPlaylistDataAsBase64)); 97 | } 98 | 99 | return drmSystem; 100 | } 101 | 102 | protected override void ValidateCollectionStateBeforeAdd(DrmSystem entity) 103 | { 104 | if (this.Any(i => i.SystemId == entity.SystemId && i.KeyId == entity.KeyId)) 105 | throw new InvalidOperationException( 106 | "The collection already contains a DRM system signaling entry with the same system ID and content key ID combination."); 107 | 108 | base.ValidateCollectionStateBeforeAdd(entity); 109 | } 110 | 111 | internal override void ValidateCollectionStateAfterLoad() 112 | { 113 | base.ValidateCollectionStateAfterLoad(); 114 | 115 | if (this.Select(i => new { i.SystemId, i.KeyId }).Distinct().Count() != LoadedItems.Count()) 116 | throw new InvalidCpixDataException( 117 | "The collection contains multiple DRM system signaling entries with the same system ID and content key ID combination."); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Cpix/CryptographyHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography.X509Certificates; 3 | using System.Security.Cryptography.Xml; 4 | using System.Xml; 5 | 6 | namespace Axinom.Cpix 7 | { 8 | static class CryptographyHelpers 9 | { 10 | internal static void ValidateRecipientCertificateAndPublicKey(X509Certificate2 certificate) 11 | { 12 | if (certificate.SignatureAlgorithm.Value == Constants.Sha1Oid) 13 | throw new WeakCertificateException("Weak certificates (signed using SHA-1) cannot be used with this library."); 14 | 15 | var rsaKey = certificate.GetRSAPublicKey(); 16 | 17 | if (rsaKey == null) 18 | throw new NotSupportedException("Only RSA keys are currently supported for recipient certificates."); 19 | 20 | if (rsaKey.KeySize < Constants.MinimumRsaKeySizeInBits) 21 | throw new WeakCertificateException($"The RSA key must be at least {Constants.MinimumRsaKeySizeInBits} bits long."); 22 | } 23 | 24 | internal static void ValidateSignerCertificate(X509Certificate2 certificate) 25 | { 26 | if (certificate.SignatureAlgorithm.Value == Constants.Sha1Oid) 27 | throw new WeakCertificateException("Weak certificates (signed using SHA-1) cannot be used with this library."); 28 | 29 | if (!certificate.HasPrivateKey) 30 | throw new ArgumentException("The private key of the supplied signer certificate is not available ."); 31 | 32 | var rsaKey = certificate.GetRSAPublicKey(); 33 | 34 | if (rsaKey == null) 35 | throw new NotSupportedException("Only RSA keys are currently supported for signer certificates."); 36 | 37 | if (rsaKey.KeySize < Constants.MinimumRsaKeySizeInBits) 38 | throw new WeakCertificateException($"The RSA key must be at least {Constants.MinimumRsaKeySizeInBits} bits long."); 39 | } 40 | 41 | internal static void ValidateRecipientCertificateAndPrivateKey(X509Certificate2 certificate) 42 | { 43 | if (certificate.SignatureAlgorithm.Value == Constants.Sha1Oid) 44 | throw new WeakCertificateException("Weak certificates (signed using SHA-1) cannot be used with this library."); 45 | 46 | if (!certificate.HasPrivateKey) 47 | throw new ArgumentException("The private key of the supplied recipient certificate is not available ."); 48 | 49 | var rsaKey = certificate.GetRSAPublicKey(); 50 | 51 | if (rsaKey == null) 52 | throw new NotSupportedException("Only RSA keys are currently supported for recipient certificates."); 53 | 54 | if (rsaKey.KeySize < Constants.MinimumRsaKeySizeInBits) 55 | throw new WeakCertificateException($"The RSA key must be at least {Constants.MinimumRsaKeySizeInBits} bits long."); 56 | } 57 | 58 | /// 59 | /// Signs an XML element referenced by ID and places the signature element under the document root element. 60 | /// Pass an empty string as the element to sign the entire document. 61 | /// 62 | internal static XmlElement SignXmlElement(XmlDocument document, string elementToSignId, X509Certificate2 signer) 63 | { 64 | if (document == null) 65 | throw new ArgumentNullException(nameof(document)); 66 | 67 | if (elementToSignId == null) 68 | throw new ArgumentNullException(nameof(elementToSignId)); 69 | 70 | if (signer == null) 71 | throw new ArgumentNullException(nameof(signer)); 72 | 73 | using (var signingKey = signer.GetRSAPrivateKey()) 74 | { 75 | var signedXml = new SignedXml(document) 76 | { 77 | SigningKey = signingKey 78 | }; 79 | 80 | // Add each content key assignment rule element as a reference to sign. 81 | var whatToSign = new Reference 82 | { 83 | // A nice strong algorithm without known weaknesses that are easily exploitable. 84 | DigestMethod = Constants.Sha512Algorithm 85 | }; 86 | 87 | if (elementToSignId == "") 88 | { 89 | // Sign the document. 90 | whatToSign.Uri = ""; 91 | 92 | // This is needed because the signature is within the signed data. 93 | whatToSign.AddTransform(new XmlDsigEnvelopedSignatureTransform()); 94 | } 95 | else 96 | { 97 | // Sign one specific element. 98 | whatToSign.Uri = "#" + elementToSignId; 99 | } 100 | 101 | signedXml.AddReference(whatToSign); 102 | 103 | // A nice strong algorithm without known weaknesses that are easily exploitable. 104 | // The below URI is also contained in "SignedXml.XmlDsigRSASHA512Url", but it doesn't 105 | // have public visibility in the .NET Core version of the library. 106 | signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; 107 | 108 | // Canonical XML 1.0 (omit comments); I suppose it works fine, no deep thoughts about this. 109 | signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigCanonicalizationUrl; 110 | 111 | // Signer certificate must be delivered with the signature. 112 | signedXml.KeyInfo.AddClause(new KeyInfoX509Data(signer)); 113 | 114 | // Ready to sign! Let's go! 115 | signedXml.ComputeSignature(); 116 | 117 | // Now stick the Signature element it generated back into the document and we are done. 118 | var signature = signedXml.GetXml(); 119 | return (XmlElement)document.DocumentElement.AppendChild(document.ImportNode(signature, true)); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Cpix/WidevinePsshData.cs: -------------------------------------------------------------------------------- 1 | // This file was generated by a tool; you should avoid making direct changes. 2 | // Consider using 'partial classes' to extend these types 3 | // Input: WidevinePsshData.proto 4 | // Input based on: https://github.com/google/shaka-packager/blob/master/packager/media/base/widevine_pssh_data.proto 5 | // 6 | // Licensing text of the base proto file: 7 | // ====================================================== 8 | // Copyright 2016 Google Inc. All rights reserved. 9 | // 10 | // Use of this source code is governed by a BSD-style 11 | // license that can be found in the LICENSE file or at 12 | // https://developers.google.com/open-source/licenses/bsd 13 | // ====================================================== 14 | 15 | 16 | #pragma warning disable CS1591, CS0612, CS3021, IDE1006 17 | namespace shaka.media 18 | { 19 | 20 | [global::ProtoBuf.ProtoContract()] 21 | public partial class WidevinePsshData : global::ProtoBuf.IExtensible 22 | { 23 | private global::ProtoBuf.IExtension __pbn__extensionData; 24 | global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) 25 | => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing); 26 | 27 | [global::ProtoBuf.ProtoMember(1)] 28 | [global::System.ComponentModel.DefaultValue(Algorithm.Unencrypted)] 29 | public Algorithm algorithm 30 | { 31 | get { return __pbn__algorithm ?? Algorithm.Unencrypted; } 32 | set { __pbn__algorithm = value; } 33 | } 34 | public bool ShouldSerializealgorithm() => __pbn__algorithm != null; 35 | public void Resetalgorithm() => __pbn__algorithm = null; 36 | private Algorithm? __pbn__algorithm; 37 | 38 | [global::ProtoBuf.ProtoMember(2, Name = @"key_id")] 39 | public global::System.Collections.Generic.List KeyIds { get; } = new global::System.Collections.Generic.List(); 40 | 41 | [global::ProtoBuf.ProtoMember(3, Name = @"provider")] 42 | [global::System.ComponentModel.DefaultValue("")] 43 | public string Provider 44 | { 45 | get { return __pbn__Provider ?? ""; } 46 | set { __pbn__Provider = value; } 47 | } 48 | public bool ShouldSerializeProvider() => __pbn__Provider != null; 49 | public void ResetProvider() => __pbn__Provider = null; 50 | private string __pbn__Provider; 51 | 52 | [global::ProtoBuf.ProtoMember(4, Name = @"content_id")] 53 | public byte[] ContentId 54 | { 55 | get { return __pbn__ContentId; } 56 | set { __pbn__ContentId = value; } 57 | } 58 | public bool ShouldSerializeContentId() => __pbn__ContentId != null; 59 | public void ResetContentId() => __pbn__ContentId = null; 60 | private byte[] __pbn__ContentId; 61 | 62 | [global::ProtoBuf.ProtoMember(6, Name = @"policy")] 63 | [global::System.ComponentModel.DefaultValue("")] 64 | public string Policy 65 | { 66 | get { return __pbn__Policy ?? ""; } 67 | set { __pbn__Policy = value; } 68 | } 69 | public bool ShouldSerializePolicy() => __pbn__Policy != null; 70 | public void ResetPolicy() => __pbn__Policy = null; 71 | private string __pbn__Policy; 72 | 73 | [global::ProtoBuf.ProtoMember(7, Name = @"crypto_period_index")] 74 | public uint CryptoPeriodIndex 75 | { 76 | get { return __pbn__CryptoPeriodIndex.GetValueOrDefault(); } 77 | set { __pbn__CryptoPeriodIndex = value; } 78 | } 79 | public bool ShouldSerializeCryptoPeriodIndex() => __pbn__CryptoPeriodIndex != null; 80 | public void ResetCryptoPeriodIndex() => __pbn__CryptoPeriodIndex = null; 81 | private uint? __pbn__CryptoPeriodIndex; 82 | 83 | [global::ProtoBuf.ProtoMember(8, Name = @"grouped_license")] 84 | public byte[] GroupedLicense 85 | { 86 | get { return __pbn__GroupedLicense; } 87 | set { __pbn__GroupedLicense = value; } 88 | } 89 | public bool ShouldSerializeGroupedLicense() => __pbn__GroupedLicense != null; 90 | public void ResetGroupedLicense() => __pbn__GroupedLicense = null; 91 | private byte[] __pbn__GroupedLicense; 92 | 93 | [global::ProtoBuf.ProtoMember(9, Name = @"protection_scheme")] 94 | public uint ProtectionScheme 95 | { 96 | get { return __pbn__ProtectionScheme.GetValueOrDefault(); } 97 | set { __pbn__ProtectionScheme = value; } 98 | } 99 | public bool ShouldSerializeProtectionScheme() => __pbn__ProtectionScheme != null; 100 | public void ResetProtectionScheme() => __pbn__ProtectionScheme = null; 101 | private uint? __pbn__ProtectionScheme; 102 | 103 | [global::ProtoBuf.ProtoContract()] 104 | public enum Algorithm 105 | { 106 | [global::ProtoBuf.ProtoEnum(Name = @"UNENCRYPTED")] 107 | Unencrypted = 0, 108 | [global::ProtoBuf.ProtoEnum(Name = @"AESCTR")] 109 | Aesctr = 1, 110 | } 111 | 112 | } 113 | 114 | } 115 | 116 | #pragma warning restore CS1591, CS0612, CS3021, IDE1006 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # Visual Studio Code 31 | .vscode/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # Cordova 47 | [Bb]ld/ 48 | [Pp]lugins/android.json 49 | [Pp]lugins/windows.json 50 | [Pp]lugins/wp8.json 51 | [Pp]lugins/remote_ios.json 52 | [Pp]latforms/ 53 | 54 | # DNX 55 | project.lock.json 56 | artifacts/ 57 | 58 | *_i.c 59 | *_p.c 60 | *_i.h 61 | *.ilk 62 | *.meta 63 | *.obj 64 | *.pch 65 | *.pdb 66 | *.pgc 67 | *.pgd 68 | *.rsp 69 | *.sbr 70 | *.tlb 71 | *.tli 72 | *.tlh 73 | *.tmp 74 | *.tmp_proj 75 | *.log 76 | *.vspscc 77 | *.vssscc 78 | .builds 79 | *.pidb 80 | *.svclog 81 | *.scc 82 | 83 | # Chutzpah Test files 84 | _Chutzpah* 85 | 86 | # Visual C++ cache files 87 | ipch/ 88 | *.aps 89 | *.ncb 90 | *.opendb 91 | *.opensdf 92 | *.sdf 93 | *.cachefile 94 | *.VC.db 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | *.sap 101 | 102 | # TFS 2012 Local Workspace 103 | $tf/ 104 | 105 | # Guidance Automation Toolkit 106 | *.gpState 107 | 108 | # ReSharper is a .NET coding add-in 109 | _ReSharper*/ 110 | *.[Rr]e[Ss]harper 111 | *.DotSettings.user 112 | 113 | # JustCode is a .NET coding add-in 114 | .JustCode 115 | 116 | # TeamCity is a build add-in 117 | _TeamCity* 118 | 119 | # DotCover is a Code Coverage Tool 120 | *.dotCover 121 | 122 | # NCrunch 123 | _NCrunch_* 124 | .*crunch*.local.xml 125 | nCrunchTemp_* 126 | 127 | # MightyMoose 128 | *.mm.* 129 | AutoTest.Net/ 130 | 131 | # Web workbench (sass) 132 | .sass-cache/ 133 | 134 | # Installshield output folder 135 | [Ee]xpress/ 136 | 137 | # DocProject is a documentation generator add-in 138 | DocProject/buildhelp/ 139 | DocProject/Help/*.HxT 140 | DocProject/Help/*.HxC 141 | DocProject/Help/*.hhc 142 | DocProject/Help/*.hhk 143 | DocProject/Help/*.hhp 144 | DocProject/Help/Html2 145 | DocProject/Help/html 146 | 147 | # Click-Once directory 148 | publish/ 149 | 150 | # Publish Web Output 151 | *.[Pp]ublish.xml 152 | *.azurePubxml 153 | 154 | # TODO: Un-comment the next line if you do not want to checkin 155 | # your web deploy settings because they may include unencrypted 156 | # passwords 157 | #*.pubxml 158 | *.publishproj 159 | 160 | # NuGet Packages 161 | *.nupkg 162 | # The packages folder can be ignored because of Package Restore 163 | **/packages/* 164 | # except build/, which is used as an MSBuild target. 165 | !**/packages/build/ 166 | # Uncomment if necessary however generally it will be regenerated when needed 167 | #!**/packages/repositories.config 168 | # NuGet v3's project.json files produces more ignoreable files 169 | *.nuget.props 170 | *.nuget.targets 171 | 172 | # Microsoft Azure Build Output 173 | csx/ 174 | *.build.csdef 175 | 176 | # Microsoft Azure Emulator 177 | ecf/ 178 | rcf/ 179 | 180 | # Microsoft Azure ApplicationInsights config file 181 | ApplicationInsights.config 182 | 183 | # Windows Store app package directory 184 | AppPackages/ 185 | BundleArtifacts/ 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | [Ss]tyle[Cc]op.* 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # LightSwitch generated files 249 | GeneratedArtifacts/ 250 | ModelManifest.xml 251 | 252 | # Paket dependency manager 253 | .paket/paket.exe 254 | 255 | # FAKE - F# Make 256 | .fake/ -------------------------------------------------------------------------------- /Cpix/DrmSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Xml; 5 | using System.Xml.XPath; 6 | 7 | namespace Axinom.Cpix 8 | { 9 | public sealed class DrmSystem : Entity 10 | { 11 | /// 12 | /// Gets or sets the system ID of the DRM system that is being signaled. 13 | /// Refer to the DASH-IF System ID registry for a list of DRM system IDs. 14 | /// 15 | public Guid SystemId { get; set; } 16 | 17 | /// 18 | /// Gets or sets the ID of the content key the DRM system signaling entry 19 | /// references. 20 | /// 21 | public Guid KeyId { get; set; } 22 | 23 | /// 24 | /// Gets or sets the full PSSH box, encoded as base64, that shall be added to 25 | /// ISOBMFF files encrypted with the content key referenced by the DRM system 26 | /// signaling entry. This data should only be associated with a hierarchical 27 | /// leaf key. 28 | /// 29 | public string Pssh { get; set; } 30 | 31 | /// 32 | /// Gets or sets the content protection data, which is the full well-formed 33 | /// XML fragment that shall be added under the ContentProtection element in a 34 | /// DASH manifest. This must be a UTF-8 XML string without a byte order mark. 35 | /// This data shall not be associated with a hierarchical leaf key. 36 | /// 37 | public string ContentProtectionData { get; set; } 38 | 39 | /// 40 | /// Gets or sets the EXT-X-KEY URI data. This is the full data to be added in 41 | /// the URI attribute of the EXT-X-KEY tag of a HLS playlist. This must be UTF-8 42 | /// text without a byte order mark. This shall only be set when the content is 43 | /// in the HLS format. The use of this element is deprecated. Using 44 | /// HLSSignalingData is recommended. 45 | /// 46 | public string UriExtXKey { get; set; } 47 | 48 | /// 49 | /// Gets or sets the HLS signaling data to be inserted in HLS master and/or 50 | /// media playlists. The data includes #EXT-X-KEY or #EXT-X-SESSION-KEY 51 | /// tags (depending on the playlist), along with potential proprietary tags. 52 | /// This data shall not be associated with a hierarchical leaf key. 53 | /// 54 | public HlsSignalingData HlsSignalingData { get; set; } 55 | 56 | /// 57 | /// Gets or sets the Smooth Streaming Protection Header data, to be used as 58 | /// the inner text of the ProtectionHeader XML element in a Smooth Streaming 59 | /// manifest. This is UTF-8 text without a byte order mark. This data shall 60 | /// not be associated with a hierarchical leaf key. 61 | /// 62 | public string SmoothStreamingProtectionHeaderData { get; set; } 63 | 64 | /// 65 | /// Gets or sets the HDS signaling data, which is the full 66 | /// "drmAdditionalHeader" XML element, intended to be used in a Flash media 67 | /// manifest. This is a UTF-8 XML string without a byte order mark. This data 68 | /// shall not be associated with a hierarchical leaf key. 69 | /// 70 | public string HdsSignalingData { get; set; } 71 | 72 | 73 | internal override void ValidateNewEntity(CpixDocument document) 74 | { 75 | ValidateEntity(); 76 | } 77 | 78 | internal override void ValidateLoadedEntity(CpixDocument document) 79 | { 80 | ValidateEntity(); 81 | } 82 | 83 | private void ValidateEntity() 84 | { 85 | if (SystemId == Guid.Empty) 86 | throw new InvalidCpixDataException("A system ID must be provided for each DRM system signaling entry."); 87 | 88 | if (KeyId == Guid.Empty) 89 | throw new InvalidCpixDataException("A content key ID must be provided for each DRM system signaling entry."); 90 | 91 | if (Pssh != null) 92 | { 93 | try 94 | { 95 | var temp = Convert.FromBase64String(Pssh); 96 | } 97 | catch (Exception ex) 98 | { 99 | throw new InvalidCpixDataException("The PSSH must be base64-encoded.", ex); 100 | } 101 | } 102 | 103 | if (ContentProtectionData != null) 104 | { 105 | try 106 | { 107 | var temp = new XPathDocument(XmlReader.Create( 108 | new MemoryStream(Encoding.UTF8.GetBytes(ContentProtectionData)), 109 | new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment })); 110 | } 111 | catch (Exception ex) 112 | { 113 | throw new InvalidCpixDataException( 114 | $"The content protection data must be a well-formed XML fragment. Error details: {ex.Message}", ex); 115 | } 116 | } 117 | 118 | if (HdsSignalingData != null) 119 | { 120 | XPathDocument document; 121 | 122 | try 123 | { 124 | document = new XPathDocument(XmlReader.Create( 125 | new MemoryStream(Encoding.UTF8.GetBytes(HdsSignalingData)), 126 | new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Document })); 127 | } 128 | catch (Exception ex) 129 | { 130 | throw new InvalidCpixDataException( 131 | $"The HDS signaling data must be a well-formed XML element. Error details: {ex.Message}", ex); 132 | } 133 | 134 | var navigator = document.CreateNavigator(); 135 | 136 | if (!(navigator.MoveToFirstChild() && navigator.LocalName.Equals("drmAdditionalHeader", StringComparison.InvariantCulture))) 137 | { 138 | throw new InvalidCpixDataException("The HDS signaling data must be the full \"drmAdditionalHeader\" XML element."); 139 | } 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Tests/ContentKeyEncryptionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace Axinom.Cpix.Tests 6 | { 7 | public sealed class ContentKeyEncryptionTests 8 | { 9 | [Fact] 10 | public void LoadingEncryptedKey_WithRecipientPrivateKey_DecryptsKey() 11 | { 12 | var keyData = TestHelpers.GenerateKeyData(); 13 | 14 | var document = new CpixDocument(); 15 | document.ContentKeys.Add(new ContentKey 16 | { 17 | Id = keyData.Item1, 18 | Value = keyData.Item2 19 | }); 20 | 21 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 22 | 23 | document = TestHelpers.Reload(document, new[] { TestHelpers.Certificate3WithPrivateKey }); 24 | 25 | var key = document.ContentKeys.Single(); 26 | Assert.NotNull(key.Value); 27 | Assert.Equal(keyData.Item1, key.Id); 28 | Assert.Equal(keyData.Item2, key.Value); 29 | Assert.True(document.ContentKeysAreReadable); 30 | } 31 | 32 | [Fact] 33 | public void LoadingEncryptedKey_WithoutRecipientPrivateKey_SucceedsWithoutDecryptingKey() 34 | { 35 | var keyData = TestHelpers.GenerateKeyData(); 36 | 37 | var document = new CpixDocument(); 38 | document.ContentKeys.Add(new ContentKey 39 | { 40 | Id = keyData.Item1, 41 | Value = keyData.Item2 42 | }); 43 | 44 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 45 | 46 | document = TestHelpers.Reload(document); 47 | 48 | var key = document.ContentKeys.Single(); 49 | Assert.Null(key.Value); 50 | Assert.Equal(keyData.Item1, key.Id); 51 | Assert.False(document.ContentKeysAreReadable); 52 | } 53 | 54 | [Fact] 55 | public void LoadingDocument_WithTwoRecipients_DetectsRecipients() 56 | { 57 | var document = new CpixDocument(); 58 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 59 | 60 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 61 | document.Recipients.Add(new Recipient(TestHelpers.Certificate4WithPublicKey)); 62 | 63 | document = TestHelpers.Reload(document); 64 | 65 | Assert.Equal(2, document.Recipients.Count); 66 | 67 | Assert.Equal(1, document.Recipients.Count(r => r.Certificate.Thumbprint == TestHelpers.Certificate3WithPublicKey.Thumbprint)); 68 | Assert.Equal(1, document.Recipients.Count(r => r.Certificate.Thumbprint == TestHelpers.Certificate4WithPublicKey.Thumbprint)); 69 | } 70 | 71 | [Fact] 72 | public void LoadingDocument_WithTwoRecipients_DecryptsContentKeysWithEitherRecipient() 73 | { 74 | var document = new CpixDocument(); 75 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 76 | 77 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 78 | document.Recipients.Add(new Recipient(TestHelpers.Certificate4WithPublicKey)); 79 | 80 | var decryptedDocument1 = TestHelpers.Reload(document, new[] { TestHelpers.Certificate3WithPrivateKey }); 81 | var decryptedDocument2 = TestHelpers.Reload(document, new[] { TestHelpers.Certificate4WithPrivateKey }); 82 | 83 | Assert.True(decryptedDocument1.ContentKeysAreReadable); 84 | Assert.True(decryptedDocument2.ContentKeysAreReadable); 85 | } 86 | 87 | [Fact] 88 | public void AddRecipient_WithLoadedDocumentAndReadContentKeys_Fails() 89 | { 90 | var document = new CpixDocument(); 91 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 92 | 93 | document = TestHelpers.Reload(document); 94 | 95 | // The keys are read-only so they cannot be encrypted! 96 | Assert.Throws(() => document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey))); 97 | } 98 | 99 | [Fact] 100 | public void AddRecipient_WithLoadedDocumentAndWrittenContentKeys_SucceedsAndEncryptsContentKeys() 101 | { 102 | // We start with some clear keys. 103 | var document = new CpixDocument(); 104 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 105 | 106 | document = TestHelpers.Reload(document); 107 | 108 | // Now we re-add keys to mark them for processing. 109 | var keys = document.ContentKeys.ToArray(); 110 | document.ContentKeys.Clear(); 111 | 112 | foreach (var key in keys) 113 | document.ContentKeys.Add(key); 114 | 115 | // This marks the keys as to be encrypted. 116 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 117 | 118 | document = TestHelpers.Reload(document, new[] { TestHelpers.Certificate3WithPrivateKey }); 119 | 120 | Assert.Single(document.Recipients); 121 | Assert.True(document.ContentKeysAreReadable); 122 | } 123 | 124 | [Fact] 125 | public void RemoveRecipients_WithLoadedDocumentAndWrittenContentKeys_SucceedsAndDecryptsContentKeys() 126 | { 127 | // We start with some encrypted keys. 128 | var document = new CpixDocument(); 129 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 130 | document.Recipients.Add(new Recipient(TestHelpers.Certificate3WithPublicKey)); 131 | 132 | // Load and decrypt keys. 133 | document = TestHelpers.Reload(document, new[] { TestHelpers.Certificate3WithPrivateKey }); 134 | 135 | // Re-add keys to mark them for processing. 136 | var keys = document.ContentKeys.ToArray(); 137 | document.ContentKeys.Clear(); 138 | 139 | foreach (var key in keys) 140 | document.ContentKeys.Add(key); 141 | 142 | // Remove recipients - we will output clear keys. 143 | document.Recipients.Clear(); 144 | 145 | document = TestHelpers.Reload(document); 146 | 147 | Assert.Empty(document.Recipients); 148 | Assert.True(document.ContentKeysAreReadable); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Cpix/ContentKeyCollection.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Internal; 2 | using System; 3 | using System.Linq; 4 | using System.Security; 5 | using System.Security.Cryptography; 6 | using System.Xml; 7 | 8 | namespace Axinom.Cpix 9 | { 10 | sealed class ContentKeyCollection : EntityCollection 11 | { 12 | public const string ContainerXmlElementName = "ContentKeyList"; 13 | 14 | public ContentKeyCollection(CpixDocument document) : base(document) 15 | { 16 | } 17 | 18 | internal override string ContainerName => ContainerXmlElementName; 19 | 20 | protected override XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, ContentKey entity) 21 | { 22 | var element = new ContentKeyElement 23 | { 24 | KeyId = entity.Id, 25 | ExplicitIv = entity.ExplicitIv, 26 | CommonEncryptionScheme = entity.CommonEncryptionScheme, 27 | }; 28 | 29 | // We support content keys without a value because many packagers 30 | // create such documents for requesting keys from key services, 31 | // which then fill in the value. 32 | if (entity.Value != null) 33 | { 34 | element.Data = new DataElement 35 | { 36 | Secret = new SecretDataElement() 37 | }; 38 | 39 | if (Document.Recipients.Any()) 40 | { 41 | // We have to encrypt the key. Okay. Ensure we have the crypto values available. 42 | if (Document.DocumentKey == null) 43 | Document.GenerateKeys(); 44 | 45 | // Unique IV is generated for every content key. 46 | var iv = new byte[128 / 8]; 47 | 48 | using (var random = RandomNumberGenerator.Create()) 49 | random.GetBytes(iv); 50 | 51 | var aes = new AesManaged 52 | { 53 | BlockSize = 128, 54 | KeySize = 256, 55 | Key = Document.DocumentKey, 56 | Mode = CipherMode.CBC, 57 | Padding = PaddingMode.PKCS7, 58 | IV = iv 59 | }; 60 | 61 | var mac = new HMACSHA512(Document.MacKey); 62 | 63 | using (var encryptor = aes.CreateEncryptor()) 64 | { 65 | var encryptedValue = encryptor.TransformFinalBlock(entity.Value, 0, entity.Value.Length); 66 | 67 | // NB! We prepend the IV to the value when saving an encrypted value to the document field. 68 | var fieldValue = iv.Concat(encryptedValue).ToArray(); 69 | 70 | element.Data.Secret.EncryptedValue = new EncryptedXmlValue 71 | { 72 | CipherData = new CipherDataContainer 73 | { 74 | CipherValue = fieldValue 75 | }, 76 | EncryptionMethod = new EncryptionMethodDeclaration 77 | { 78 | Algorithm = Constants.Aes256CbcAlgorithm 79 | } 80 | }; 81 | 82 | // Never not MAC. 83 | element.Data.Secret.ValueMAC = mac.ComputeHash(fieldValue); 84 | } 85 | } 86 | else 87 | { 88 | // We are saving the key in the clear. 89 | element.Data.Secret.PlainValue = entity.Value; 90 | } 91 | } 92 | 93 | return XmlHelpers.AppendChildAndReuseNamespaces(element, container); 94 | } 95 | 96 | protected override ContentKey DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces) 97 | { 98 | var contentKey = XmlHelpers.Deserialize(element); 99 | contentKey.LoadTimeValidate(); 100 | 101 | if (contentKey.HasPlainValue && Document.Recipients.Any()) 102 | throw new InvalidCpixDataException("A content key was delivered in the clear but delivery data was defined. Malformed CPIX!?"); 103 | 104 | byte[] value = null; 105 | 106 | if (contentKey.HasEncryptedValue && Document.ContentKeysAreReadable) 107 | { 108 | var mac = new HMACSHA512(Document.MacKey); 109 | 110 | var calculatedMac = mac.ComputeHash(contentKey.Data.Secret.EncryptedValue.CipherData.CipherValue); 111 | 112 | if (!calculatedMac.SequenceEqual(contentKey.Data.Secret.ValueMAC)) 113 | throw new SecurityException("MAC validation failed - a content key value has been tampered with!"); 114 | 115 | var iv = contentKey.Data.Secret.EncryptedValue.CipherData.CipherValue.Take(128 / 8).ToArray(); 116 | var encryptedKey = contentKey.Data.Secret.EncryptedValue.CipherData.CipherValue.Skip(128 / 8).ToArray(); 117 | 118 | var aes = new AesManaged 119 | { 120 | BlockSize = 128, 121 | KeySize = 256, 122 | Key = Document.DocumentKey, 123 | Mode = CipherMode.CBC, 124 | Padding = PaddingMode.PKCS7, 125 | IV = iv 126 | }; 127 | 128 | using (var decryptor = aes.CreateDecryptor()) 129 | { 130 | value = decryptor.TransformFinalBlock(encryptedKey, 0, encryptedKey.Length); 131 | } 132 | } 133 | else if (contentKey.HasPlainValue) 134 | { 135 | value = contentKey.Data.Secret.PlainValue; 136 | } 137 | else 138 | { 139 | // Value is encrypted and we cannot read it. Nothing to do here. 140 | } 141 | 142 | return new ContentKey 143 | { 144 | Id = contentKey.KeyId, 145 | ExplicitIv = contentKey.ExplicitIv, 146 | CommonEncryptionScheme = contentKey.CommonEncryptionScheme, 147 | Value = value, 148 | IsLoadedEncryptedKey = contentKey.HasEncryptedValue 149 | }; 150 | } 151 | 152 | protected override void ValidateCollectionStateBeforeAdd(ContentKey entity) 153 | { 154 | if (this.Any(key => key.Id == entity.Id)) 155 | throw new InvalidOperationException("The collection already contains a content key with the same ID."); 156 | 157 | if (!Document.ContentKeysAreReadable) 158 | throw new InvalidOperationException("New content keys cannot be added to a loaded CPIX document that contains encrypted content keys if you do not possess a delivery key."); 159 | 160 | base.ValidateCollectionStateBeforeAdd(entity); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /TestVectorGenerator/Complex.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Tests; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Axinom.Cpix.TestVectorGenerator 7 | { 8 | sealed class Complex : ITestVector 9 | { 10 | public string Description => "All types of entities, with many data fields filled, with encryption of content keys and with signatures on everything. The document as a whole is signed using Cert4 and each collection is signed using both Cert3 and Cert4."; 11 | public bool OutputIsValid => true; 12 | 13 | public void Generate(Stream outputStream) 14 | { 15 | var document = new CpixDocument(); 16 | 17 | const string complexLabel = "滆 柦柋牬 趉軨鄇 鶊鵱, 緳廞徲 鋑鋡髬 溮煡煟 綡蒚"; 18 | 19 | document.SignedBy = TestHelpers.Certificate4WithPrivateKey; 20 | document.Recipients.AddSignature(TestHelpers.Certificate3WithPrivateKey); 21 | document.Recipients.AddSignature(TestHelpers.Certificate4WithPrivateKey); 22 | document.ContentKeys.AddSignature(TestHelpers.Certificate3WithPrivateKey); 23 | document.ContentKeys.AddSignature(TestHelpers.Certificate4WithPrivateKey); 24 | document.DrmSystems.AddSignature(TestHelpers.Certificate3WithPrivateKey); 25 | document.DrmSystems.AddSignature(TestHelpers.Certificate4WithPrivateKey); 26 | document.ContentKeyPeriods.AddSignature(TestHelpers.Certificate3WithPrivateKey); 27 | document.ContentKeyPeriods.AddSignature(TestHelpers.Certificate4WithPrivateKey); 28 | document.UsageRules.AddSignature(TestHelpers.Certificate3WithPrivateKey); 29 | document.UsageRules.AddSignature(TestHelpers.Certificate4WithPrivateKey); 30 | 31 | document.ContentKeys.Add(new ContentKey 32 | { 33 | Id = new Guid("b4c3188b-eddd-453d-9bc2-1cbca7566239"), 34 | Value = Convert.FromBase64String("b1pkxdNYqPxljV68gohWcw=="), 35 | ExplicitIv = Convert.FromBase64String("eMCi02KFz9fdUGd/6B+lgw=="), 36 | CommonEncryptionScheme = "cenc" 37 | }); 38 | document.ContentKeys.Add(new ContentKey 39 | { 40 | Id = new Guid("c6294999-5f48-445f-bcce-f7e5f736d7c6"), 41 | Value = Convert.FromBase64String("moOVrJvuhUUQ4LpPusAd5g=="), 42 | ExplicitIv = Convert.FromBase64String("XiLNTv8ZHbMI+F2g2TkL6w=="), 43 | CommonEncryptionScheme = "cenc" 44 | }); 45 | document.ContentKeys.Add(new ContentKey 46 | { 47 | Id = new Guid("b181a4df-2c38-41a4-993f-90b2f21343f6"), 48 | Value = Convert.FromBase64String("67gabJtKDWd2crHr+JQT1A=="), 49 | ExplicitIv = Convert.FromBase64String("E5XdJoK4ja+Rf/dcVvhRTA=="), 50 | CommonEncryptionScheme = "cenc" 51 | }); 52 | document.ContentKeys.Add(new ContentKey 53 | { 54 | Id = new Guid("a466cdfd-e556-4b1d-8098-c1a4aa78997a"), 55 | Value = Convert.FromBase64String("rRuRUWAibaUtai0qQnb71g=="), 56 | ExplicitIv = Convert.FromBase64String("yIZGwf6xwXgRHyXBNV0jRA=="), 57 | CommonEncryptionScheme = "cenc" 58 | }); 59 | 60 | DrmSignalingHelpers.AddDefaultSignalingForAllKeys(document); 61 | 62 | document.ContentKeyPeriods.Add(new ContentKeyPeriod 63 | { 64 | Id = "keyperiod_1", 65 | Index = 1 66 | }); 67 | document.ContentKeyPeriods.Add(new ContentKeyPeriod 68 | { 69 | Id = "keyperiod_2", 70 | Start = new DateTimeOffset(2020, 9, 4, 1, 1, 1, TimeSpan.Zero), 71 | End = new DateTimeOffset(2020, 9, 4, 2, 1, 1, TimeSpan.Zero) 72 | }); 73 | 74 | document.Recipients.Add(new Recipient(TestHelpers.Certificate1WithPublicKey)); 75 | document.Recipients.Add(new Recipient(TestHelpers.Certificate2WithPublicKey)); 76 | 77 | document.UsageRules.Add(new UsageRule 78 | { 79 | KeyId = document.ContentKeys.First().Id, 80 | IntendedTrackType = "UHD", 81 | 82 | AudioFilters = new[] 83 | { 84 | new AudioFilter 85 | { 86 | MinChannels = 1, 87 | MaxChannels = 2 88 | }, 89 | new AudioFilter 90 | { 91 | MinChannels = 8, 92 | MaxChannels = 10 93 | } 94 | }, 95 | BitrateFilters = new[] 96 | { 97 | new BitrateFilter 98 | { 99 | MinBitrate = 1000, 100 | MaxBitrate = 5 * 1000 * 1000 101 | }, 102 | new BitrateFilter 103 | { 104 | MinBitrate = 10 * 1000 * 1000, 105 | MaxBitrate = 32 * 1000 * 1000 106 | } 107 | }, 108 | LabelFilters = new[] 109 | { 110 | new LabelFilter("EncryptedStream"), 111 | new LabelFilter("CencStream"), 112 | new LabelFilter(complexLabel), 113 | } 114 | }); 115 | 116 | document.UsageRules.Add(new UsageRule 117 | { 118 | KeyId = document.ContentKeys.Last().Id, 119 | IntendedTrackType = "UHD+HFR", 120 | 121 | BitrateFilters = new[] 122 | { 123 | new BitrateFilter 124 | { 125 | MinBitrate = 1000, 126 | MaxBitrate = 5 * 1000 * 1000 127 | }, 128 | new BitrateFilter 129 | { 130 | MinBitrate = 10 * 1000 * 1000, 131 | MaxBitrate = 32 * 1000 * 1000 132 | } 133 | }, 134 | LabelFilters = new[] 135 | { 136 | new LabelFilter("EncryptedStream"), 137 | new LabelFilter("CencStream"), 138 | new LabelFilter(complexLabel), 139 | }, 140 | VideoFilters = new[] 141 | { 142 | new VideoFilter 143 | { 144 | MinPixels = 1000, 145 | MaxPixels = 1920 * 1080, 146 | MinFramesPerSecond = 10, 147 | MaxFramesPerSecond = 30, 148 | WideColorGamut = false, 149 | HighDynamicRange = true, 150 | }, 151 | new VideoFilter 152 | { 153 | MinPixels = 1000, 154 | MaxPixels = 4096 * 4096, 155 | MinFramesPerSecond = 30, 156 | MaxFramesPerSecond = 200, 157 | WideColorGamut = false, 158 | HighDynamicRange = false, 159 | } 160 | } 161 | }); 162 | 163 | document.Save(outputStream); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Tests/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace Axinom.Cpix.Tests 9 | { 10 | internal static class TestHelpers 11 | { 12 | public static CpixDocument Reload(CpixDocument document, IReadOnlyCollection decryptionCertificates = null) 13 | { 14 | var buffer = new MemoryStream(); 15 | 16 | document.Save(buffer); 17 | buffer.Position = 0; 18 | 19 | return CpixDocument.Load(buffer, decryptionCertificates); 20 | } 21 | 22 | public static Tuple GenerateKeyData() 23 | { 24 | var key = new byte[16]; 25 | Random.GetBytes(key); 26 | 27 | return new Tuple(Guid.NewGuid(), key); 28 | } 29 | 30 | public static ContentKey GenerateContentKey() 31 | { 32 | var keyData = GenerateKeyData(); 33 | 34 | return new ContentKey 35 | { 36 | Id = keyData.Item1, 37 | Value = keyData.Item2 38 | }; 39 | } 40 | 41 | public static UsageRule AddUsageRule(CpixDocument document) 42 | { 43 | var contentKey = document.ContentKeys.First(); 44 | var contentKeyPeriod = document.ContentKeyPeriods.First(); 45 | 46 | var rule = new UsageRule 47 | { 48 | KeyId = contentKey.Id, 49 | IntendedTrackType = "TestTrackType", 50 | 51 | // Some arbitrary filters here, just to generate interesting test data. 52 | AudioFilters = new[] 53 | { 54 | new AudioFilter 55 | { 56 | MaxChannels = 5, 57 | MinChannels = 0 58 | } 59 | }, 60 | BitrateFilters = new[] 61 | { 62 | new BitrateFilter 63 | { 64 | MinBitrate = 100, 65 | MaxBitrate = 5198493 66 | }, 67 | new BitrateFilter 68 | { 69 | MinBitrate = 5198494, 70 | MaxBitrate = 100000000000000 71 | } 72 | }, 73 | LabelFilters = new[] 74 | { 75 | new LabelFilter("aaaaa"), 76 | new LabelFilter("bbbb"), 77 | }, 78 | VideoFilters = new[] 79 | { 80 | new VideoFilter 81 | { 82 | MinPixels = 1000, 83 | MaxPixels = 1920 * 1080, 84 | MinFramesPerSecond = 10, 85 | MaxFramesPerSecond = 30, 86 | WideColorGamut = false, 87 | HighDynamicRange = true, 88 | }, 89 | new VideoFilter 90 | { 91 | MinPixels = 1000, 92 | MaxPixels = 4096 * 4096, 93 | MinFramesPerSecond = 30, 94 | MaxFramesPerSecond = 200, 95 | WideColorGamut = false, 96 | HighDynamicRange = false, 97 | } 98 | }, 99 | KeyPeriodFilters = new[] 100 | { 101 | new KeyPeriodFilter 102 | { 103 | PeriodId = contentKeyPeriod.Id 104 | } 105 | } 106 | }; 107 | 108 | document.UsageRules.Add(rule); 109 | 110 | return rule; 111 | } 112 | 113 | public static void PopulateCollections(CpixDocument document) 114 | { 115 | document.Recipients.Add(new Recipient(Certificate3WithPublicKey)); 116 | document.Recipients.Add(new Recipient(Certificate4WithPublicKey)); 117 | 118 | var key1 = GenerateContentKey(); 119 | var key2 = GenerateContentKey(); 120 | 121 | document.ContentKeys.Add(key1); 122 | document.ContentKeys.Add(key2); 123 | 124 | document.DrmSystems.Add(new DrmSystem 125 | { 126 | SystemId = Guid.NewGuid(), 127 | KeyId = document.ContentKeys.First().Id, 128 | ContentProtectionData = "Imaginary content protection data XML" 129 | }); 130 | 131 | document.ContentKeyPeriods.Add(new ContentKeyPeriod 132 | { 133 | Id = "keyperiod_1", 134 | Start = DateTimeOffset.UtcNow, 135 | End = DateTimeOffset.UtcNow.AddHours(1) 136 | }); 137 | 138 | AddUsageRule(document); 139 | AddUsageRule(document); 140 | 141 | // Sanity check. 142 | foreach (var collection in document.EntityCollections) 143 | { 144 | if (collection.Count == 0) 145 | throw new Exception("TestHelpers need update - not all collections got populated!"); 146 | } 147 | } 148 | 149 | public static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create(); 150 | 151 | // makecert -pe -n "CN=CPIX Example Entity 1" -sky exchange -a sha512 -len 4096 -r -ss My 152 | public static readonly X509Certificate2 Certificate1WithPublicKey = new X509Certificate2("Cert1.cer"); 153 | public static readonly X509Certificate2 Certificate2WithPublicKey = new X509Certificate2("Cert2.cer"); 154 | public static readonly X509Certificate2 Certificate3WithPublicKey = new X509Certificate2("Cert3.cer"); 155 | public static readonly X509Certificate2 Certificate4WithPublicKey = new X509Certificate2("Cert4.cer"); 156 | 157 | public static readonly X509Certificate2 Certificate1WithPrivateKey = new X509Certificate2("Cert1.pfx", "Cert1"); 158 | public static readonly X509Certificate2 Certificate2WithPrivateKey = new X509Certificate2("Cert2.pfx", "Cert2"); 159 | public static readonly X509Certificate2 Certificate3WithPrivateKey = new X509Certificate2("Cert3.pfx", "Cert3"); 160 | public static readonly X509Certificate2 Certificate4WithPrivateKey = new X509Certificate2("Cert4.pfx", "Cert4"); 161 | 162 | public static readonly X509Certificate2 WeakSha1CertificateWithPublicKey = new X509Certificate2("WeakCert_Sha1.cer"); 163 | public static readonly X509Certificate2 WeakSha1CertificateWithPrivateKey = new X509Certificate2("WeakCert_Sha1.pfx", "WeakCert_Sha1"); 164 | 165 | public static readonly X509Certificate2 WeakSmallKeyCertificateWithPublicKey = new X509Certificate2("WeakCert_SmallKey.cer"); 166 | public static readonly X509Certificate2 WeakSmallKeyCertificateWithPrivateKey = new X509Certificate2("WeakCert_SmallKey.pfx", "WeakCert_SmallKey"); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Cpix/RecipientCollection.cs: -------------------------------------------------------------------------------- 1 | using Axinom.Cpix.Internal; 2 | using System; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Security.Cryptography.X509Certificates; 6 | using System.Xml; 7 | 8 | namespace Axinom.Cpix 9 | { 10 | sealed class RecipientCollection : EntityCollection 11 | { 12 | public const string ContainerXmlElementName = "DeliveryDataList"; 13 | 14 | internal RecipientCollection(CpixDocument document) : base(document) 15 | { 16 | } 17 | 18 | internal override string ContainerName => ContainerXmlElementName; 19 | 20 | protected override XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, Recipient entity) 21 | { 22 | var recipientRsa = entity.Certificate.GetRSAPublicKey(); 23 | 24 | // Ensure that we have the document-scoped cryptographic material available. 25 | if (Document.DocumentKey == null) 26 | Document.GenerateKeys(); 27 | 28 | var encryptedDocumentKey = recipientRsa.Encrypt(Document.DocumentKey, RSAEncryptionPadding.OaepSHA1); 29 | var encryptedMacKey = recipientRsa.Encrypt(Document.MacKey, RSAEncryptionPadding.OaepSHA1); 30 | 31 | var element = new DeliveryDataElement 32 | { 33 | DeliveryKey = new DeliveryKeyElement 34 | { 35 | X509Data = new X509Data 36 | { 37 | Certificate = entity.Certificate.GetRawCertData() 38 | } 39 | }, 40 | DocumentKey = new DocumentKeyElement 41 | { 42 | Algorithm = Constants.Aes256CbcAlgorithm, 43 | Data = new DataElement 44 | { 45 | Secret = new SecretDataElement 46 | { 47 | EncryptedValue = new EncryptedXmlValue 48 | { 49 | EncryptionMethod = new EncryptionMethodDeclaration 50 | { 51 | Algorithm = Constants.RsaOaepAlgorithm 52 | }, 53 | CipherData = new CipherDataContainer 54 | { 55 | CipherValue = encryptedDocumentKey 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | MacMethod = new MacMethodElement 62 | { 63 | Algorithm = Constants.HmacSha512Algorithm, 64 | Key = new EncryptedXmlValue 65 | { 66 | EncryptionMethod = new EncryptionMethodDeclaration 67 | { 68 | Algorithm = Constants.RsaOaepAlgorithm 69 | }, 70 | CipherData = new CipherDataContainer 71 | { 72 | CipherValue = encryptedMacKey 73 | } 74 | } 75 | } 76 | }; 77 | 78 | return XmlHelpers.AppendChildAndReuseNamespaces(element, container); 79 | } 80 | 81 | protected override Recipient DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces) 82 | { 83 | // First, just extract the X.509 certificate. It must exist or we will consider the delivery data invalid. 84 | var certificateNode = element.SelectSingleNode("cpix:DeliveryKey/ds:X509Data/ds:X509Certificate", namespaces); 85 | 86 | if (certificateNode == null) 87 | throw new InvalidCpixDataException("Found a delivery data element with no X.509 certificate embedded. This is not supported."); 88 | 89 | var certificate = new X509Certificate2(Convert.FromBase64String(certificateNode.InnerText)); 90 | 91 | // If we do not already have the document secrets available, try to load them. 92 | if (Document.DocumentKey == null) 93 | { 94 | var matchingRecipientCertificate = Document.RecipientCertificates.FirstOrDefault(c => c.Thumbprint == certificate.Thumbprint); 95 | 96 | if (matchingRecipientCertificate != null) 97 | { 98 | // Yes, we have a delivery key! Use this delivery key to load the delivery data. 99 | var deliveryData = XmlHelpers.Deserialize(element); 100 | deliveryData.LoadTimeValidate(); 101 | 102 | var rsa = matchingRecipientCertificate.GetRSAPrivateKey(); 103 | var macKey = rsa.Decrypt(deliveryData.MacMethod.Key.CipherData.CipherValue, RSAEncryptionPadding.OaepSHA1); 104 | var documentKey = rsa.Decrypt(deliveryData.DocumentKey.Data.Secret.EncryptedValue.CipherData.CipherValue, RSAEncryptionPadding.OaepSHA1); 105 | 106 | Document.ImportKeys(documentKey, macKey); 107 | } 108 | } 109 | 110 | return new Recipient(certificate); 111 | } 112 | 113 | protected override void ValidateCollectionStateBeforeAdd(Recipient entity) 114 | { 115 | base.ValidateCollectionStateBeforeAdd(entity); 116 | 117 | if (AllItems.Any(i => i.Certificate == entity.Certificate || i.Certificate.Thumbprint == entity.Certificate.Thumbprint)) 118 | throw new InvalidOperationException("The collection already contains a recipient identified by the same certificate."); 119 | 120 | // If there were no recipients before and we just added one, this means that keys will from now on be encrypted. 121 | // We thus need to make sure that all keys are new keys - loaded keys that remain clear are not tolerable! 122 | if (!AllItems.Any() && Document.ContentKeys.LoadedItems.Any()) 123 | throw new InvalidOperationException("You cannot add a recipient to a CPIX document that contains loaded clear content keys. If you wish to encrypt all such keys, you must first remove and re-add them to the document to signal that intent."); 124 | } 125 | 126 | internal override void ValidateCollectionStateBeforeSave() 127 | { 128 | base.ValidateCollectionStateBeforeSave(); 129 | 130 | // If there are no recipients but the document contains loaded encrypted content keys, they will remain encrypted. 131 | if (!AllItems.Any() && Document.ContentKeys.LoadedItems.Any(key => key.IsLoadedEncryptedKey)) 132 | throw new InvalidOperationException("You cannot remove all recipients from a CPIX document that contains loaded encrypted content keys. If you wish to convert all such keys to clear keys, you must first remove and re-add them to the document to signal that intent."); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Resources/Schema/xenc-schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | ]> 12 | 13 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /Cpix/EntityCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix 8 | { 9 | /// 10 | /// A collection of entities stored in a CPIX document. 11 | /// 12 | /// 13 | /// An item added to the collection must not be modified after Add(). 14 | /// An item retrieved from the collection must not be modified, ever. 15 | /// 16 | /// Violation of these constraints may lead to undefined behavior. 17 | /// 18 | public abstract class EntityCollection : EntityCollectionBase, ICollection where TEntity : Entity 19 | { 20 | /// 21 | /// Adds a new item to the collection. 22 | /// The item will be validated and should not be modified by the caller after this. 23 | /// 24 | public void Add(TEntity item) 25 | { 26 | if (item == null) 27 | throw new ArgumentNullException(nameof(item)); 28 | 29 | VerifyNotReadOnly(); 30 | 31 | if (Contains(item)) 32 | throw new ArgumentException("The item is already in this collection."); 33 | 34 | item.ValidateNewEntity(Document); 35 | 36 | ValidateCollectionStateBeforeAdd(item); 37 | 38 | _newItems.Add(item); 39 | } 40 | 41 | public override void Clear() 42 | { 43 | VerifyNotReadOnly(); 44 | 45 | _newItems.Clear(); 46 | 47 | // We also delete any XML elements for loaded items. 48 | foreach (var data in _loadedItemsData) 49 | data.Item2.ParentNode.RemoveChild(data.Item2); 50 | 51 | _loadedItemsData.Clear(); 52 | } 53 | 54 | public bool Contains(TEntity item) => AllItems.Contains(item); 55 | 56 | public void CopyTo(TEntity[] array, int arrayIndex) 57 | { 58 | var items = AllItems.ToArray(); 59 | Array.Copy(items, 0, array, arrayIndex, items.Length); 60 | } 61 | 62 | public bool Remove(TEntity item) 63 | { 64 | VerifyNotReadOnly(); 65 | 66 | if (item == null) 67 | return false; 68 | 69 | if (_newItems.Remove(item)) 70 | return true; 71 | 72 | var loadedItemData = _loadedItemsData.SingleOrDefault(d => d.Item1 == item); 73 | 74 | if (loadedItemData == null) 75 | return false; 76 | 77 | loadedItemData.Item2.ParentNode.RemoveChild(loadedItemData.Item2); 78 | _loadedItemsData.Remove(loadedItemData); 79 | return true; 80 | } 81 | 82 | public IEnumerator GetEnumerator() => AllItems.GetEnumerator(); 83 | IEnumerator IEnumerable.GetEnumerator() => AllItems.GetEnumerator(); 84 | 85 | /// 86 | /// Gets the number of items in the collection. 87 | /// 88 | public override int Count => AllItems.Count(); 89 | 90 | #region Internal API 91 | internal IEnumerable NewItems => _newItems; 92 | internal IEnumerable LoadedItems => _loadedItemsData.Select(data => data.Item1); 93 | internal IEnumerable AllItems => LoadedItems.Concat(_newItems); 94 | 95 | protected override IEnumerable LoadedEntities => LoadedItems; 96 | protected override IEnumerable NewEntities => _newItems; 97 | 98 | internal override void SaveChanges(XmlDocument document, XmlNamespaceManager namespaces) 99 | { 100 | var containerElement = (XmlElement)document.SelectSingleNode("/cpix:CPIX/cpix:" + ContainerName, namespaces); 101 | 102 | if (Count == 0 && SignedBy.Count() == 0) 103 | { 104 | // We don't have any contents to put in it AND we don't have any signatures to apply. 105 | // This is the only scenario where we do not need the container element in the document, so remove it 106 | // and consider the save operation completed. All other paths follow the longer logic chain. 107 | 108 | if (containerElement != null) 109 | containerElement.ParentNode.RemoveChild(containerElement); 110 | 111 | return; 112 | } 113 | 114 | // We need a container element, so create one if it is missing. 115 | if (containerElement == null) 116 | { 117 | var rootElement = document.DocumentElement; 118 | 119 | // This namespace must exist, as the root element itself uses it. We will reuse the same prefix. 120 | var prefix = rootElement.GetPrefixOfNamespace(Constants.CpixNamespace); 121 | var element = document.CreateElement(prefix, ContainerName, Constants.CpixNamespace); 122 | 123 | containerElement = XmlHelpers.InsertTopLevelCpixXmlElementInCorrectOrder(element, document); 124 | } 125 | 126 | // Add any new items and then mark them as loaded items. 127 | foreach (var item in _newItems.ToArray()) 128 | { 129 | var element = SerializeEntity(document, namespaces, containerElement, item); 130 | 131 | _newItems.Remove(item); 132 | _loadedItemsData.Add(new Tuple(item, element)); 133 | } 134 | 135 | SaveNewSignatures(document, containerElement); 136 | } 137 | 138 | internal override void Load(XmlDocument document, XmlNamespaceManager namespaces) 139 | { 140 | var containerElement = document.SelectSingleNode("/cpix:CPIX/cpix:" + ContainerName, namespaces); 141 | 142 | if (containerElement == null) 143 | return; // No data. 144 | 145 | // We assume that each child element is an item in the collection (enforced by schema). 146 | foreach (XmlElement element in containerElement.ChildNodes.OfType()) 147 | { 148 | // Entities will all be validated later, when everything is loaded (to simplify reference handling). 149 | var entity = DeserializeEntity(element, namespaces); 150 | _loadedItemsData.Add(new Tuple(entity, element)); 151 | } 152 | } 153 | #endregion 154 | 155 | #region Protected API 156 | protected EntityCollection(CpixDocument document) : base(document) 157 | { 158 | } 159 | 160 | /// 161 | /// Serializes an entity into the indicated container in the XML document, returning the newly created XML element. 162 | /// 163 | protected abstract XmlElement SerializeEntity(XmlDocument document, XmlNamespaceManager namespaces, XmlElement container, TEntity entity); 164 | 165 | /// 166 | /// Deserializes an entity from an XML document, returning it. 167 | /// 168 | protected abstract TEntity DeserializeEntity(XmlElement element, XmlNamespaceManager namespaces); 169 | 170 | /// 171 | /// Performs collection-scope validation before an entity is added to the collection. 172 | /// The entity has already passed individual validation, so this just concerns "global" state validation. 173 | /// 174 | protected virtual void ValidateCollectionStateBeforeAdd(TEntity entity) 175 | { 176 | } 177 | #endregion 178 | 179 | #region Implementation details 180 | private readonly List _newItems = new List(); 181 | private readonly List> _loadedItemsData = new List>(); 182 | #endregion 183 | } 184 | } -------------------------------------------------------------------------------- /ReadmeQuickStartExamples/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | namespace Axinom.Cpix.ReadmeQuickStartExamples 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | // Here we hold the code for the quick start examples in the readme file, to validate that the code works. 12 | 13 | Console.WriteLine("EXAMPLE: Writing CPIX"); 14 | WritingCpixExample(); 15 | 16 | Console.WriteLine("EXAMPLE: Reading CPIX"); 17 | ReadingCpixExample(); 18 | 19 | Console.WriteLine("EXAMPLE: Modifying CPIX"); 20 | ModifyingCpixExample(); 21 | 22 | Console.WriteLine("EXAMPLE: Mapping content keys."); 23 | MappingContentKeysExample(); 24 | } 25 | 26 | private static void WritingCpixExample() 27 | { 28 | var document = new CpixDocument(); 29 | // Let's create a CPIX document with two content keys. 30 | 31 | document.ContentKeys.Add(new ContentKey 32 | { 33 | Id = Guid.NewGuid(), 34 | Value = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6 } 35 | }); 36 | document.ContentKeys.Add(new ContentKey 37 | { 38 | Id = Guid.NewGuid(), 39 | Value = new byte[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5 } 40 | }); 41 | 42 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert1.pfx", "Cert1")) 43 | using (var recipientCertificate = new X509Certificate2("Cert2.cer")) 44 | { 45 | // Optional: we sign the list added elements to and also the document as a whole. 46 | document.ContentKeys.AddSignature(myCertificateAndPrivateKey); 47 | document.SignedBy = myCertificateAndPrivateKey; 48 | 49 | // Optional: the presence of recipients will automatically mark the content keys to be encrypted on save. 50 | document.Recipients.Add(new Recipient(recipientCertificate)); 51 | 52 | document.Save("cpix.xml"); 53 | } 54 | } 55 | 56 | private static void ReadingCpixExample() 57 | { 58 | // A suitable input document is the one generated by the "writing CPIX" quick start example. 59 | 60 | CpixDocument document; 61 | 62 | // Optional: any private keys referenced by the certificate(s) you provide to Load() will be used for 63 | // decrypting any encrypted content keys. Even if you do not have a matching private key, the document 64 | // will still be successfully loaded but you will simply not have access to the values of the content keys. 65 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert2.pfx", "Cert2")) 66 | document = CpixDocument.Load("cpix.xml", myCertificateAndPrivateKey); 67 | 68 | if (document.ContentKeysAreReadable) 69 | Console.WriteLine("We have access to the content key values."); 70 | else 71 | Console.WriteLine("The content keys are encrypted and we do not have a delivery key."); 72 | 73 | var firstKey = document.ContentKeys.FirstOrDefault(); 74 | var firstSignerOfKeys = document.ContentKeys.SignedBy.FirstOrDefault(); 75 | 76 | if (firstKey != null) 77 | Console.WriteLine("First content key ID: " + firstKey.Id); 78 | else 79 | Console.WriteLine("No content keys in document."); 80 | 81 | if (firstSignerOfKeys != null) 82 | Console.WriteLine("Content keys first signed by: " + firstSignerOfKeys.SubjectName.Format(false)); 83 | else 84 | Console.WriteLine("The content keys collection was not signed."); 85 | 86 | if (document.SignedBy != null) 87 | Console.WriteLine("Document signed by: " + document.SignedBy.SubjectName.Format(false)); 88 | else 89 | Console.WriteLine("The document as a whole was not signed."); 90 | } 91 | 92 | private static void ModifyingCpixExample() 93 | { 94 | // Scenario: we take an input document containing some content keys and define usage rules for those keys. 95 | // A suitable input document is the one generated by the "writing CPIX" quick start example. 96 | 97 | var document = CpixDocument.Load("cpix.xml"); 98 | 99 | if (document.ContentKeys.Count() < 2) 100 | throw new Exception("This example assumes at least 2 content keys to be present in the CPIX document."); 101 | 102 | // We are modifying the document, so we must first remove any document signature. 103 | document.SignedBy = null; 104 | 105 | // We are going to add some usage rules, so remove any signature on usage rules. 106 | document.UsageRules.RemoveAllSignatures(); 107 | 108 | // If any usage rules already exist, get rid of them all. 109 | document.UsageRules.Clear(); 110 | 111 | // Assign the first content key to all audio streams. 112 | document.UsageRules.Add(new UsageRule 113 | { 114 | KeyId = document.ContentKeys.First().Id, 115 | 116 | AudioFilters = new[] { new AudioFilter() } 117 | }); 118 | 119 | // Assign the second content key to all video streams. 120 | document.UsageRules.Add(new UsageRule 121 | { 122 | KeyId = document.ContentKeys.Skip(1).First().Id, 123 | 124 | VideoFilters = new[] { new VideoFilter() } 125 | }); 126 | 127 | // Save all changes. Note that we do not sign or re-sign anything in this example (although we could). 128 | document.Save("cpix.xml"); 129 | } 130 | 131 | private static void MappingContentKeysExample() 132 | { 133 | // Scenario: we take a CPIX document with content keys and usage rules for audio and video. 134 | // Then we map these content keys to content key contexts containing audio and video that we want to encrypt. 135 | // A suitable input document is the one generated by the "modifying CPIX" quick start example. 136 | 137 | CpixDocument document; 138 | 139 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert2.pfx", "Cert2")) 140 | document = CpixDocument.Load("cpix.xml", myCertificateAndPrivateKey); 141 | 142 | if (!document.ContentKeysAreReadable) 143 | throw new Exception("The content keys were encrypted and we did not have a delivery key."); 144 | 145 | // Let's imagine we have stereo audio at 32 kbps. 146 | var audioKey = document.ResolveContentKey(new ContentKeyContext 147 | { 148 | Type = ContentKeyContextType.Audio, 149 | 150 | Bitrate = 32 * 1000, 151 | AudioChannelCount = 2 152 | }); 153 | 154 | // Let's imagine we have both SD and HD video. 155 | var sdVideoKey = document.ResolveContentKey(new ContentKeyContext 156 | { 157 | Type = ContentKeyContextType.Video, 158 | 159 | Bitrate = 1 * 1000 * 1000, 160 | PicturePixelCount = 640 * 480, 161 | WideColorGamut = false, 162 | HighDynamicRange = false, 163 | VideoFramesPerSecond = 30 164 | }); 165 | 166 | var hdVideoKey = document.ResolveContentKey(new ContentKeyContext 167 | { 168 | Type = ContentKeyContextType.Video, 169 | 170 | Bitrate = 4 * 1000 * 1000, 171 | PicturePixelCount = 1920 * 1080, 172 | WideColorGamut = false, 173 | HighDynamicRange = false, 174 | VideoFramesPerSecond = 30 175 | }); 176 | 177 | Console.WriteLine("Key to use for audio: " + audioKey.Id); 178 | Console.WriteLine("Key to use for SD video: " + sdVideoKey.Id); 179 | Console.WriteLine("Key to use for HD video: " + hdVideoKey.Id); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Cpix/EntityCollectionBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography.X509Certificates; 5 | using System.Xml; 6 | 7 | namespace Axinom.Cpix 8 | { 9 | /// 10 | /// Base class for entity collections. This contains all the functionality that does not depend 11 | /// on the specific type of entity contained in the collection but is valid for all collections. 12 | /// 13 | public abstract class EntityCollectionBase 14 | { 15 | /// 16 | /// Gets the count of items in the collection. 17 | /// 18 | public abstract int Count { get; } 19 | 20 | /// 21 | /// Removes all items from the collection. 22 | /// 23 | public abstract void Clear(); 24 | 25 | /// 26 | /// Gets whether the collection is read-only. Always returns true if the entire document is read-only. 27 | /// 28 | /// The collection is read-only if you are dealing with a loaded CPIX document that contains signatures covering this 29 | /// collection. Remove any collection-scoped signatures and document-scoped signatures to make the collection writable. 30 | /// 31 | public bool IsReadOnly => Document.IsReadOnly || _loadedSignatures.Any(); 32 | 33 | /// 34 | /// Applies a digital signature to the collection. 35 | /// 36 | /// The signature is generated when the document is saved, so you can still modify the collection after this call. 37 | /// 38 | public void AddSignature(X509Certificate2 signerCertificate) 39 | { 40 | if (signerCertificate == null) 41 | throw new ArgumentNullException(nameof(signerCertificate)); 42 | 43 | // Cannot add signatures to the collection if the document itself is signed! 44 | Document.VerifyIsNotReadOnly(); 45 | 46 | if (SignedBy.Contains(signerCertificate)) 47 | throw new InvalidOperationException("The collection is already signed by this identity."); 48 | 49 | CryptographyHelpers.ValidateSignerCertificate(signerCertificate); 50 | 51 | _newSigners.Add(signerCertificate); 52 | } 53 | 54 | /// 55 | /// Gets the certificates of the identities that have signed this collection. 56 | /// 57 | public IEnumerable SignedBy => LoadedSigners.Concat(_newSigners); 58 | 59 | /// 60 | /// Removes all digital signatures that apply to this collection. 61 | /// 62 | public void RemoveAllSignatures() 63 | { 64 | foreach (var signature in _loadedSignatures) 65 | signature.Item1.ParentNode.RemoveChild(signature.Item1); 66 | 67 | _loadedSignatures.Clear(); 68 | _newSigners.Clear(); 69 | } 70 | 71 | #region Internal API 72 | /// 73 | /// Undecorated name of the XML element that serves as the container for this collection. 74 | /// 75 | internal abstract string ContainerName { get; } 76 | 77 | /// 78 | /// Saves any changes in this entity set to the supplied document. 79 | /// 80 | internal abstract void SaveChanges(XmlDocument document, XmlNamespaceManager namespaces); 81 | 82 | /// 83 | /// Loads the entity set from the supplied XML document. 84 | /// 85 | internal abstract void Load(XmlDocument document, XmlNamespaceManager namespaces); 86 | 87 | internal void ImportLoadedSignature(XmlElement signature, X509Certificate2 certificate) 88 | { 89 | _loadedSignatures.Add(new Tuple(signature, certificate)); 90 | } 91 | 92 | /// 93 | /// Performs validation of the collection and its contents before it is saved. 94 | /// 95 | internal virtual void ValidateCollectionStateBeforeSave() 96 | { 97 | ValidateEntitiesBeforeSave(); 98 | } 99 | 100 | /// 101 | /// Performs validation of the collection and its contents after it has been loaded. 102 | /// 103 | internal virtual void ValidateCollectionStateAfterLoad() 104 | { 105 | ValidateEntitiesAfterLoad(); 106 | } 107 | #endregion 108 | 109 | #region Protected API 110 | protected CpixDocument Document { get; } 111 | 112 | protected abstract IEnumerable LoadedEntities { get; } 113 | protected abstract IEnumerable NewEntities { get; } 114 | 115 | protected IEnumerable NewSigners => _newSigners; 116 | protected IEnumerable LoadedSigners => _loadedSignatures.Select(s => s.Item2); 117 | 118 | protected EntityCollectionBase(CpixDocument document) 119 | { 120 | if (document == null) 121 | throw new ArgumentNullException(nameof(document)); 122 | 123 | Document = document; 124 | } 125 | 126 | /// 127 | /// Throws an exception if the collection is read-only. 128 | /// 129 | /// You should first verify that the document is not read-only, to provide most specific user feedback. 130 | /// 131 | protected void VerifyNotReadOnly() 132 | { 133 | if (!IsReadOnly) 134 | return; 135 | 136 | throw new InvalidOperationException("The entity collection is read-only. You must remove or re-apply any digital signatures on the collection to make it writable."); 137 | } 138 | 139 | protected void SaveNewSignatures(XmlDocument document, XmlElement containerElement) 140 | { 141 | if (!NewSigners.Any()) 142 | return; 143 | 144 | // We need an ID on the element to sign it, so let's give it the same ID as its name. 145 | // Unless it already has an ID, of course, in which case use the existing one. 146 | var elementId = containerElement.GetAttribute("id"); 147 | 148 | if (elementId == "") 149 | { 150 | containerElement.SetAttribute("id", ContainerName); 151 | elementId = ContainerName; 152 | } 153 | 154 | // Add any signatures and mark them as applied. 155 | foreach (var signer in _newSigners.ToArray()) 156 | { 157 | var signature = CryptographyHelpers.SignXmlElement(document, elementId, signer); 158 | 159 | _newSigners.Remove(signer); 160 | _loadedSignatures.Add(new Tuple(signature, signer)); 161 | } 162 | } 163 | #endregion 164 | 165 | #region Implementation details 166 | private readonly List _newSigners = new List(); 167 | private readonly List> _loadedSignatures = new List>(); 168 | 169 | /// 170 | /// Performs validation of the collection's contents before it is saved. 171 | /// 172 | private void ValidateEntitiesBeforeSave() 173 | { 174 | // Just individually validate each new item. 175 | foreach (var item in NewEntities) 176 | item.ValidateNewEntity(Document); 177 | 178 | // No need to validate any loaded entities, as we won't save them even if modified. 179 | } 180 | 181 | /// 182 | /// Performs validation of the collection's contents after it has been loaded. 183 | /// 184 | private void ValidateEntitiesAfterLoad() 185 | { 186 | foreach (var item in LoadedEntities) 187 | item.ValidateLoadedEntity(Document); 188 | } 189 | #endregion 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Axinom CPIX library 2 | =================== 3 | 4 | A .NET Standard library for working with CPIX (Content Protection Information Exchange) documents, as defined by [DASH Industry Forum](http://dashif.org/guidelines/). This library implements CPIX version 2.3. 5 | 6 | Installation 7 | ============ 8 | 9 | The library is available from nuget.org as [Axinom.Cpix](https://www.nuget.org/packages/Axinom.Cpix/). 10 | 11 | Supported platforms are: 12 | 13 | * Any platform that supports .NET Standard 2.0. 14 | 15 | Features 16 | ======== 17 | 18 | The following features are implemented: 19 | 20 | * Content key save/load 21 | * Supports 128-bit and 256-bit keys 22 | * Usage rule save/load 23 | * Resolving content keys based on usage rules 24 | * Encryption of content keys (optional) 25 | * Decryption of content keys 26 | * Authenticated encryption (mandatory if encryption is used) 27 | * Signing of content keys 28 | * Signing of usage rules 29 | * Signing of the document 30 | * Automatic verification of all signatures 31 | * Modification of existing document without having access to a decryption key 32 | * Modification of existing document without invalidating signatures 33 | * Automatic document validation against CPIX XML schema 34 | * Delivery key identification based on X.509 certificates. 35 | * DRM system metadata 36 | 37 | The following features are partially implemented: 38 | 39 | * Key periods and key period filters 40 | * Read/write and basic validation. Content key resolution not implemented. 41 | 42 | The following features are NOT implemented: 43 | 44 | * Document update history 45 | * Minor metadata attributes (names/IDs/etc) 46 | * Hierarchical content keys 47 | 48 | Documents containing unimplemented features can still be processed - the unknown elements will simply be ignored on load and passed through without modification on save. 49 | 50 | Cryptography 51 | ============ 52 | 53 | The set of supported cryptographic algorithms is: 54 | 55 | * RSA-OAEP-MGF1-SHA1 - for delivery key and MAC key encryption. 56 | * AES-256-CBC with PKCS#7 padding - for content key encryption. 57 | * HMAC-SHA512 - for encrypted content key MAC. 58 | * RSASSA-PKCS1-v1_5 - for digital signatures. 59 | * SHA-512 - for digital signature digest calculation. 60 | 61 | Documents using other algorithms may fail to load. 62 | 63 | The MAC key size must be exactly 512 bits. 64 | 65 | The following requirements are placed on any X.509 certificates used by the library: 66 | 67 | * Only RSA key pairs are supported. 68 | * RSA key size must be at least 3072 bits. 69 | * Signing algorithm must not be SHA-1. 70 | 71 | Quick start: writing CPIX 72 | ========================= 73 | 74 | ```C# 75 | var document = new CpixDocument(); 76 | // Let's create a CPIX document with two content keys. 77 | 78 | document.ContentKeys.Add(new ContentKey 79 | { 80 | Id = Guid.NewGuid(), 81 | Value = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6 } 82 | }); 83 | document.ContentKeys.Add(new ContentKey 84 | { 85 | Id = Guid.NewGuid(), 86 | Value = new byte[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5 } 87 | }); 88 | 89 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert1.pfx", "Cert1")) 90 | using (var recipientCertificate = new X509Certificate2("Cert2.cer")) 91 | { 92 | // Optional: we sign the list added elements to and also the document as a whole. 93 | document.ContentKeys.AddSignature(myCertificateAndPrivateKey); 94 | document.SignedBy = myCertificateAndPrivateKey; 95 | 96 | // Optional: the presence of recipients will automatically mark the content keys to be encrypted on save. 97 | document.Recipients.Add(new Recipient(recipientCertificate)); 98 | 99 | document.Save("cpix.xml"); 100 | } 101 | ``` 102 | 103 | Quick start: reading CPIX 104 | ========================= 105 | 106 | ```C# 107 | // A suitable input document is the one generated by the "writing CPIX" quick start example. 108 | 109 | CpixDocument document; 110 | 111 | // Optional: any private keys referenced by the certificate(s) you provide to Load() will be used for 112 | // decrypting any encrypted content keys. Even if you do not have a matching private key, the document 113 | // will still be successfully loaded but you will simply not have access to the values of the content keys. 114 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert2.pfx", "Cert2")) 115 | document = CpixDocument.Load("cpix.xml", myCertificateAndPrivateKey); 116 | 117 | if (document.ContentKeysAreReadable) 118 | Console.WriteLine("We have access to the content key values."); 119 | else 120 | Console.WriteLine("The content keys are encrypted and we do not have a delivery key."); 121 | 122 | var firstKey = document.ContentKeys.FirstOrDefault(); 123 | var firstSignerOfKeys = document.ContentKeys.SignedBy.FirstOrDefault(); 124 | 125 | if (firstKey != null) 126 | Console.WriteLine("First content key ID: " + firstKey.Id); 127 | else 128 | Console.WriteLine("No content keys in document."); 129 | 130 | if (firstSignerOfKeys != null) 131 | Console.WriteLine("Content keys first signed by: " + firstSignerOfKeys.SubjectName.Format(false)); 132 | else 133 | Console.WriteLine("The content keys collection was not signed."); 134 | 135 | if (document.SignedBy != null) 136 | Console.WriteLine("Document signed by: " + document.SignedBy.SubjectName.Format(false)); 137 | else 138 | Console.WriteLine("The document as a whole was not signed."); 139 | ``` 140 | 141 | Quick start: modifying CPIX 142 | ========================= 143 | 144 | ```C# 145 | // Scenario: we take an input document containing some content keys and define usage rules for those keys. 146 | // A suitable input document is the one generated by the "writing CPIX" quick start example. 147 | 148 | var document = CpixDocument.Load("cpix.xml"); 149 | 150 | if (document.ContentKeys.Count() < 2) 151 | throw new Exception("This example assumes at least 2 content keys to be present in the CPIX document."); 152 | 153 | // We are modifying the document, so we must first remove any document signature. 154 | document.SignedBy = null; 155 | 156 | // We are going to add some usage rules, so remove any signature on usage rules. 157 | document.UsageRules.RemoveAllSignatures(); 158 | 159 | // If any usage rules already exist, get rid of them all. 160 | document.UsageRules.Clear(); 161 | 162 | // Assign the first content key to all audio streams. 163 | document.UsageRules.Add(new UsageRule 164 | { 165 | KeyId = document.ContentKeys.First().Id, 166 | 167 | AudioFilters = new[] { new AudioFilter() } 168 | }); 169 | 170 | // Assign the second content key to all video streams. 171 | document.UsageRules.Add(new UsageRule 172 | { 173 | KeyId = document.ContentKeys.Skip(1).First().Id, 174 | 175 | VideoFilters = new[] { new VideoFilter() } 176 | }); 177 | 178 | // Save all changes. Note that we do not sign or re-sign anything in this example (although we could). 179 | document.Save("cpix.xml"); 180 | ``` 181 | 182 | Quick start: mapping content keys 183 | ================================= 184 | 185 | ```C# 186 | // Scenario: we take a CPIX document with content keys and usage rules for audio and video. 187 | // Then we map these content keys to content key contexts containing audio and video that we want to encrypt. 188 | // A suitable input document is the one generated by the "modifying CPIX" quick start example. 189 | 190 | CpixDocument document; 191 | 192 | using (var myCertificateAndPrivateKey = new X509Certificate2("Cert2.pfx", "Cert2")) 193 | document = CpixDocument.Load("cpix.xml", myCertificateAndPrivateKey); 194 | 195 | if (!document.ContentKeysAreReadable) 196 | throw new Exception("The content keys were encrypted and we did not have a delivery key."); 197 | 198 | // Let's imagine we have stereo audio at 32 kbps. 199 | var audioKey = document.ResolveContentKey(new ContentKeyContext 200 | { 201 | Type = ContentKeyContextType.Audio, 202 | 203 | Bitrate = 32 * 1000, 204 | AudioChannelCount = 2 205 | }); 206 | 207 | // Let's imagine we have both SD and HD video. 208 | var sdVideoKey = document.ResolveContentKey(new ContentKeyContext 209 | { 210 | Type = ContentKeyContextType.Video, 211 | 212 | Bitrate = 1 * 1000 * 1000, 213 | PicturePixelCount = 640 * 480, 214 | WideColorGamut = false, 215 | HighDynamicRange = false, 216 | VideoFramesPerSecond = 30 217 | }); 218 | 219 | var hdVideoKey = document.ResolveContentKey(new ContentKeyContext 220 | { 221 | Type = ContentKeyContextType.Video, 222 | 223 | Bitrate = 4 * 1000 * 1000, 224 | PicturePixelCount = 1920 * 1080, 225 | WideColorGamut = false, 226 | HighDynamicRange = false, 227 | VideoFramesPerSecond = 30 228 | }); 229 | 230 | Console.WriteLine("Key to use for audio: " + audioKey.Id); 231 | Console.WriteLine("Key to use for SD video: " + sdVideoKey.Id); 232 | Console.WriteLine("Key to use for HD video: " + hdVideoKey.Id); 233 | ``` -------------------------------------------------------------------------------- /Cpix/XmlHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | 9 | namespace Axinom.Cpix 10 | { 11 | static class XmlHelpers 12 | { 13 | /// 14 | /// XML-deserializes an object of type T from an XmlElement. 15 | /// 16 | internal static T Deserialize(XmlElement element) 17 | { 18 | using (var buffer = new MemoryStream()) 19 | { 20 | using (var writer = XmlWriter.Create(buffer)) 21 | element.WriteTo(writer); 22 | 23 | buffer.Position = 0; 24 | 25 | var serializer = new XmlSerializer(typeof(T)); 26 | return (T)serializer.Deserialize(buffer); 27 | } 28 | } 29 | 30 | /// 31 | /// Appends a child element XML-serialized from an object of type T and reuses already-declared namespaces in doing so. 32 | /// 33 | internal static XmlElement AppendChildAndReuseNamespaces(T xmlObject, XmlElement parent) 34 | { 35 | // We have a little problem here. See, XmlSerializer generates full documents, which means that 36 | // it will declare the namespaces on absolutely everything it genreates. This causes a lot of 37 | // redundancy and spam. We need to reuse the namespaces as far as possible. 38 | 39 | // The navigator gives us access to the namespaces that are in scope at the parent element. 40 | var parentNavigator = parent.CreateNavigator(); 41 | var namespaces = parentNavigator.GetNamespacesInScope(XmlNamespaceScope.ExcludeXml); 42 | 43 | var serialized = CreateXmlElementAndReuseNamespaces(xmlObject, namespaces); 44 | 45 | return (XmlElement)parent.AppendChild(parent.OwnerDocument.ImportNode(serialized, true)); 46 | } 47 | 48 | /// 49 | /// Transforms an object of type T to an XmlElement via XML-serialization, 50 | /// reusing existing namespaces instead of re-declaring them. 51 | /// 52 | internal static XmlElement CreateXmlElementAndReuseNamespaces(T xmlObject, IDictionary namespacesToReuse) 53 | { 54 | if (xmlObject == null) 55 | throw new ArgumentNullException(nameof(xmlObject)); 56 | 57 | if (namespacesToReuse == null) 58 | throw new ArgumentNullException(nameof(namespacesToReuse)); 59 | 60 | using (var intermediateXmlBuffer = new MemoryStream()) 61 | { 62 | var serializer = new XmlSerializer(typeof(T)); 63 | 64 | // We create a temporary container element for serializing the object. 65 | // This container element will delcare all the relevant namespaces exactly 66 | // the same as they exist in the scope of the to-be-parent-element (provided 67 | // in namespacesToReuse) without re-defining them as is normal behavior. 68 | // Then we just extract the XmlElement and discard the container. 69 | // Very roundabout way but it is pretty much the only way to achieve desired behavior! 70 | 71 | using (var writer = XmlWriter.Create(intermediateXmlBuffer, new XmlWriterSettings 72 | { 73 | Encoding = Encoding.UTF8, 74 | CloseOutput = false, 75 | 76 | // We will be lazy and let the writer close up the container. 77 | WriteEndDocumentOnClose = true 78 | })) 79 | { 80 | var namespaces = new XmlSerializerNamespaces(); 81 | 82 | // Dummy namespace used for the container itself, to avoid conflicts. 83 | // Hardcoded value but likelyhood of conflict is zero, barring deliberately designed conflicting data. 84 | writer.WriteStartElement("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Container", "http://dummy.example.com"); 85 | 86 | foreach (var ns in namespacesToReuse) 87 | { 88 | namespaces.Add(ns.Key, ns.Value); 89 | 90 | if (string.IsNullOrEmpty(ns.Key)) 91 | { 92 | // Default namespace. 93 | writer.WriteAttributeString(null, "xmlns", Constants.XmlnsNamespace, ns.Value); 94 | } 95 | else 96 | { 97 | // Prefixed namespace. 98 | writer.WriteAttributeString("xmlns", ns.Key, Constants.XmlnsNamespace, ns.Value); 99 | } 100 | } 101 | 102 | // This will reuse all the namespaces we have inherited. 103 | serializer.Serialize(writer, xmlObject, namespaces); 104 | } 105 | 106 | // Seek back to beginning to load contents into XmlDocument. 107 | intermediateXmlBuffer.Position = 0; 108 | 109 | var xmlDocument = new XmlDocument(); 110 | xmlDocument.Load(intermediateXmlBuffer); 111 | 112 | // Rip out the actual element from the container and return it. 113 | return xmlDocument.DocumentElement.ChildNodes.OfType().Single(); 114 | } 115 | } 116 | 117 | /// 118 | /// XML-serializes an object of type T, returning it as an XmlDocument. 119 | /// 120 | internal static XmlDocument Serialize(T xmlObject) 121 | { 122 | using (var intermediateXmlBuffer = new MemoryStream()) 123 | { 124 | var serializer = new XmlSerializer(typeof(T)); 125 | serializer.Serialize(intermediateXmlBuffer, xmlObject); 126 | 127 | // Seek back to beginning to load contents into XmlDocument. 128 | intermediateXmlBuffer.Position = 0; 129 | 130 | var xmlDocument = new XmlDocument(); 131 | xmlDocument.Load(intermediateXmlBuffer); 132 | 133 | return xmlDocument; 134 | } 135 | } 136 | 137 | /// 138 | /// Top-level CPIX elements must be inserted to the document in a specific order, as the CPIX document 139 | /// is strictly ordered. This method will automatically insert elements in the correct order. 140 | /// 141 | internal static XmlElement InsertTopLevelCpixXmlElementInCorrectOrder(XmlElement element, XmlDocument document) 142 | { 143 | // If this returns null, our element should be the first one. 144 | var insertAfter = TryDetectInsertAfterElementForCpixTopLevelInsertion(element, document); 145 | 146 | if (insertAfter == null) 147 | return (XmlElement)document.DocumentElement.PrependChild(element); 148 | else 149 | return (XmlElement)document.DocumentElement.InsertAfter(element, insertAfter); 150 | } 151 | 152 | private static XmlElement TryDetectInsertAfterElementForCpixTopLevelInsertion(XmlElement element, XmlDocument document) 153 | { 154 | // Top-level elements have a specific order they need to be in! We insert in the appropriate order. 155 | 156 | var theseComeBefore = Constants.TopLevelXmlElementOrder 157 | .TakeWhile(item => item.Item1 != element.LocalName || item.Item2 != element.NamespaceURI) 158 | .ToArray(); 159 | 160 | // If we got everything, this means the current element is unknown and we have a defect! 161 | if (theseComeBefore.Length == Constants.TopLevelXmlElementOrder.Length) 162 | throw new ArgumentException("The correct ordering of this element in a CPIX document cannot be determined as the element is unknown.", nameof(element)); 163 | 164 | if (theseComeBefore.Length == 0) 165 | return null; // This is the first element. 166 | 167 | // If there already exist elements of the same type, add after the latest of the same type. 168 | var insertAfter = document.DocumentElement.ChildNodes.OfType() 169 | .LastOrDefault(e => e.LocalName == element.LocalName && e.NamespaceURI == element.NamespaceURI); 170 | 171 | if (insertAfter != null) 172 | return insertAfter; 173 | 174 | // Otherwise, add after whatever we detect should come right before us. 175 | // We start scanning from the last one, obviously. 176 | foreach (var candidate in theseComeBefore.Reverse()) 177 | { 178 | insertAfter = document.DocumentElement.ChildNodes.OfType() 179 | .LastOrDefault(e => e.LocalName == candidate.Item1 && e.NamespaceURI == candidate.Item2); 180 | 181 | if (insertAfter != null) 182 | return insertAfter; 183 | } 184 | 185 | // None of the "come before" exist? Then our element is the first one! 186 | return null; 187 | } 188 | 189 | /// 190 | /// Loads, reformats/indents and saves an XML document. Used primarily just for testing with formatted XML. 191 | /// 192 | internal static void PrettyPrintXml(Stream input, Stream output) 193 | { 194 | var document = new XmlDocument(); 195 | document.PreserveWhitespace = true; 196 | document.Load(input); 197 | 198 | using (var writer = XmlWriter.Create(output, new XmlWriterSettings 199 | { 200 | Encoding = Encoding.UTF8, 201 | Indent = true, 202 | IndentChars = "\t", 203 | CloseOutput = false 204 | })) 205 | { 206 | document.Save(writer); 207 | } 208 | } 209 | 210 | /// 211 | /// Creates a namespace manager with some convenient namespaces predefined. 212 | /// 213 | internal static XmlNamespaceManager CreateCpixNamespaceManager(XmlDocument document) 214 | { 215 | var manager = new XmlNamespaceManager(document.NameTable); 216 | manager.AddNamespace("cpix", Constants.CpixNamespace); 217 | manager.AddNamespace("pskc", Constants.PskcNamespace); 218 | manager.AddNamespace("enc", Constants.XmlEncryptionNamespace); 219 | manager.AddNamespace("ds", Constants.XmlDigitalSignatureNamespace); 220 | 221 | return manager; 222 | } 223 | 224 | /// 225 | /// Adds a namespace declaration attribute. 226 | /// 227 | internal static void DeclareNamespace(XmlElement onElement, string prefix, string ns) 228 | { 229 | var attribute = onElement.OwnerDocument.CreateAttribute("xmlns", prefix, Constants.XmlnsNamespace); 230 | attribute.Value = ns; 231 | onElement.Attributes.Append(attribute); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Tests/ContentKeyCrudTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Xml; 5 | using Xunit; 6 | 7 | namespace Axinom.Cpix.Tests 8 | { 9 | public sealed class ContentKeyCrudTests 10 | { 11 | [Fact] 12 | public void AddContentKey_WithLoadedEmptyDocument_Succeeds() 13 | { 14 | var document = new CpixDocument(); 15 | document = TestHelpers.Reload(document); 16 | 17 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 18 | document = TestHelpers.Reload(document); 19 | 20 | Assert.Single(document.ContentKeys); 21 | } 22 | 23 | [Fact] 24 | public void AddContentKey_WithLoadedDocumentAndExistingContentKey_Succeeds() 25 | { 26 | var document = new CpixDocument(); 27 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 28 | document = TestHelpers.Reload(document); 29 | 30 | document.ContentKeys.Add(TestHelpers.GenerateContentKey()); 31 | document = TestHelpers.Reload(document); 32 | 33 | Assert.Equal(2, document.ContentKeys.Count); 34 | } 35 | 36 | [Fact] 37 | public void AddContentKey_WithVariousValidData_Succeeds() 38 | { 39 | var document = new CpixDocument(); 40 | 41 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 42 | { 43 | Id = Guid.NewGuid(), 44 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 45 | }))); 46 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 47 | { 48 | Id = Guid.NewGuid(), 49 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.Last()], 50 | }))); 51 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 52 | { 53 | Id = Guid.NewGuid(), 54 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 55 | ExplicitIv = new byte[Constants.ContentKeyExplicitIvLengthInBytes] 56 | }))); 57 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 58 | { 59 | Id = Guid.NewGuid(), 60 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 61 | ExplicitIv = null 62 | }))); 63 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 64 | { 65 | Id = Guid.NewGuid(), 66 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 67 | CommonEncryptionScheme = "cenc" 68 | }))); 69 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 70 | { 71 | Id = Guid.NewGuid(), 72 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 73 | CommonEncryptionScheme = "cens" 74 | }))); 75 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 76 | { 77 | Id = Guid.NewGuid(), 78 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 79 | CommonEncryptionScheme = "cbc1" 80 | }))); 81 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 82 | { 83 | Id = Guid.NewGuid(), 84 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 85 | CommonEncryptionScheme = "cbcs" 86 | }))); 87 | Assert.Null(Record.Exception(() => document.ContentKeys.Add(new ContentKey 88 | { 89 | Id = Guid.NewGuid(), 90 | Value = null 91 | }))); 92 | } 93 | 94 | [Fact] 95 | public void AddContentKey_WithVariousInvalidData_Fails() 96 | { 97 | var document = new CpixDocument(); 98 | 99 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 100 | { 101 | Id = Guid.Empty, 102 | Value = new byte[16] 103 | })); 104 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 105 | { 106 | Id = Guid.NewGuid(), 107 | Value = new byte[15] 108 | })); 109 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 110 | { 111 | Id = Guid.NewGuid(), 112 | Value = new byte[17] 113 | })); 114 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 115 | { 116 | Id = Guid.NewGuid(), 117 | Value = new byte[0] 118 | })); 119 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 120 | { 121 | Id = Guid.NewGuid(), 122 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 123 | ExplicitIv = new byte[Constants.ContentKeyExplicitIvLengthInBytes + 1] 124 | })); 125 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 126 | { 127 | Id = Guid.NewGuid(), 128 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 129 | ExplicitIv = new byte[0] 130 | })); 131 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 132 | { 133 | Id = Guid.NewGuid(), 134 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 135 | CommonEncryptionScheme = "" 136 | })); 137 | Assert.Throws(() => document.ContentKeys.Add(new ContentKey 138 | { 139 | Id = Guid.NewGuid(), 140 | Value = new byte[Constants.ValidContentKeyLengthsInBytes.First()], 141 | CommonEncryptionScheme = "abcd" 142 | })); 143 | } 144 | 145 | [Fact] 146 | public void Save_WithNullContentKeyValue_Succeeds() 147 | { 148 | var contentKey = TestHelpers.GenerateContentKey(); 149 | 150 | // Make the content key value null. 151 | contentKey.Value = null; 152 | 153 | var document = new CpixDocument(); 154 | document.ContentKeys.Add(contentKey); 155 | 156 | // CPIX document serialization should allow null content key values. 157 | Assert.Null(Record.Exception(() => document.Save(new MemoryStream()))); 158 | 159 | var buffer = new MemoryStream(); 160 | document.Save(buffer); 161 | buffer.Position = 0; 162 | 163 | var actualDocument = CpixDocument.Load(buffer); 164 | 165 | Assert.Single(actualDocument.ContentKeys); 166 | Assert.Null(actualDocument.ContentKeys.First().Value); 167 | } 168 | 169 | [Fact] 170 | public void Save_WithSneakilyCorruptedContentKey_Fails() 171 | { 172 | var contentKey = TestHelpers.GenerateContentKey(); 173 | 174 | var document = new CpixDocument(); 175 | // It will be validated here. 176 | document.ContentKeys.Add(contentKey); 177 | 178 | // Corrupt it after validation! 179 | contentKey.Value = new byte[5]; 180 | 181 | // The corruption should still be caught. 182 | Assert.Throws(() => document.Save(new MemoryStream())); 183 | } 184 | 185 | [Fact] 186 | public void AddContentKey_Twice_Fails() 187 | { 188 | var contentKey = TestHelpers.GenerateContentKey(); 189 | 190 | var document = new CpixDocument(); 191 | document.ContentKeys.Add(contentKey); 192 | 193 | // Same instance. 194 | Assert.Throws(() => document.ContentKeys.Add(contentKey)); 195 | 196 | // Same ID but different instance. 197 | Assert.ThrowsAny(() => document.ContentKeys.Add(new ContentKey 198 | { 199 | Id = contentKey.Id, 200 | Value = contentKey.Value 201 | })); 202 | } 203 | 204 | [Fact] 205 | public void RemoveContentKey_WithNewWritableCollection_Succeeds() 206 | { 207 | var contentKey = TestHelpers.GenerateContentKey(); 208 | 209 | var document = new CpixDocument(); 210 | document.ContentKeys.Add(contentKey); 211 | document.ContentKeys.Remove(contentKey); 212 | } 213 | 214 | [Fact] 215 | public void RemoveContentKey_WithLoadedWritableCollection_Succeeds() 216 | { 217 | var contentKey = TestHelpers.GenerateContentKey(); 218 | 219 | var document = new CpixDocument(); 220 | document.ContentKeys.Add(contentKey); 221 | 222 | document = TestHelpers.Reload(document); 223 | 224 | document.ContentKeys.Remove(document.ContentKeys.Single()); 225 | } 226 | 227 | [Fact] 228 | public void RemoveContentKey_WithUnknownContentKey_Succeeds() 229 | { 230 | var contentKey = TestHelpers.GenerateContentKey(); 231 | 232 | var document = new CpixDocument(); 233 | document.ContentKeys.Remove(contentKey); 234 | } 235 | 236 | [Fact] 237 | public void RoundTrip_WithSignedCollection_Succeeds() 238 | { 239 | var contentKey = TestHelpers.GenerateContentKey(); 240 | 241 | var document = new CpixDocument(); 242 | document.ContentKeys.Add(contentKey); 243 | document.ContentKeys.AddSignature(TestHelpers.Certificate1WithPrivateKey); 244 | 245 | document = TestHelpers.Reload(document); 246 | 247 | Assert.Single(document.ContentKeys); 248 | } 249 | 250 | [Fact] 251 | public void RoundTrip_WithSpecifyingOptionalData_LoadsExpectedKey() 252 | { 253 | var contentKey = new ContentKey 254 | { 255 | Id = Guid.NewGuid(), 256 | Value = new byte[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, 257 | ExplicitIv = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } 258 | }; 259 | 260 | var document = new CpixDocument(); 261 | document.ContentKeys.Add(contentKey); 262 | 263 | document = TestHelpers.Reload(document); 264 | 265 | var loadedKey = document.ContentKeys.Single(); 266 | 267 | Assert.Single(document.ContentKeys); 268 | Assert.Equal(contentKey.Id, loadedKey.Id); 269 | Assert.Equal(contentKey.Value, loadedKey.Value); 270 | Assert.Equal(contentKey.ExplicitIv, loadedKey.ExplicitIv); 271 | } 272 | 273 | [Fact] 274 | public void RoundTrip_WithoutSpecifyingOptionalData_LoadsExpectedKey() 275 | { 276 | var contentKey = new ContentKey 277 | { 278 | Id = Guid.NewGuid(), 279 | Value = new byte[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 } 280 | }; 281 | 282 | var document = new CpixDocument(); 283 | document.ContentKeys.Add(contentKey); 284 | 285 | document = TestHelpers.Reload(document); 286 | 287 | var loadedKey = document.ContentKeys.Single(); 288 | 289 | Assert.Single(document.ContentKeys); 290 | Assert.Equal(contentKey.Id, loadedKey.Id); 291 | Assert.Equal(contentKey.Value, loadedKey.Value); 292 | Assert.Null(loadedKey.ExplicitIv); 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /Cpix/DrmSignalingHelpers.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Xml.Linq; 6 | using shaka.media; 7 | 8 | namespace Axinom.Cpix 9 | { 10 | /// 11 | /// Helpers for generating DRM signaling data in common scenarios. These are primarily meant for generating 12 | /// realistic example data but may also be suitable for production scenarios. The flexibility can be rather limited, 13 | /// though. 14 | /// 15 | /// You can use the decoders at https://tools.axinom.com/ to verify the generated structures are valid. 16 | /// 17 | /// No guarantees are made about API compatibility here - this is left public mostly because it is annoying 18 | /// code to write and if we can help someone out by having them use this tested implementation, so be it. 19 | /// 20 | public static class DrmSignalingHelpers 21 | { 22 | public static readonly Guid FairPlaySystemId = new Guid("94ce86fb-07ff-4f43-adB8-93d2fa968ca2"); 23 | public static readonly Guid PlayReadySystemId = new Guid("9a04f079-9840-4286-ab92-e65be0885f95"); 24 | public static readonly Guid WidevineSystemId = new Guid("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); 25 | 26 | /// 27 | /// Adds default DRM system signaling entries for all keys. 28 | /// 29 | public static void AddDefaultSignalingForAllKeys(CpixDocument document) 30 | { 31 | foreach (var key in document.ContentKeys) 32 | { 33 | document.DrmSystems.Add(new DrmSystem 34 | { 35 | SystemId = WidevineSystemId, 36 | KeyId = key.Id, 37 | ContentProtectionData = GenerateWidevineDashSignaling(key.Id), 38 | HlsSignalingData = new HlsSignalingData 39 | { 40 | MasterPlaylistData = GenerateWidevineHlsMasterPlaylistSignaling(key.Id), 41 | MediaPlaylistData = GenerateWidevineHlsMediaPlaylistSignaling(key.Id), 42 | } 43 | }); 44 | document.DrmSystems.Add(new DrmSystem 45 | { 46 | SystemId = PlayReadySystemId, 47 | KeyId = key.Id, 48 | ContentProtectionData = GeneratePlayReadyDashSignaling(key.Id), 49 | SmoothStreamingProtectionHeaderData = GeneratePlayReadyMssSignaling(key.Id) 50 | }); 51 | document.DrmSystems.Add(new DrmSystem 52 | { 53 | SystemId = FairPlaySystemId, 54 | KeyId = key.Id, 55 | HlsSignalingData = new HlsSignalingData 56 | { 57 | MasterPlaylistData = GenerateFairPlayHlsMasterPlaylistSignaling(key.Id), 58 | MediaPlaylistData = GenerateFairPlayHlsMediaPlaylistSignaling(key.Id) 59 | } 60 | }); 61 | } 62 | } 63 | 64 | public static string GeneratePlayReadyDashSignaling(Guid keyId, string playReadyLaUrl = null) 65 | { 66 | var psshBoxContents = GeneratePlayReadyHeader(keyId, playReadyLaUrl); 67 | var psshBox = CreatePsshBox(PlayReadySystemId, psshBoxContents); 68 | 69 | var psshElement = new XElement(DashConstants.PsshName, Convert.ToBase64String(psshBox)); 70 | var proElement = new XElement(DashConstants.ProName, Convert.ToBase64String(psshBoxContents)); 71 | 72 | return psshElement.ToString() + proElement.ToString(); 73 | } 74 | 75 | public static string GenerateWidevineDashSignaling(Guid keyId) 76 | { 77 | var psshBoxContents = GenerateWidevineHeader(keyId); 78 | var psshBox = CreatePsshBox(WidevineSystemId, psshBoxContents); 79 | var psshElement = new XElement(DashConstants.PsshName, Convert.ToBase64String(psshBox)); 80 | 81 | return psshElement.ToString(); 82 | } 83 | 84 | public static string GeneratePlayReadyMssSignaling(Guid keyId, string playReadyLaUrl = null) 85 | { 86 | // For MSS the signaling is the base64-encoded PRO. 87 | 88 | var psshBoxContents = GeneratePlayReadyHeader(keyId, playReadyLaUrl); 89 | return Convert.ToBase64String(psshBoxContents); 90 | } 91 | 92 | public static string GenerateWidevineHlsMasterPlaylistSignaling(Guid keyId) 93 | { 94 | return $"#EXT-X-SESSION-KEY:{GenerateWidevineHlsAttributes(keyId)}"; 95 | } 96 | 97 | public static string GenerateWidevineHlsMediaPlaylistSignaling(Guid keyId) 98 | { 99 | return $"#EXT-X-KEY:{GenerateWidevineHlsAttributes(keyId)}"; 100 | } 101 | 102 | public static string GenerateFairPlayHlsMasterPlaylistSignaling(Guid keyId, byte[] iv = null) 103 | { 104 | if (iv != null && iv.Length != 16) 105 | throw new ArgumentException("IV must be exactly 16 bytes."); 106 | 107 | return $"#EXT-X-SESSION-KEY:{GenerateFairPlayHlsAttributes(keyId, iv)}"; 108 | } 109 | 110 | public static string GenerateFairPlayHlsMediaPlaylistSignaling(Guid keyId, byte[] iv = null) 111 | { 112 | if (iv != null && iv.Length != 16) 113 | throw new ArgumentException("IV must be exactly 16 bytes."); 114 | 115 | return $"#EXT-X-KEY:{GenerateFairPlayHlsAttributes(keyId, iv)}"; 116 | } 117 | 118 | public static byte[] GeneratePlayReadyPsshBox(Guid keyId, string playReadyLaUrl = null) 119 | { 120 | var psshBoxContents = GeneratePlayReadyHeader(keyId, playReadyLaUrl); 121 | 122 | return CreatePsshBox(PlayReadySystemId, psshBoxContents); 123 | } 124 | 125 | public static byte[] GenerateWidevinePsshBox(Guid keyId) 126 | { 127 | var psshBoxContents = GenerateWidevineHeader(keyId); 128 | 129 | return CreatePsshBox(WidevineSystemId, psshBoxContents); 130 | } 131 | 132 | /// 133 | /// Creates a PSSH (Protection System Specific Header) box suitable for embedding into a media 134 | /// file that follows the ISO Base Media File Format specification. 135 | /// 136 | private static byte[] CreatePsshBox(Guid systemId, byte[] data) 137 | { 138 | // Size (32) BE 139 | // Type (32) 140 | // Version (8) 141 | // Flags (24) 142 | // SystemID (16*8) BE 143 | // DataSize (32) BE 144 | // Data (DataSize*8) 145 | 146 | using (var buffer = new MemoryStream()) 147 | { 148 | using (var writer = new MultiEndianBinaryWriter(buffer, ByteOrder.BigEndian)) 149 | { 150 | writer.Write(4 + 4 + 1 + 3 + 16 + 4 + data.Length); 151 | writer.Write(new[] { 'p', 's', 's', 'h' }); 152 | writer.Write(0); // 0 flags, 0 version. 153 | writer.Write(systemId.ToBigEndianByteArray()); 154 | writer.Write(data.Length); 155 | writer.Write(data); 156 | } 157 | 158 | return buffer.ToArray(); 159 | } 160 | } 161 | 162 | private static class DashConstants 163 | { 164 | private const string MpdNamespace = "urn:mpeg:dash:schema:mpd:2011"; 165 | private const string CencNamespace = "urn:mpeg:cenc:2013"; 166 | private const string PlayReadyNamespace = "urn:microsoft:playready"; 167 | 168 | public static readonly XName ContentProtectionName = XName.Get("ContentProtection", MpdNamespace); 169 | public static readonly XName PsshName = XName.Get("pssh", CencNamespace); 170 | public static readonly XName ProName = XName.Get("pro", PlayReadyNamespace); 171 | 172 | public static string GetProtectionSystemSchemeIdUri(Guid systemId) 173 | { 174 | return $"urn:uuid:{systemId}"; 175 | } 176 | } 177 | 178 | private static byte[] GenerateWidevineHeader(Guid keyId) 179 | { 180 | using (var buffer = new MemoryStream()) 181 | { 182 | var widevineHeader = new WidevinePsshData() 183 | { 184 | KeyIds = { keyId.ToBigEndianByteArray() }, 185 | ProtectionScheme = 1667591779 // "cenc". 186 | }; 187 | 188 | Serializer.Serialize(buffer, widevineHeader); 189 | 190 | return buffer.ToArray(); 191 | } 192 | } 193 | 194 | private static byte[] GeneratePlayReadyHeader(Guid keyId, string playReadyLaUrl = null) 195 | { 196 | var kidString = Convert.ToBase64String(keyId.ToByteArray()); 197 | 198 | // Plain text manipulation here to keep things simple. Some common issues include: 199 | // 1) The first element must be EXACTLY as written here. Including small things like order of attributes. 200 | // 2) There must be no extra whitespace anywhere. 201 | var xml = $"16AESCTR{(!string.IsNullOrWhiteSpace(playReadyLaUrl) ? $"{playReadyLaUrl}" : "")}{kidString}"; 202 | 203 | var xmlBytes = Encoding.Unicode.GetBytes(xml); 204 | 205 | using (var buffer = new MemoryStream()) 206 | { 207 | using (var writer = new BinaryWriter(buffer)) 208 | { 209 | // Size (32) 210 | // RecordCount (16) 211 | // RecordType (16) 212 | // RecordLength (16) 213 | // Data (xml) 214 | 215 | writer.Write(xmlBytes.Length + 4 + 2 + 2 + 2); // Length. 216 | writer.Write((ushort)1); // Record count. 217 | writer.Write((ushort)1); // Record type (RM header). 218 | writer.Write((ushort)xmlBytes.Length); // Record length. 219 | writer.Write(xmlBytes); 220 | } 221 | 222 | return buffer.ToArray(); 223 | } 224 | } 225 | 226 | private static string GenerateWidevineHlsAttributes(Guid keyId) 227 | { 228 | const string widevineMethodValue = "SAMPLE-AES-CTR"; 229 | 230 | var psshBoxContents = GenerateWidevineHeader(keyId); 231 | var psshBox = CreatePsshBox(WidevineSystemId, psshBoxContents); 232 | var psshBoxAsBase64 = Convert.ToBase64String(psshBox); 233 | 234 | // Widevine uses KEYID as 0x1234 hex string. Big endian, naturally. 235 | var keyIdString = "0x" + ByteArrayToHexString(keyId.ToBigEndianByteArray()); 236 | 237 | var widevineAttributes = $"METHOD={widevineMethodValue},URI=\"data:text/plain;base64,{psshBoxAsBase64}\",KEYID={keyIdString},KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\",KEYFORMATVERSIONS=\"1\""; 238 | 239 | return widevineAttributes; 240 | } 241 | 242 | private static string GenerateFairPlayHlsAttributes(Guid keyId, byte[] iv = null) 243 | { 244 | return $"METHOD=SAMPLE-AES,URI=\"skd://{keyId}" + 245 | (iv == null ? "" : $":{ByteArrayToHexString(iv)}") + 246 | "\",KEYFORMAT=\"com.apple.streamingkeydelivery\",KEYFORMATVERSIONS=\"1\""; 247 | } 248 | 249 | private static string ByteArrayToHexString(byte[] bytes) 250 | { 251 | var hex = BitConverter.ToString(bytes); 252 | return hex.Replace("-", ""); 253 | } 254 | 255 | /// 256 | /// Serializes the GUID to a byte array, using the big endian format for all components. 257 | /// This format is often used by non-Microsoft tooling. 258 | /// 259 | private static byte[] ToBigEndianByteArray(this Guid guid) 260 | { 261 | if (!BitConverter.IsLittleEndian) 262 | throw new InvalidOperationException("This method has not been tested on big endian machines and likely would not operate correctly."); 263 | 264 | var bytes = guid.ToByteArray(); 265 | 266 | Array.Reverse(bytes, 0, 4); 267 | Array.Reverse(bytes, 4, 2); 268 | Array.Reverse(bytes, 6, 2); 269 | 270 | return bytes; 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /Resources/Schema/cpix.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | --------------------------------------------------------------------------------