├── 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 |
--------------------------------------------------------------------------------