<---               larley.dev             1759563670
----------------------------------------------------
I've finally set up my own, self-hosted PlayReady License Server.
Contact me on Discord if you encounter any problems.

The server provides two functionalities:

1 - License Acquisition
-----------------------
Accessible via:

https://playready.larley.dev/RightsManager/AcquireLicense

It can act in 3 different ways:

1)
If no URL parameters are supplied, the server will act as a 
drop-in replacement for the official PlayReady license server
(test.playready.microsoft.com) by generating appropriate content 
keys for the key IDs provided in the WRMHEADER using the PlayReady 
Test Server Key Seed "XVBovsmzhP9gRIZxWfFta3VVRPzVEWmJsazEJ46I".

2)
The license generated by the key IDs specified can be modified by
providing an "adjustment" URL parameter set to a Base64-URL encoded
JSON configuration. Syntax is given at the end.

3)
As soon as the "override" URL parameter is supplied, the WRMHEADER's
contents are completely ignored and custom content keys can be 
specified in a Base64-URL encoded JSON configuration.
Note that a License Challenge is still required to get the binding key
for the license.

JSON Syntax
-----------
Both configurations "adjustment" and "override" can contain the same
fields, but located in different parts of the JSON. These default 
fields include:

{
  // Minimum Security Level
  "security_level": 150,

  // Type of Content Key
  // Supported values: "AES128BIT_CTR", "AES128BIT_CBC"
  "content_key_type": "AES128BIT_CTR",

  // Type of License
  // Supported values: "PERSISTENT", NON_PERSISTENT"
  "license_type": "NON_PERSISTENT",

  // See: microsoft.media.drm.advancedlicense.realtimeexpiration
  "real_time_expiration": false,

  // The date and time before which the license is not valid
  // See: microsoft.media.drm.advancedlicense.begindate
  "begin_date": 1759200000,

  // The date and time after which the license is not valid
  // See: microsoft.media.drm.advancedlicense.expirationdate
  "expiration_date": 1759200000,

  // The date after which the license may be permanently deleted 
  // from the PlayReady license store on the client
  // See: microsoft.media.drm.medialicense.removaldate
  "removal_date": 1759200000,

  // DEPRECATED as of PK v3.0
  // Specifies the time period in seconds during which protected 
  // content can be played on a client after its clock becomes unset
  // See: microsoft.media.drm.medialicense.graceperiod
  "grace_period": 10,

  // The metering identifier (UUID) for the metering aggregation service 
  // that meters this content
  // See: microsoft.media.drm.medialicense.meteringid
  "metering_id": "b5b94d9b-2988-4cbd-bf0f-105636f8e1c8",

  // The number of seconds until the license expires after the client 
  // first plays the content
  // See: microsoft.media.drm.playright.firstplayexpiration
  "first_play_expiration": 120,

  // Minimum Analog Television Output Protection Level
  // See: learn.microsoft.com/playready/overview/output-protection-levels
  "avopl": 100,

  // Minimum Compressed Digital Video Output Protection Level
  // See: learn.microsoft.com/playready/overview/output-protection-levels
  "cdvopl": 400,

  // Minimum Uncompressed Digital Video Output Protection Level
  // See: learn.microsoft.com/playready/overview/output-protection-levels
  "udvopl": 100,

  // Minimum Compressed Digital Audio Output Protection Level
  // See: learn.microsoft.com/playready/overview/output-protection-levels
  "cdaopl": 100,

  // Minimum Uncompressed Digital Audio Output Protection Level
  // See: learn.microsoft.com/playready/overview/output-protection-levels
  "udaopl": 100,

  // Explicit Analog Video Output Protection 
  // See: Compliance Rules 6.5
  "avop": ["760ae755-682a-41e0-b1b3-dcdf836a7306", "AAAAAQ=="],

  // Explicit Digital Audio Output Protection
  // See: Compliance Rules 6.4
  "daop": ["6d5cfa59-c250-4426-930e-fac72c8fcfa6", "AAAAAQ=="],

  // Explicit Digital Video Output Protection
  // See: Compliance Rules 6.6
  "dvop": ["abb2c6f1-e663-4625-a945-972d17b231e7", "AAAAAQ=="],

  // Play Enablers
  // See: Compliance Rules
  "play_enablers": ["786627d8-c2a6-44be-8f88-08ae255b01a7"]
}

For the adjustment config, these values are located in the root of the
JSON, along with other values that are specific to the adjustment config.

{
  // Override the Key Seed used to generate content keys
  // 30 bytes, Base64 encoded
  "custom_key_seed": "XVBovsmzhP9gRIZxWfFta3VVRPzVEWmJsazEJ46I",

  // default values go here...
}

The override model contains a list of licenses, each with their own
key ID and content key. Because of this, the default config values
are located in each license.

{
  "licenses": [
    {
      // Base64 encoded big-endian key ID 
      "key_id": "b2Ua4dvkRDS8tGkNFWTEHA==",

      // Base64 encoded content key
      "content_key": "iNqFKuT6Lh42rrLVyUmXsQ==",

      // default values go here...
    },
    {
      // second license...
    }
  ]
} 


2 - Client Information
----------------------
Accessible via:

https://playready.larley.dev/RightsManager/ClientInfo

This endpoint provides some insights into the client certificate
and Tee/Ree features provided by the client in the encrypted challenge
payload. Simply send a POST request to this endpoint containing a
normal liense challenge and the server will respond in XML



Additional information about the server
---------------------------------------

Revocation Information
----------------------
The server refreshes its revocation information every hour and the
current version is present in the <CustomData> field of every license
response.

Error responses
---------------
The server returns very detailed error messages about why an opration
might have failed. This can be very useful when developing PlayReady 
clients for troubleshooting problems.
A general response is located below (namespaces removed):

<soap:Envelope>
  <soap:Body>
    <soap:Fault>
      <faultcode>soap:Server</faultcode>
      <faultstring>ERROR MESSAGE</faultstring>
      <faultactor>ENDPOINT</faultactor>
      <detail>
        <Exception>
          <StatusCode>STATUS CODE</StatusCode>
        </Exception>
      </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

Python examples
---------------

Normal license acquisition:

response = requests.post(
  url="https://playready.larley.dev/RightsManager/AcquireLicense",
  data=request
)

License adjustment example (limiting devices to SL3000):

response = requests.post(
  url="https://playready.larley.dev/RightsManager/AcquireLicense",
  params={
    "adjustment": base64.urlsafe_b64encode(json.dumps({
      "security_level": 3000
    }).encode())
  },
  data=request
)

License override example (custom content key and key id):

response = requests.post(
  url="https://playready.larley.dev/RightsManager/AcquireLicense",
  params={
    "override": base64.urlsafe_b64encode(json.dumps({
      "licenses": [{
        "key_id": "b2Ua4dvkRDS8tGkNFWTEHA==",
        "content_key": "JmkiPT3oQ3-Y_7Qp7rpOuQ=="
      }]
    }).encode())
  },
  data=request
)

Client info example:

response = requests.post(
  url="https://playready.larley.dev/RightsManager/ClientInfo",
  data=request
)