{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# FHIR Digitally Signing FHIR Bundle or QuestionnaireResponse Object\n", "\n", "This is a Jupyter Notebook using Python 3.7 and openSSl to create JSON Web Signature (JWS)(see RFC 7515) and attach it to a FHIR Bundle or QuestionnaireResponse resource.\n", "\n", "- If the resource is a Bundle use Bundle.signature\n", "- If the resource is a QuestionnaireResponse use its [Signature extension](http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature\n", ")\n", "\n", "See signatures: http://build.fhir.org/signatures.html\n", "\n", "**For IG Publishing option to remove the signature from the Signature element and let the IG publisher sign and verify it (currently the IG publisher is messing up the narrative so it is tricky to predict the narrative)**\n", "\n", "*Although self-signed certificates are used for the purpose of these examples, they are not recommended for production systems.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Import Libraries" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/ehaas/.pyenv/versions/fhir_builds/lib/python3.7/site-packages/jose/backends/cryptography_backend.py:4: CryptographyDeprecationWarning: Python 3.7 is no longer supported by the Python core team and support for it is deprecated in cryptography. The next release of cryptography will remove support for Python 3.7.\n", " from cryptography.exceptions import InvalidSignature, InvalidTag\n" ] } ], "source": [ "from pprint import pprint\n", "from requests import get, post\n", "from json import dumps, loads\n", "from yaml import dump as dumpy, load as loady, SafeLoader\n", "from pathlib import Path\n", "from datetime import datetime\n", "import pytz\n", "from jose import jws #python JWS package\n", "from base64 import b64encode\n", "from jcs import canonicalize #package for a JCS (RFC 8785) compliant canonicalizer.\n", "from cryptography import x509\n", "from cryptography.hazmat.backends import default_backend\n", "from lxml import etree\n", "from copy import deepcopy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Set up Parameters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " #### 1.01 Choose the FHIR Bundle or QuestionnaireResponse File to Sign" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "=========== remove_nested_for_pub = True ================\n", "=========== detached = True ================\n", "=========== remove_sig_for_pub = False ================\n", "=========== cross_format = False ================\n", "=========== is_individual_cert = True ================\n", "=========== payload_id = cdex-questionnaireresponse-example4 ================\n" ] } ], "source": [ "\n", "local_file = True # vs remote files\n", "remove_nested_for_pub = True # if publication rule is to remove all nested meta profile - simple case for nested entries TODO update for all nested meta.profiles\n", "detached = True # Option to make a detached signature. (default to True for FHIR)\n", "remove_sig_for_pub = False # to let the IG publisher do ti TODO update for all nested meta.profiles\n", "cross_format = False # option to use cross-format validation rules so can xform from FHIR xml to FHIR json\n", "is_individual_cert = True # whether cert is for organization or individual. (true for search bundle example)\n", "\n", "# payload_id = 'cdex-document-digital-sig-example' # can be a Bundle or Questionnaire\n", "# payload_id = 'cdex-searchbundle-digital-sig-example' # can be a Bundle or Questionnaire\n", "payload_id = 'cdex-questionnaireresponse-example4' # can be a Bundle or Questionnaire\n", "\n", "in_path = '/Users/ehaas/Documents/FHIR/davinci-ecdx/input/examples-yaml'\n", "\n", "out_path = 'out_files' #YAML only\n", "out_path = in_path #YAML only\n", "out_file_name = 'signed_object' #YAML only\n", "out_file_name = payload_id #YAML only\n", "\n", "# my_url = \"https://argopatientlist.aidbox.app/fhir/Bundle\"\n", "# my_url = 'http://test.fhir.org/r4/Bundle'\n", "my_url = 'https://hl7.org/fhir/us/davinci-cdex'\n", "\n", "auto_timestamp = False\n", "timezone= 'US/Pacific'\n", "\n", "\n", "print(f'=========== remove_nested_for_pub = {remove_nested_for_pub} ================')\n", "print(f'=========== detached = {detached} ================')\n", "print(f'=========== remove_sig_for_pub = {remove_sig_for_pub} ================')\n", "print(f'=========== cross_format = {cross_format} ================')\n", "print(f'=========== is_individual_cert = {is_individual_cert} ================')\n", "print(f'=========== payload_id = {payload_id} ================')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.02. Define the location of the Document Signing Certificate\n", "\n", " - See the Jupyter file [Create_Cert.ipynb]() for how to generate your own self-signed certificate." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ ", ...)>" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "certificate_path = Path('example_provider_cert') if is_individual_cert else Path('example_org_cert') # update this to your folder\n", "cert_pem = certificate_path / 'cert.pem'\n", "cert_der = certificate_path / 'cert.der'\n", "certificate = x509.load_der_x509_certificate(cert_der.read_bytes(), default_backend())\n", "certificate" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Create JWS to Attach to Bundle or QuestionnaireResponse" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.01 Auto Timestamp Function" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def get_timestamp(timezone): # Get current time in timezone\n", " tz = pytz.timezone('US/Pacific')\n", " current_time = datetime.now(tz)\n", " iso_timestamp = current_time.isoformat() # Format as ISO 8601 compliant string\n", " return(iso_timestamp)\n", "# print(get_timestamp(timezone))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.04 Extract Common Name (CN) from the Subject as String" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def get_cn():\n", " for attr in certificate.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME):\n", " cn_raw = attr.value # e.g., \"John Hancock, MD\"\n", " # Check if CN is an email address (contains '@')\n", " if '@' not in cn_raw:\n", " cn = cn_raw\n", " break # Use the first non-email CN\n", " return(cn)\n", "# print(get_cn())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.06 Extract Subject Key Identifier (KID) as Hexadecimal String" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "e6efdb350b69b369a43ae60154e17f39ac75eb58\n" ] } ], "source": [ "def get_kid():\n", " kid_ext = certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)\n", " kid = kid_ext.value.digest.hex() # Hexadecimal string\n", " return(kid)\n", "\n", "# cert_der = Path('/Users/ehaas/Documents/FHIR/davinci-ecdx/CDEX-Signatures/cert.der')\n", "# certificate = x509.load_der_x509_certificate(cert_der.read_bytes(), default_backend())\n", "# print(get_kid())\n", "\n", "# print(get_kid())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.08 Extract Subject Alternative Name (SAN) as Dict\n", "\n", "- format\n", " - \"otherName\" = 2.16.840.1.113883.4.6;UTF8:9941339108\n", " - \"URI\" = https://example.org/fhir/Practitioner/123" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def get_san():\n", " san = {}\n", " san['NPI'] = []\n", " san_ext = certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName)\n", " san['DNS'] = san_ext.value.get_values_for_type(x509.DNSName)\n", " san['OtherName'] = san_ext.value.get_values_for_type(x509.OtherName)\n", " san['URI'] = san_ext.value.get_values_for_type(x509.UniformResourceIdentifier)\n", " for other_name in san['OtherName']:\n", " if other_name.type_id.dotted_string == '2.16.840.1.113883.4.6':\n", " san['NPI'].append(other_name.value.decode('utf-8').replace('\\x0c\\n',''))\n", " return san\n", "# print(get_san()['NPI'][0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.09 Canonicalize the XHTML in the Narrative using C14 N 1.0" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def canonicalize_xhtml(narrative_div):\n", "\n", " parser = etree.XMLParser(remove_blank_text=True)\n", " root = etree.fromstring(narrative_div, parser)\n", " # Canonicalize the XML using C14N 1.0\n", " canonicalized_str = etree.tostring(root, method=\"c14n\", with_comments=False).decode('utf-8')\n", " return canonicalized_str\n", "\n", "# narrative_div = '
Acute Asthmatic attack. Was wheezing\\n\\n\\n\\n for days prior to admission.
'\n", "# canonicalize_xhtml(narrative_div)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.1. Prepare Header\n", "\n", " note the base64 DER is Cert PEM file wihout the footer and header and line returns" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'alg': 'RS256',\n", " 'kty': 'RS',\n", " 'srCms': [{'commId': {'id': 'urn:oid:1.2.840.10065.1.12.1.5',\n", " 'desc': 'Verification Signature'},\n", " 'commQuals': ['Verification of medical record integrity']}],\n", " 'sigT': '2020-10-23T04:54:56.048+00:00',\n", " 'kid': 'bfbe3e5c0470444576548113928d5f1e4e3f2eeb',\n", " 'x5c': ['MIIFVzCCA7+gAwIBAgIUBn43F4O110zVNHPUtBnnXf33FQwwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYXVzYWxpdG8xHTAbBgNVBAoMFEV4YW1wbGUgT3JnYW5pemF0aW9uMRkwFwYDVQQDDBBKb2huIEhhbmNvY2ssIE1EMSMwIQYJKoZIhvcNAQkBFhRqaGFuY29ja0BleGFtcGxlLm9yZzAeFw0yNTA2MjUyMzEyMzlaFw0yNzA2MTUyMzEyMzlaMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU2F1c2FsaXRvMR0wGwYDVQQKDBRFeGFtcGxlIE9yZ2FuaXphdGlvbjEZMBcGA1UEAwwQSm9obiBIYW5jb2NrLCBNRDEjMCEGCSqGSIb3DQEJARYUamhhbmNvY2tAZXhhbXBsZS5vcmcwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCcXmvX60GA5G+Dl4iRn9TS/wTF1FTH9RmrP29G6XSOuVDEgGZwUTJI/OkRLPj+JUKy/kMY3Ym41k3JRr8NrJ7Ucjf3Te2Y0zmRMfGKO2X7p01Id8rGhnbsTkWjszcckjKOTk7E4HXO7XQmVvRZaPrjnVVsz6aIUmmUyBemUxsPQxqkd77zRKe1J+fMbpbSnaF2S5H9I5IpQu3erSjNwunumJA/5sNASMUf+ZrK5htwPflonlVA9HEPo6N5tJsCMEY5qkZAXD55PUbf8Ixrd3+t1iXNAgMdXPp9NjfmkzaHOsR5EL78oVftKH8XMgs9L+XXhcmp+SuSbUT+laQFnKZZ661EB8UVQGPhsHcuYz7M/+GD7lkmn5w7g6izY05Ds1tdth3hB+E1e0V8al0+HYxXtmL28ObrurZt5VOT636aBWeak3m1lt+JLiTWwcIXuriJwXCQ7W2OhIrleBnt5YRdF/VwkAf5Bp40DKrYSvBT/x3ParbcAs5rua4MiztzwzMCAwEAAaOBnDCBmTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DBgBgNVHREEWTBXgg93d3cuZXhhbXBsZS5vcmegGQYJYIZIAYb5WwQGoAwMCjk5NDEzMzkxMDCGKWh0dHBzOi8vZXhhbXBsZS5vcmcvZmhpci9QcmFjdGl0aW9uZXIvMTIzMB0GA1UdDgQWBBS/vj5cBHBERXZUgROSjV8eTj8u6zANBgkqhkiG9w0BAQsFAAOCAYEAfGh0fv76BMyRpin/6m2NnOAwYN+WYNW4ZwQKPoh+iBFjgb/gXZ/x3dnGq2RUnRHLybcvqOwC8t4+L2Qaentz/zSLfBIiHP+vtauVo4YirUKIFO5NkDbPRwEX6ZpotwRE2p3YPOqmLjUpuqezKRfuzffESIGCWf6bAZoH8ovyzkpUOxT2lcJL2YQgxaEE+/MOEDxby/G5tNlHWU1rm29DZD6iTn8FHMOb4DAeLs7deUBKeAxmCN1w07IbHfTX+8xozBk8m4rQaRI2FacGcN/S4OkiijlhkYnDdbb6ZoIB99AMG/0tMgWgh55FCL9w2yubFDsR+1yZW5w6xvVubFoq2rKIYN6BTMT349N+KYFRTb+pX04QFFCqX1OyuQEJjJpZauLIVSmUaVzDWdaRmt0rkLD6R85thYJpNxMr4oArsy5il5L0JVMEXG11ChExRCJ7XshktLjj+A8ldURrLHRWXotx6mNVp3+MoiChx7PfnGvCPaPvkLQqie8d8rk0ejN3']}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "der = cert_pem.read_text()\n", "der = der.replace('-----BEGIN CERTIFICATE-----','')\n", "der = der.replace('-----END CERTIFICATE-----','')\n", "der = der.replace('\\n','')\n", "header = {\n", " \"alg\": \"RS256\",\n", " \"kty\": \"RS\",\n", " \"srCms\": [\n", " {\n", " \"commId\":{\n", " \"id\": \"urn:oid:1.2.840.10065.1.12.1.5\",\n", " \"desc\": \"Verification Signature\"\n", " },\n", " \"commQuals\": [\"Verification of medical record integrity\"]\n", " }\n", " ],\n", " \"sigT\": get_timestamp(timezone) if auto_timestamp else '2020-10-23T04:54:56.048+00:00',\n", " \"kid\": get_kid(),\n", " \"x5c\": [der],\n", " }\n", "header" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3.2 Fetch Payload from FHIR Server or Local File\n", "- **set local_file to \"True\" for local file**" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'resourceType': 'QuestionnaireResponse',\n", " 'id': 'cdex-questionnaireresponse-example4',\n", " 'meta': {'extension': [{'url': 'http://hl7.org/fhir/StructureDefinition/instance-name',\n", " 'valueString': 'CDex QuestionnaireResponse Example4'},\n", " {'url': 'http://hl7.org/fhir/StructureDefinition/instance-description',\n", " 'valueMarkdown': 'This Questionnaire Response example demonstrates the response to a CDEX Data Query or Attachments that uses a Questionnaire to request the data and an electronic *digital* Verification signature is required. It is adapted from the Da Vinci DTR IG. The digital signature is a JSON [FHIR Signature](http://hl7.org/fhir/R4/signatures.html) in the QuestionnaireResponse signature extension. This example is adapted from the Da Vinci DTR IG.'}],\n", " 'profile': ['http://hl7.org/fhir/us/davinci-cdex/StructureDefinition/cdex-sdc-questionnaireresponse']},\n", " 'questionnaire': 'http://example.org/cdex-questionnaire-example2',\n", " 'status': 'completed',\n", " 'subject': {'identifier': {'use': 'usual',\n", " 'type': {'coding': [{'system': 'http://terminology.hl7.org/CodeSystem/v2-0203',\n", " 'code': 'MB',\n", " 'display': 'Member Number'}],\n", " 'text': 'Member Number'},\n", " 'system': 'http://example.org/cdex/payer/member-ids',\n", " 'value': 'Member123'},\n", " 'display': 'Amy Shaw'},\n", " 'authored': '2022-06-17',\n", " 'author': {'identifier': {'system': 'http://hl7.org/fhir/sid/us-npi',\n", " 'value': '9941339100'}},\n", " 'text': {'status': 'generated',\n", " 'div': '

Questionnaire Response Summary for Amy Shaw

Questionnaire: CDEX Questionnaire Example2

Date Completed: June 17, 2022

Patient: Amy Shaw (Member Number: Member123)

Questions and Answers

  1. Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy): Examplitis
  2. Order Reason: Replacement
'},\n", " 'item': [{'linkId': '1',\n", " 'text': 'Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy)',\n", " 'answer': [{'valueString': 'Examplitis'}]},\n", " {'linkId': '2',\n", " 'text': 'Order Reason',\n", " 'answer': [{'valueCoding': {'system': 'http://example.org',\n", " 'code': '4',\n", " 'display': 'Replacement'}}]}],\n", " 'extension': [{'url': 'http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature',\n", " 'valueSignature': {'type': [{'system': 'urn:iso-astm:E1762-95:2013',\n", " 'code': '1.2.840.10065.1.12.1.5',\n", " 'display': 'Verification Signature'}],\n", " 'when': '2020-10-23T04:54:56.048+00:00',\n", " 'who': {'identifier': {'system': 'http://hl7.org/fhir/sid/us-npi',\n", " 'type': {'coding': [{'system': 'http://terminology.hl7.org/CodeSystem/v2-0203',\n", " 'code': 'NPI'}]},\n", " 'value': '9941339100'},\n", " 'display': 'John Hancock, MD'},\n", " 'onBehalfOf': {'identifier': {'system': 'http://hl7.org/fhir/sid/us-npi',\n", " 'value': '1234567893'}},\n", " 'targetFormat': 'application/fhir+json;canonicalization=http://hl7.org/fhir/canonicalization/json#document',\n", " 'sigFormat': 'application/jose',\n", " 'data': 'ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkltSm1ZbVV6WlRWak1EUTNNRFEwTkRVM05qVTBPREV4TXpreU9HUTFaakZsTkdVelpqSmxaV0lpTENKcmRIa2lPaUpTVXlJc0luTnBaMVFpT2lJeU1ESXdMVEV3TFRJelZEQTBPalUwT2pVMkxqQTBPQ3N3TURvd01DSXNJbk55UTIxeklqcGJleUpqYjIxdFNXUWlPbnNpWkdWell5STZJbFpsY21sbWFXTmhkR2x2YmlCVGFXZHVZWFIxY21VaUxDSnBaQ0k2SW5WeWJqcHZhV1E2TVM0eUxqZzBNQzR4TURBMk5TNHhMakV5TGpFdU5TSjlMQ0pqYjIxdFVYVmhiSE1pT2xzaVZtVnlhV1pwWTJGMGFXOXVJRzltSUcxbFpHbGpZV3dnY21WamIzSmtJR2x1ZEdWbmNtbDBlU0pkZlYwc0luUjVjQ0k2SWtwWFZDSXNJbmcxWXlJNld5Sk5TVWxHVm5wRFEwRTNLMmRCZDBsQ1FXZEpWVUp1TkROR05FOHhNVEI2Vms1SVVGVjBRbTV1V0dZek0wWlJkM2RFVVZsS1MyOWFTV2gyWTA1QlVVVk1RbEZCZDJkYVZYaERla0ZLUW1kT1ZrSkJXVlJCYkZaVVRWSk5kMFZSV1VSV1VWRkpSRUZ3UkZsWGVIQmFiVGw1WW0xc2FFMVNTWGRGUVZsRVZsRlJTRVJCYkZSWldGWjZXVmQ0Y0dSSE9IaElWRUZpUW1kT1ZrSkJiMDFHUlZZMFdWY3hkMkpIVldkVU0wcHVXVmMxY0dWdFJqQmhWemwxVFZKcmQwWjNXVVJXVVZGRVJFSkNTMkl5YUhWSlJXaG9ZbTFPZGxreWMzTkpSVEZGVFZOTmQwbFJXVXBMYjFwSmFIWmpUa0ZSYTBKR2FGSnhZVWRHZFZreU9XcGhNRUpzWlVkR2RHTkhlR3hNYlRsNVducEJaVVozTUhsT1ZFRXlUV3BWZVUxNlJYbE5lbXhoUm5jd2VVNTZRVEpOVkZWNVRYcEZlVTE2YkdGTlNVZFdUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSVlJOUWtWSFFURlZSVU5CZDB0Uk1rWnpZVmRhZG1OdE5YQlpWRVZUVFVKQlIwRXhWVVZDZDNkS1ZUSkdNV015Um5OaFdGSjJUVkl3ZDBkM1dVUldVVkZMUkVKU1JtVkhSblJqUjNoc1NVVTVlVm95Um5WaFdIQm9aRWRzZG1KcVJWcE5RbU5IUVRGVlJVRjNkMUZUYlRsdlltbENTVmxYTldwaU1rNXlURU5DVGxKRVJXcE5RMFZIUTFOeFIxTkpZak5FVVVWS1FWSlpWV0Z0YUdoaWJVNTJXVEowUVZwWWFHaGlXRUp6V2xNMWRtTnRZM2RuWjBkcFRVRXdSME5UY1VkVFNXSXpSRkZGUWtGUlZVRkJORWxDYW5kQmQyZG5SMHRCYjBsQ1oxRkRZMWh0ZGxnMk1FZEJOVWNyUkd3MGFWSnVPVlJUTDNkVVJqRkdWRWc1VW0xeVVESTVSelpZVTA5MVZrUkZaMGRhZDFWVVNra3ZUMnRTVEZCcUswcFZTM2t2YTAxWk0xbHROREZyTTBwU2NqaE9ja28zVldOcVpqTlVaVEpaTUhwdFVrMW1SMHRQTWxnM2NEQXhTV1E0Y2tkb2JtSnpWR3RYYW5ONlkyTnJha3RQVkdzM1JUUklXRTgzV0ZGdFZuWlNXbUZRY21wdVZsWnplalpoU1ZWdGJWVjVRbVZ0VlhoelVGRjRjV3RrTnpkNlVrdGxNVW9yWmsxaWNHSlRibUZHTWxNMVNEbEpOVWx3VVhVelpYSlRhazUzZFc1MWJVcEJMelZ6VGtGVFRWVm1LMXB5U3pWb2RIZFFabXh2Ym14V1FUbElSVkJ2Tms0MWRFcHpRMDFGV1RWeGExcEJXRVExTlZCVlltWTRTWGh5WkRNcmRERnBXRTVCWjAxa1dGQndPVTVxWm0xcmVtRklUM05TTlVWTU56aHZWbVowUzBnNFdFMW5jemxNSzFoWWFHTnRjQ3RUZFZOaVZWUXJiR0ZSUm01TFdsbzJOakZGUWpoVlZsRkhVR2h6U0dOMVdYbzNUUzhyUjBRM2JHdHRialYzTjJjMmFYcFpNRFZFY3pGMFpIUm9NMmhDSzBVeFpUQldPR0ZzTUN0SVdYaFlkRzFNTWpoUFluSjFjbHAwTlZaUFZEWXpObUZDVjJWaGF6TnRNV3gwSzBwTWFWUlhkMk5KV0hWeWFVcDNXRU5STjFjeVQyaEpjbXhsUW01ME5WbFNaRVl2Vm5kclFXWTFRbkEwTUVSTGNsbFRka0pVTDNnelVHRnlZbU5CY3pWeWRXRTBUV2w2ZEhwM2VrMURRWGRGUVVGaFQwSnVSRU5DYlZSQlNrSm5UbFpJVWsxRlFXcEJRVTFCYzBkQk1WVmtSSGRSUlVGM1NVWTBSRUpuUW1kT1ZraFNSVVZYVkVKWVoyYzVNMlF6WTNWYVdHaG9ZbGhDYzFwVE5YWmpiV1ZuUjFGWlNsbEpXa2xCV1dJMVYzZFJSMjlCZDAxRGFtczFUa1JGZWsxNmEzaE5SRU5IUzFkb01HUklRbnBQYVRoMldsaG9hR0pZUW5OYVV6VjJZMjFqZGxwdGFIQmphVGxSWTIxR2FtUkhiREJoVnpsMVdsaEpkazFVU1hwTlFqQkhRVEZWWkVSblVWZENRbE12ZG1vMVkwSklRa1ZTV0ZwVloxSlBVMnBXT0dWVWFqaDFObnBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUVU5RFFWbEZRV1pIYURCbWRqYzJRazE1VW5CcGJpODJiVEpPYms5QmQxbE9LMWRaVGxjMFduZFJTMUJ2YUN0cFFrWnFaMkl2WjFoYUwzZ3paRzVIY1RKU1ZXNVNTRXg1WW1OMmNVOTNRemgwTkN0TU1sRmhaVzUwZWk5NlUweG1Ra2xwU0ZBcmRuUmhkVlp2TkZscGNsVkxTVVpQTlU1clJHSlFVbmRGV0RaYWNHOTBkMUpGTW5BeldWQlBjVzFNYWxWd2RYRmxla3RTWm5WNlptWkZVMGxIUTFkbU5tSkJXbTlJT0c5MmVYcHJjRlZQZUZReWJHTktUREpaVVdkNFlVVkZLeTlOVDBWRWVHSjVMMGMxZEU1c1NGZFZNWEp0TWpsRVdrUTJhVlJ1T0VaSVRVOWlORVJCWlV4ek4yUmxWVUpMWlVGNGJVTk9NWGN3TjBsaVNHWlVXQ3M0ZUc5NlFtczRiVFJ5VVdGU1NUSkdZV05IWTA0dlV6UlBhMmxwYW14b2ExbHVSR1JpWWpaYWIwbENPVGxCVFVjdk1IUk5aMWRuYURVMVJrTk1PWGN5ZVhWaVJrUnpVaXN4ZVZwWE5YYzJlSFpXZFdKR2IzRXlja3RKV1U0MlFsUk5WRE0wT1U0clMxbEdVbFJpSzNCWU1EUlJSa1pEY1ZneFQzbDFVVVZLYWtwd1dtRjFURWxXVTIxVllWWjZSRmRrWVZKdGREQnlhMHhFTmxJNE5YUm9XVXB3VG5oTmNqUnZRWEp6ZVRWcGJEVk1NRXBXVFVWWVJ6RXhRMmhGZUZKRFNqZFljMmhyZEV4cWFpdEJPR3hrVlZKeVRFaFNWMWh2ZEhnMmJVNVdjRE1yVFc5cFEyaDROMUJtYmtkMlExQmhVSFpyVEZGeGFXVTRaRGh5YXpCbGFrNHpJbDE5Li5rcUZ4VkxwV1VfMU40Yzg2TVJJYXlWQXFneF9JSFBfdk5iWlpGamN3cnRvVDV3ZnhWejQyd2NCZmVtQjlTWWRYaTdUR3BqN2ZzbVNWVEVlUk1qMm1rZ1o0ek95LUF3azFOX0pkSzNvZmFUeWphYkR5NTJyUERndFN6ZzMwdlBqaWJLTFFQVWxXN3doR01Xb2NORWVXNW1yRGdwUjR6UThGMFNJSjNvU2pkbFVoalZuZzRYRmxhZ2lKOHgtZldIQ0JZXzRWTmFZU2tUc0JFMlUxZURXWUgxR2NyVmF1ejh0MlBkOFJ2allkbXg5Z1JoM0x3dVZRNmJVTVpJZS14WDVLUmtVV19KbmJmdG8tM3ZZb2JOZ3gyUVk2enNvR0ZSbzFsWHlrUDZjRDhIS2VmUFRGT0pFNUFuT0U0NjB1SVdJU1lmU3kyMXBDaThaWmYxbFRuZ1ZCME5vZXZJYjd4OFctNldJSXhSX2N2X3JlVlNfcElGb3ZSVS1tTVNDa1Y2akhoX2NDWnMxUDhOSG5oajVPdlhVaHhqajkwWHlOd1BVclJlMDdMbllHSlRxbEtmVnRoMHNfMmtWRU83b20zVVQ4RTJaZHRQdkVpeFIwWkF3RW5Ra1Qxa1BEaXd6ZXotcHpNTTBQbGJ1NWFHQlV1SGZRTFpYVHpteU9pdU0tUG1wRw=='}}]}" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def fetch_payload():\n", "\n", " # url = \"https://argopatientlist.aidbox.app/fhir/Bundle\"\n", " # url = 'http://test.fhir.org/r4/Bundle'\n", " url = 'https://hl7.org/fhir/us/davinci-cdex'\n", "\n", " username = \"basic\"\n", " password = \"secret\"\n", " headers = {\"Accept\": \"application/fhir+json\" , \"Content-Type\": \"application/fhir+json\"}\n", "\n", " r = get(f'{url}/{payload_id}.json', auth=(username, password), headers = headers)\n", " my_obj = r.json()\n", " # print(\"=\"*80)\n", " # print(\"STATUS: \",r.status_code)\n", "\n", " # print(\"=\"*80)\n", " # print(\"HEADERS:\\n\")\n", " # for k,v in r.headers.items():\n", " # print(f'{k} = {v}')\n", " # print(\"=\"*80)\n", " # print(\"BODY:\\n\")\n", " # print(dumps(my_obj,indent=2))\n", " return(my_obj)\n", "\n", "\n", "if local_file: \n", " path = Path() / in_path / f'{payload_id}.yml'\n", " my_obj =loady(path.read_text(), Loader=SafeLoader)\n", "else:\n", " my_obj = fetch_payload()\n", "# print(dumpy(my_obj,indent=2, sort_keys=False))\n", "my_obj" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.2.1 Prepare Payload\n", "\n", "The payload is the base64_url form of the canonicalized version of the resource before attaching the signature\n", "\n", "1. Creat a deepcopy \n", "2. Canonicalize the narrative(s) (xhtml) using the C14N 1.0 specification (http://www.w3.org/2006/12/xml-c14n10).\n", "3. remove bundle.id, .meta .signature elements per Subscriptions, optionally remove nested meta profiles per publication rules\n", "4. if publication rule is to remove all nested meta profile - option to remove all nested meta profiles, (only simple case for nested entries - TODO update for all nested meta.profiles)\n", "5. For FHIR Document - insert anchors for publisher of form into the deepcopy for verification.\n", "6. Canonicalize the resource using IETF JSON Canonicalization Scheme (JCS)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "b'{\"author\":{\"identifier\":{\"system\":\"http://hl7.org/fhir/sid/us-npi\",\"value\":\"9941339100\"}},\"authored\":\"2022-06-17\",\"item\":[{\"answer\":[{\"valueString\":\"Examplitis\"}],\"linkId\":\"1\",\"text\":\"Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy)\"},{\"answer\":[{\"valueCoding\":{\"code\":\"4\",\"display\":\"Replacement\",\"system\":\"http://example.org\"}}],\"linkId\":\"2\",\"text\":\"Order Reason\"}],\"questionnaire\":\"http://example.org/cdex-questionnaire-example2\",\"resourceType\":\"QuestionnaireResponse\",\"status\":\"completed\",\"subject\":{\"display\":\"Amy Shaw\",\"identifier\":{\"system\":\"http://example.org/cdex/payer/member-ids\",\"type\":{\"coding\":[{\"code\":\"MB\",\"display\":\"Member Number\",\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0203\"}],\"text\":\"Member Number\"},\"use\":\"usual\",\"value\":\"Member123\"}},\"text\":{\"div\":\"

Questionnaire Response Summary for Amy Shaw

Questionnaire: CDEX Questionnaire Example2

Date Completed: June 17, 2022

Patient: Amy Shaw (Member Number: Member123)

Questions and Answers

  1. Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy): Examplitis
  2. Order Reason: Replacement
\",\"status\":\"generated\"}}'\n", "1423\n", "KeyError no QuestionnaireResponse.'entry'\n" ] }, { "data": { "text/plain": [ "(b'{\"author\":{\"identifier\":{\"system\":\"http://hl7.org/fhir/sid/us-npi\",\"value\":\"9941339100\"}},\"authored\":\"2022-06-17\",\"item\":[{\"answer\":[{\"valueString\":\"Examplitis\"}],\"linkId\":\"1\",\"text\":\"Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy)\"},{\"answer\":[{\"valueCoding\":{\"code\":\"4\",\"display\":\"Replacement\",\"system\":\"http://example.org\"}}],\"linkId\":\"2\",\"text\":\"Order Reason\"}],\"questionnaire\":\"http://example.org/cdex-questionnaire-example2\",\"resourceType\":\"QuestionnaireResponse\",\"status\":\"completed\",\"subject\":{\"display\":\"Amy Shaw\",\"identifier\":{\"system\":\"http://example.org/cdex/payer/member-ids\",\"type\":{\"coding\":[{\"code\":\"MB\",\"display\":\"Member Number\",\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0203\"}],\"text\":\"Member Number\"},\"use\":\"usual\",\"value\":\"Member123\"}},\"text\":{\"div\":\"

Questionnaire Response Summary for Amy Shaw

Questionnaire: CDEX Questionnaire Example2

Date Completed: June 17, 2022

Patient: Amy Shaw (Member Number: Member123)

Questions and Answers

  1. Relevant Patient Diagnoses (conditions that might be expected to improve with oxygen therapy): Examplitis
  2. Order Reason: Replacement
\",\"status\":\"generated\"}}',\n", " 1423)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "if my_obj['resourceType'] in ['Bundle', 'QuestionnaireResponse']:\n", " deep_copy = deepcopy(my_obj)\n", " deep_copy.pop('id', None)\n", " deep_copy.pop('meta', None)\n", " deep_copy.pop('signature', None)\n", "else:\n", " print('Not a Bundle or QuestionnaireResponse')\n", "\n", "if remove_nested_for_pub and deep_copy['resourceType'] == 'Bundle':\n", " print (\" ======= Removed nested meta profile from Bundle! ===========\")\n", " for bundle_entry in deep_copy['entry']:\n", " try:\n", " entry_profile = bundle_entry['resource']['meta'].pop('profile', None)\n", " except KeyError as e:\n", " print(f\"KeyError no {e} at entry - {bundle_entry['resource']['resourceType']} FullUrl: {bundle_entry['fullUrl']}\")\n", " else:\n", " # print(entry_profile)\n", " if not bundle_entry['resource']['meta']:\n", " bundle_entry['resource'].pop('meta')\n", " # else:\n", " # print(bundle_entry['resource']['meta'])\n", " # if deep_copy['type'] == \"document\": #For FHIR Document - insert anchors for publisher\n", " # entry_anchor = f' '\n", " # xhtml_declaration = '
'\n", " # bundle_entry['resource']['text']['div'] = bundle_entry['resource']['text']['div'].replace(xhtml_declaration,xhtml_declaration + entry_anchor)\n", " # print(bundle_entry['resource']['text']['div'])\n", "if deep_copy['resourceType'] == 'QuestionnaireResponse':\n", " try:\n", " for i, extension in enumerate(deep_copy['extension']):\n", " if extension['url'] == 'http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature':\n", " deep_copy_signature_ext = deep_copy['extension'].pop(i) # remove element\n", " if i == 0:\n", " deep_copy.pop('extension') # remove extension if empty\n", " except KeyError:\n", " print('No signature extension found')\n", "\n", "payload = canonicalize(deep_copy)\n", "print(payload)\n", "print(len(payload))\n", "if cross_format:\n", " try:\n", " deep_copy['text']['div'] = canonicalize_xhtml(deep_copy['text']['div'])\n", " except KeyError as e:\n", " print(f\"KeyError no {deep_copy['resourceType']}.{e}\")\n", "try:\n", " for bundle_entry in deep_copy['entry']:\n", " try:\n", " print(f\"Length before xhtml canonicalization for entry {bundle_entry['resource']['resourceType']}: {len(bundle_entry['resource']['text']['div'])}\")\n", " bundle_entry['resource']['text']['div'] = canonicalize_xhtml(bundle_entry['resource']['text']['div'])\n", " print(f\"Length after xhtml canonicalization for entry {bundle_entry['resource']['resourceType']}: {len(bundle_entry['resource']['text']['div'])}\")\n", " except KeyError as e:\n", " print(f\"KeyError no bundle_entry['resource'][{e}]\")\n", " continue\n", "except KeyError as e:\n", " print(f\"KeyError no {deep_copy['resourceType']}.{e}\")\n", "payload = canonicalize(deep_copy)\n", "payload, len(payload)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Then base64_url the payload entry\n", "\n", "note this step is combined with 3.3 below using the jws.sign method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.3 Create Signature using private key and the RS256 algorithm to get the JWS compact serialization format\n", "\n", "note the signature is displayed with the parts labeled and separated with line breaks for easier viewing." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "header:\n", "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJmYmUzZTVjMDQ3MDQ0NDU3NjU0ODExMzkyOGQ1ZjFlNGUzZjJlZWIiLCJrdHkiOiJSUyIsInNpZ1QiOiIyMDIwLTEwLTIzVDA0OjU0OjU2LjA0OCswMDowMCIsInNyQ21zIjpbeyJjb21tSWQiOnsiZGVzYyI6IlZlcmlmaWNhdGlvbiBTaWduYXR1cmUiLCJpZCI6InVybjpvaWQ6MS4yLjg0MC4xMDA2NS4xLjEyLjEuNSJ9LCJjb21tUXVhbHMiOlsiVmVyaWZpY2F0aW9uIG9mIG1lZGljYWwgcmVjb3JkIGludGVncml0eSJdfV0sInR5cCI6IkpXVCIsIng1YyI6WyJNSUlGVnpDQ0E3K2dBd0lCQWdJVUJuNDNGNE8xMTB6Vk5IUFV0Qm5uWGYzM0ZRd3dEUVlKS29aSWh2Y05BUUVMQlFBd2daVXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbFRZWFZ6WVd4cGRHOHhIVEFiQmdOVkJBb01GRVY0WVcxd2JHVWdUM0puWVc1cGVtRjBhVzl1TVJrd0Z3WURWUVFEREJCS2IyaHVJRWhoYm1Odlkyc3NJRTFFTVNNd0lRWUpLb1pJaHZjTkFRa0JGaFJxYUdGdVkyOWphMEJsZUdGdGNHeGxMbTl5WnpBZUZ3MHlOVEEyTWpVeU16RXlNemxhRncweU56QTJNVFV5TXpFeU16bGFNSUdWTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKVTJGMWMyRnNhWFJ2TVIwd0d3WURWUVFLREJSRmVHRnRjR3hsSUU5eVoyRnVhWHBoZEdsdmJqRVpNQmNHQTFVRUF3d1FTbTlvYmlCSVlXNWpiMk5yTENCTlJERWpNQ0VHQ1NxR1NJYjNEUUVKQVJZVWFtaGhibU52WTJ0QVpYaGhiWEJzWlM1dmNtY3dnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FDY1htdlg2MEdBNUcrRGw0aVJuOVRTL3dURjFGVEg5Um1yUDI5RzZYU091VkRFZ0dad1VUSkkvT2tSTFBqK0pVS3kva01ZM1ltNDFrM0pScjhOcko3VWNqZjNUZTJZMHptUk1mR0tPMlg3cDAxSWQ4ckdobmJzVGtXanN6Y2NraktPVGs3RTRIWE83WFFtVnZSWmFQcmpuVlZzejZhSVVtbVV5QmVtVXhzUFF4cWtkNzd6UktlMUorZk1icGJTbmFGMlM1SDlJNUlwUXUzZXJTak53dW51bUpBLzVzTkFTTVVmK1pySzVodHdQZmxvbmxWQTlIRVBvNk41dEpzQ01FWTVxa1pBWEQ1NVBVYmY4SXhyZDMrdDFpWE5BZ01kWFBwOU5qZm1remFIT3NSNUVMNzhvVmZ0S0g4WE1nczlMK1hYaGNtcCtTdVNiVVQrbGFRRm5LWlo2NjFFQjhVVlFHUGhzSGN1WXo3TS8rR0Q3bGttbjV3N2c2aXpZMDVEczF0ZHRoM2hCK0UxZTBWOGFsMCtIWXhYdG1MMjhPYnJ1clp0NVZPVDYzNmFCV2VhazNtMWx0K0pMaVRXd2NJWHVyaUp3WENRN1cyT2hJcmxlQm50NVlSZEYvVndrQWY1QnA0MERLcllTdkJUL3gzUGFyYmNBczVydWE0TWl6dHp3ek1DQXdFQUFhT0JuRENCbVRBSkJnTlZIUk1FQWpBQU1Bc0dBMVVkRHdRRUF3SUY0REJnQmdOVkhSRUVXVEJYZ2c5M2QzY3VaWGhoYlhCc1pTNXZjbWVnR1FZSllJWklBWWI1V3dRR29Bd01Dams1TkRFek16a3hNRENHS1doMGRIQnpPaTh2WlhoaGJYQnNaUzV2Y21jdlptaHBjaTlRY21GamRHbDBhVzl1WlhJdk1USXpNQjBHQTFVZERnUVdCQlMvdmo1Y0JIQkVSWFpVZ1JPU2pWOGVUajh1NnpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVlFQWZHaDBmdjc2Qk15UnBpbi82bTJObk9Bd1lOK1dZTlc0WndRS1BvaCtpQkZqZ2IvZ1haL3gzZG5HcTJSVW5SSEx5YmN2cU93Qzh0NCtMMlFhZW50ei96U0xmQklpSFArdnRhdVZvNFlpclVLSUZPNU5rRGJQUndFWDZacG90d1JFMnAzWVBPcW1MalVwdXFlektSZnV6ZmZFU0lHQ1dmNmJBWm9IOG92eXprcFVPeFQybGNKTDJZUWd4YUVFKy9NT0VEeGJ5L0c1dE5sSFdVMXJtMjlEWkQ2aVRuOEZITU9iNERBZUxzN2RlVUJLZUF4bUNOMXcwN0liSGZUWCs4eG96Qms4bTRyUWFSSTJGYWNHY04vUzRPa2lpamxoa1luRGRiYjZab0lCOTlBTUcvMHRNZ1dnaDU1RkNMOXcyeXViRkRzUisxeVpXNXc2eHZWdWJGb3EycktJWU42QlRNVDM0OU4rS1lGUlRiK3BYMDRRRkZDcVgxT3l1UUVKakpwWmF1TElWU21VYVZ6RFdkYVJtdDBya0xENlI4NXRoWUpwTnhNcjRvQXJzeTVpbDVMMEpWTUVYRzExQ2hFeFJDSjdYc2hrdExqaitBOGxkVVJyTEhSV1hvdHg2bU5WcDMrTW9pQ2h4N1Bmbkd2Q1BhUHZrTFFxaWU4ZDhyazBlak4zIl19\n", "\n", "payload:\n", "eyJhdXRob3IiOnsiaWRlbnRpZmllciI6eyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3NpZC91cy1ucGkiLCJ2YWx1ZSI6Ijk5NDEzMzkxMDAifX0sImF1dGhvcmVkIjoiMjAyMi0wNi0xNyIsIml0ZW0iOlt7ImFuc3dlciI6W3sidmFsdWVTdHJpbmciOiJFeGFtcGxpdGlzIn1dLCJsaW5rSWQiOiIxIiwidGV4dCI6IlJlbGV2YW50IFBhdGllbnQgRGlhZ25vc2VzIChjb25kaXRpb25zIHRoYXQgbWlnaHQgYmUgZXhwZWN0ZWQgdG8gaW1wcm92ZSB3aXRoIG94eWdlbiB0aGVyYXB5KSJ9LHsiYW5zd2VyIjpbeyJ2YWx1ZUNvZGluZyI6eyJjb2RlIjoiNCIsImRpc3BsYXkiOiJSZXBsYWNlbWVudCIsInN5c3RlbSI6Imh0dHA6Ly9leGFtcGxlLm9yZyJ9fV0sImxpbmtJZCI6IjIiLCJ0ZXh0IjoiT3JkZXIgUmVhc29uIn1dLCJxdWVzdGlvbm5haXJlIjoiaHR0cDovL2V4YW1wbGUub3JnL2NkZXgtcXVlc3Rpb25uYWlyZS1leGFtcGxlMiIsInJlc291cmNlVHlwZSI6IlF1ZXN0aW9ubmFpcmVSZXNwb25zZSIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInN1YmplY3QiOnsiZGlzcGxheSI6IkFteSBTaGF3IiwiaWRlbnRpZmllciI6eyJzeXN0ZW0iOiJodHRwOi8vZXhhbXBsZS5vcmcvY2RleC9wYXllci9tZW1iZXItaWRzIiwidHlwZSI6eyJjb2RpbmciOlt7ImNvZGUiOiJNQiIsImRpc3BsYXkiOiJNZW1iZXIgTnVtYmVyIiwic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS92Mi0wMjAzIn1dLCJ0ZXh0IjoiTWVtYmVyIE51bWJlciJ9LCJ1c2UiOiJ1c3VhbCIsInZhbHVlIjoiTWVtYmVyMTIzIn19LCJ0ZXh0Ijp7ImRpdiI6IjxkaXYgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sXCI-PGgxPlF1ZXN0aW9ubmFpcmUgUmVzcG9uc2UgU3VtbWFyeSBmb3IgQW15IFNoYXc8L2gxPjxwPjxzdHJvbmc-UXVlc3Rpb25uYWlyZTogPC9zdHJvbmc-PGEgaHJlZj1cImh0dHA6Ly9leGFtcGxlLm9yZy9jZGV4LXF1ZXN0aW9ubmFpcmUtZXhhbXBsZTJcIj5DREVYIFF1ZXN0aW9ubmFpcmUgRXhhbXBsZTI8L2E-PC9wPjxwPjxzdHJvbmc-RGF0ZSBDb21wbGV0ZWQ6IDwvc3Ryb25nPkp1bmUgMTcsIDIwMjI8L3A-PHA-PHN0cm9uZz5QYXRpZW50OiA8L3N0cm9uZz5BbXkgU2hhdyAoTWVtYmVyIE51bWJlcjogTWVtYmVyMTIzKTwvcD48aDI-UXVlc3Rpb25zIGFuZCBBbnN3ZXJzPC9oMj48b2w-PGxpPjxzdHJvbmc-UmVsZXZhbnQgUGF0aWVudCBEaWFnbm9zZXMgKGNvbmRpdGlvbnMgdGhhdCBtaWdodCBiZSBleHBlY3RlZCB0byBpbXByb3ZlIHdpdGggb3h5Z2VuIHRoZXJhcHkpOiA8L3N0cm9uZz5FeGFtcGxpdGlzPC9saT48bGk-PHN0cm9uZz5PcmRlciBSZWFzb246IDwvc3Ryb25nPlJlcGxhY2VtZW50PC9saT48L29sPjwvZGl2PiIsInN0YXR1cyI6ImdlbmVyYXRlZCJ9fQ\n", "\n", "signature:\n", "kqFxVLpWU_1N4c86MRIayVAqgx_IHP_vNbZZFjcwrtoT5wfxVz42wcBfemB9SYdXi7TGpj7fsmSVTEeRMj2mkgZ4zOy-Awk1N_JdK3ofaTyjabDy52rPDgtSzg30vPjibKLQPUlW7whGMWocNEeW5mrDgpR4zQ8F0SIJ3oSjdlUhjVng4XFlagiJ8x-fWHCBY_4VNaYSkTsBE2U1eDWYH1GcrVauz8t2Pd8RvjYdmx9gRh3LwuVQ6bUMZIe-xX5KRkUW_Jnbfto-3vYobNgx2QY6zsoGFRo1lXykP6cD8HKefPTFOJE5AnOE460uIWISYfSy21pCi8ZZf1lTngVB0NoevIb7x8W-6WIIxR_cv_reVS_pIFovRU-mMSCkV6jHh_cCZs1P8NHnhj5OvXUhxjj90XyNwPUrRe07LnYGJTqlKfVth0s_2kVEO7om3UT8E2ZdtPvEixR0ZAwEnQkT1kPDiwzez-pzMM0Plbu5aGBUuHfQLZXTzmyOiuM-PmpG\n", "\n" ] } ], "source": [ "private_key_path = certificate_path / 'private-key.pem'\n", "private_key = private_key_path.read_text()\n", "private_key\n", "\n", "signature = jws.sign(payload,private_key,algorithm='RS256',headers=header)\n", "\n", "labels = ['header', 'payload', 'signature']\n", "for i,j in enumerate(signature.split('.')):\n", " print(f'{labels[i]}:\\n{j}\\n')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.4. Option to Create \"detached-content\" payload by removing the payload from the JWS\n", "\n", "note the signature is displayed with the parts labeled and separated with line breaks for easier viewing then as compact serialization format" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "header:\n", "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJmYmUzZTVjMDQ3MDQ0NDU3NjU0ODExMzkyOGQ1ZjFlNGUzZjJlZWIiLCJrdHkiOiJSUyIsInNpZ1QiOiIyMDIwLTEwLTIzVDA0OjU0OjU2LjA0OCswMDowMCIsInNyQ21zIjpbeyJjb21tSWQiOnsiZGVzYyI6IlZlcmlmaWNhdGlvbiBTaWduYXR1cmUiLCJpZCI6InVybjpvaWQ6MS4yLjg0MC4xMDA2NS4xLjEyLjEuNSJ9LCJjb21tUXVhbHMiOlsiVmVyaWZpY2F0aW9uIG9mIG1lZGljYWwgcmVjb3JkIGludGVncml0eSJdfV0sInR5cCI6IkpXVCIsIng1YyI6WyJNSUlGVnpDQ0E3K2dBd0lCQWdJVUJuNDNGNE8xMTB6Vk5IUFV0Qm5uWGYzM0ZRd3dEUVlKS29aSWh2Y05BUUVMQlFBd2daVXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbFRZWFZ6WVd4cGRHOHhIVEFiQmdOVkJBb01GRVY0WVcxd2JHVWdUM0puWVc1cGVtRjBhVzl1TVJrd0Z3WURWUVFEREJCS2IyaHVJRWhoYm1Odlkyc3NJRTFFTVNNd0lRWUpLb1pJaHZjTkFRa0JGaFJxYUdGdVkyOWphMEJsZUdGdGNHeGxMbTl5WnpBZUZ3MHlOVEEyTWpVeU16RXlNemxhRncweU56QTJNVFV5TXpFeU16bGFNSUdWTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKVTJGMWMyRnNhWFJ2TVIwd0d3WURWUVFLREJSRmVHRnRjR3hsSUU5eVoyRnVhWHBoZEdsdmJqRVpNQmNHQTFVRUF3d1FTbTlvYmlCSVlXNWpiMk5yTENCTlJERWpNQ0VHQ1NxR1NJYjNEUUVKQVJZVWFtaGhibU52WTJ0QVpYaGhiWEJzWlM1dmNtY3dnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FDY1htdlg2MEdBNUcrRGw0aVJuOVRTL3dURjFGVEg5Um1yUDI5RzZYU091VkRFZ0dad1VUSkkvT2tSTFBqK0pVS3kva01ZM1ltNDFrM0pScjhOcko3VWNqZjNUZTJZMHptUk1mR0tPMlg3cDAxSWQ4ckdobmJzVGtXanN6Y2NraktPVGs3RTRIWE83WFFtVnZSWmFQcmpuVlZzejZhSVVtbVV5QmVtVXhzUFF4cWtkNzd6UktlMUorZk1icGJTbmFGMlM1SDlJNUlwUXUzZXJTak53dW51bUpBLzVzTkFTTVVmK1pySzVodHdQZmxvbmxWQTlIRVBvNk41dEpzQ01FWTVxa1pBWEQ1NVBVYmY4SXhyZDMrdDFpWE5BZ01kWFBwOU5qZm1remFIT3NSNUVMNzhvVmZ0S0g4WE1nczlMK1hYaGNtcCtTdVNiVVQrbGFRRm5LWlo2NjFFQjhVVlFHUGhzSGN1WXo3TS8rR0Q3bGttbjV3N2c2aXpZMDVEczF0ZHRoM2hCK0UxZTBWOGFsMCtIWXhYdG1MMjhPYnJ1clp0NVZPVDYzNmFCV2VhazNtMWx0K0pMaVRXd2NJWHVyaUp3WENRN1cyT2hJcmxlQm50NVlSZEYvVndrQWY1QnA0MERLcllTdkJUL3gzUGFyYmNBczVydWE0TWl6dHp3ek1DQXdFQUFhT0JuRENCbVRBSkJnTlZIUk1FQWpBQU1Bc0dBMVVkRHdRRUF3SUY0REJnQmdOVkhSRUVXVEJYZ2c5M2QzY3VaWGhoYlhCc1pTNXZjbWVnR1FZSllJWklBWWI1V3dRR29Bd01Dams1TkRFek16a3hNRENHS1doMGRIQnpPaTh2WlhoaGJYQnNaUzV2Y21jdlptaHBjaTlRY21GamRHbDBhVzl1WlhJdk1USXpNQjBHQTFVZERnUVdCQlMvdmo1Y0JIQkVSWFpVZ1JPU2pWOGVUajh1NnpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVlFQWZHaDBmdjc2Qk15UnBpbi82bTJObk9Bd1lOK1dZTlc0WndRS1BvaCtpQkZqZ2IvZ1haL3gzZG5HcTJSVW5SSEx5YmN2cU93Qzh0NCtMMlFhZW50ei96U0xmQklpSFArdnRhdVZvNFlpclVLSUZPNU5rRGJQUndFWDZacG90d1JFMnAzWVBPcW1MalVwdXFlektSZnV6ZmZFU0lHQ1dmNmJBWm9IOG92eXprcFVPeFQybGNKTDJZUWd4YUVFKy9NT0VEeGJ5L0c1dE5sSFdVMXJtMjlEWkQ2aVRuOEZITU9iNERBZUxzN2RlVUJLZUF4bUNOMXcwN0liSGZUWCs4eG96Qms4bTRyUWFSSTJGYWNHY04vUzRPa2lpamxoa1luRGRiYjZab0lCOTlBTUcvMHRNZ1dnaDU1RkNMOXcyeXViRkRzUisxeVpXNXc2eHZWdWJGb3EycktJWU42QlRNVDM0OU4rS1lGUlRiK3BYMDRRRkZDcVgxT3l1UUVKakpwWmF1TElWU21VYVZ6RFdkYVJtdDBya0xENlI4NXRoWUpwTnhNcjRvQXJzeTVpbDVMMEpWTUVYRzExQ2hFeFJDSjdYc2hrdExqaitBOGxkVVJyTEhSV1hvdHg2bU5WcDMrTW9pQ2h4N1Bmbkd2Q1BhUHZrTFFxaWU4ZDhyazBlak4zIl19\n", "\n", "payload:\n", "\n", "\n", "signature:\n", "kqFxVLpWU_1N4c86MRIayVAqgx_IHP_vNbZZFjcwrtoT5wfxVz42wcBfemB9SYdXi7TGpj7fsmSVTEeRMj2mkgZ4zOy-Awk1N_JdK3ofaTyjabDy52rPDgtSzg30vPjibKLQPUlW7whGMWocNEeW5mrDgpR4zQ8F0SIJ3oSjdlUhjVng4XFlagiJ8x-fWHCBY_4VNaYSkTsBE2U1eDWYH1GcrVauz8t2Pd8RvjYdmx9gRh3LwuVQ6bUMZIe-xX5KRkUW_Jnbfto-3vYobNgx2QY6zsoGFRo1lXykP6cD8HKefPTFOJE5AnOE460uIWISYfSy21pCi8ZZf1lTngVB0NoevIb7x8W-6WIIxR_cv_reVS_pIFovRU-mMSCkV6jHh_cCZs1P8NHnhj5OvXUhxjj90XyNwPUrRe07LnYGJTqlKfVth0s_2kVEO7om3UT8E2ZdtPvEixR0ZAwEnQkT1kPDiwzez-pzMM0Plbu5aGBUuHfQLZXTzmyOiuM-PmpG\n", "\n", "\n", "Signature in compact serialization format:\n", "================================================================================\n", "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJmYmUzZTVjMDQ3MDQ0NDU3NjU0ODExMzkyOGQ1ZjFlNGUzZjJlZWIiLCJrdHkiOiJSUyIsInNpZ1QiOiIyMDIwLTEwLTIzVDA0OjU0OjU2LjA0OCswMDowMCIsInNyQ21zIjpbeyJjb21tSWQiOnsiZGVzYyI6IlZlcmlmaWNhdGlvbiBTaWduYXR1cmUiLCJpZCI6InVybjpvaWQ6MS4yLjg0MC4xMDA2NS4xLjEyLjEuNSJ9LCJjb21tUXVhbHMiOlsiVmVyaWZpY2F0aW9uIG9mIG1lZGljYWwgcmVjb3JkIGludGVncml0eSJdfV0sInR5cCI6IkpXVCIsIng1YyI6WyJNSUlGVnpDQ0E3K2dBd0lCQWdJVUJuNDNGNE8xMTB6Vk5IUFV0Qm5uWGYzM0ZRd3dEUVlKS29aSWh2Y05BUUVMQlFBd2daVXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbFRZWFZ6WVd4cGRHOHhIVEFiQmdOVkJBb01GRVY0WVcxd2JHVWdUM0puWVc1cGVtRjBhVzl1TVJrd0Z3WURWUVFEREJCS2IyaHVJRWhoYm1Odlkyc3NJRTFFTVNNd0lRWUpLb1pJaHZjTkFRa0JGaFJxYUdGdVkyOWphMEJsZUdGdGNHeGxMbTl5WnpBZUZ3MHlOVEEyTWpVeU16RXlNemxhRncweU56QTJNVFV5TXpFeU16bGFNSUdWTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKVTJGMWMyRnNhWFJ2TVIwd0d3WURWUVFLREJSRmVHRnRjR3hsSUU5eVoyRnVhWHBoZEdsdmJqRVpNQmNHQTFVRUF3d1FTbTlvYmlCSVlXNWpiMk5yTENCTlJERWpNQ0VHQ1NxR1NJYjNEUUVKQVJZVWFtaGhibU52WTJ0QVpYaGhiWEJzWlM1dmNtY3dnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FDY1htdlg2MEdBNUcrRGw0aVJuOVRTL3dURjFGVEg5Um1yUDI5RzZYU091VkRFZ0dad1VUSkkvT2tSTFBqK0pVS3kva01ZM1ltNDFrM0pScjhOcko3VWNqZjNUZTJZMHptUk1mR0tPMlg3cDAxSWQ4ckdobmJzVGtXanN6Y2NraktPVGs3RTRIWE83WFFtVnZSWmFQcmpuVlZzejZhSVVtbVV5QmVtVXhzUFF4cWtkNzd6UktlMUorZk1icGJTbmFGMlM1SDlJNUlwUXUzZXJTak53dW51bUpBLzVzTkFTTVVmK1pySzVodHdQZmxvbmxWQTlIRVBvNk41dEpzQ01FWTVxa1pBWEQ1NVBVYmY4SXhyZDMrdDFpWE5BZ01kWFBwOU5qZm1remFIT3NSNUVMNzhvVmZ0S0g4WE1nczlMK1hYaGNtcCtTdVNiVVQrbGFRRm5LWlo2NjFFQjhVVlFHUGhzSGN1WXo3TS8rR0Q3bGttbjV3N2c2aXpZMDVEczF0ZHRoM2hCK0UxZTBWOGFsMCtIWXhYdG1MMjhPYnJ1clp0NVZPVDYzNmFCV2VhazNtMWx0K0pMaVRXd2NJWHVyaUp3WENRN1cyT2hJcmxlQm50NVlSZEYvVndrQWY1QnA0MERLcllTdkJUL3gzUGFyYmNBczVydWE0TWl6dHp3ek1DQXdFQUFhT0JuRENCbVRBSkJnTlZIUk1FQWpBQU1Bc0dBMVVkRHdRRUF3SUY0REJnQmdOVkhSRUVXVEJYZ2c5M2QzY3VaWGhoYlhCc1pTNXZjbWVnR1FZSllJWklBWWI1V3dRR29Bd01Dams1TkRFek16a3hNRENHS1doMGRIQnpPaTh2WlhoaGJYQnNaUzV2Y21jdlptaHBjaTlRY21GamRHbDBhVzl1WlhJdk1USXpNQjBHQTFVZERnUVdCQlMvdmo1Y0JIQkVSWFpVZ1JPU2pWOGVUajh1NnpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVlFQWZHaDBmdjc2Qk15UnBpbi82bTJObk9Bd1lOK1dZTlc0WndRS1BvaCtpQkZqZ2IvZ1haL3gzZG5HcTJSVW5SSEx5YmN2cU93Qzh0NCtMMlFhZW50ei96U0xmQklpSFArdnRhdVZvNFlpclVLSUZPNU5rRGJQUndFWDZacG90d1JFMnAzWVBPcW1MalVwdXFlektSZnV6ZmZFU0lHQ1dmNmJBWm9IOG92eXprcFVPeFQybGNKTDJZUWd4YUVFKy9NT0VEeGJ5L0c1dE5sSFdVMXJtMjlEWkQ2aVRuOEZITU9iNERBZUxzN2RlVUJLZUF4bUNOMXcwN0liSGZUWCs4eG96Qms4bTRyUWFSSTJGYWNHY04vUzRPa2lpamxoa1luRGRiYjZab0lCOTlBTUcvMHRNZ1dnaDU1RkNMOXcyeXViRkRzUisxeVpXNXc2eHZWdWJGb3EycktJWU42QlRNVDM0OU4rS1lGUlRiK3BYMDRRRkZDcVgxT3l1UUVKakpwWmF1TElWU21VYVZ6RFdkYVJtdDBya0xENlI4NXRoWUpwTnhNcjRvQXJzeTVpbDVMMEpWTUVYRzExQ2hFeFJDSjdYc2hrdExqaitBOGxkVVJyTEhSV1hvdHg2bU5WcDMrTW9pQ2h4N1Bmbkd2Q1BhUHZrTFFxaWU4ZDhyazBlak4zIl19..kqFxVLpWU_1N4c86MRIayVAqgx_IHP_vNbZZFjcwrtoT5wfxVz42wcBfemB9SYdXi7TGpj7fsmSVTEeRMj2mkgZ4zOy-Awk1N_JdK3ofaTyjabDy52rPDgtSzg30vPjibKLQPUlW7whGMWocNEeW5mrDgpR4zQ8F0SIJ3oSjdlUhjVng4XFlagiJ8x-fWHCBY_4VNaYSkTsBE2U1eDWYH1GcrVauz8t2Pd8RvjYdmx9gRh3LwuVQ6bUMZIe-xX5KRkUW_Jnbfto-3vYobNgx2QY6zsoGFRo1lXykP6cD8HKefPTFOJE5AnOE460uIWISYfSy21pCi8ZZf1lTngVB0NoevIb7x8W-6WIIxR_cv_reVS_pIFovRU-mMSCkV6jHh_cCZs1P8NHnhj5OvXUhxjj90XyNwPUrRe07LnYGJTqlKfVth0s_2kVEO7om3UT8E2ZdtPvEixR0ZAwEnQkT1kPDiwzez-pzMM0Plbu5aGBUuHfQLZXTzmyOiuM-PmpG\n" ] } ], "source": [ "if detached:\n", " split_sig = signature.split('.')\n", " split_sig[1] = ''\n", " signature = '.'.join(split_sig)\n", " for i,j in enumerate(signature.split('.')):\n", " print(f'{labels[i]}:\\n{j}\\n')\n", "\n", "print(f'\\nSignature in compact serialization format:\\n{\"=\"*80}\\n{signature}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. base64 the JWS and Add/Update the Signature element to the Bundle or QR\n", " \n", "- this is what would be contained and/or referenced by TASK over-the-wire" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " {\n", " \"url\": \"http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature\",\n", " \"valueSignature\": {\n", " \"type\": [\n", " {\n", " \"system\": \"urn:iso-astm:E1762-95:2013\",\n", " \"code\": \"1.2.840.10065.1.12.1.5\",\n", " \"display\": \"Verification Signature\"\n", " }\n", " ],\n", " \"when\": \"2020-10-23T04:54:56.048+00:00\",\n", " \"who\": {\n", " \"identifier\": {\n", " \"system\": \"http://hl7.org/fhir/sid/us-npi\",\n", " \"type\": {\n", " \"coding\": [\n", " {\n", " \"system\": \"http://terminology.hl7.org/CodeSystem/v2-0203\",\n", " \"code\": \"NPI\"\n", " }\n", " ]\n", " },\n", " \"value\": \"9941339100\"\n", " },\n", " \"display\": \"John Hancock, MD\"\n", " },\n", " \"onBehalfOf\": {\n", " \"identifier\": {\n", " \"system\": \"http://hl7.org/fhir/sid/us-npi\",\n", " \"value\": \"1234567893\"\n", " }\n", " },\n", " \"targetFormat\": \"application/fhir+json;canonicalization=http://hl7.org/fhir/canonicalization/json#document\",\n", " \"sigFormat\": \"application/jose\",\n", " \"data\": \"ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkltSm1ZbVV6WlRWak1EUTNNRFEwTkRVM05qVTBPREV4TXpreU9HUTFaakZsTkdVelpqSmxaV0lpTENKcmRIa2lPaUpTVXlJc0luTnBaMVFpT2lJeU1ESXdMVEV3TFRJelZEQTBPalUwT2pVMkxqQTBPQ3N3TURvd01DSXNJbk55UTIxeklqcGJleUpqYjIxdFNXUWlPbnNpWkdWell5STZJbFpsY21sbWFXTmhkR2x2YmlCVGFXZHVZWFIxY21VaUxDSnBaQ0k2SW5WeWJqcHZhV1E2TVM0eUxqZzBNQzR4TURBMk5TNHhMakV5TGpFdU5TSjlMQ0pqYjIxdFVYVmhiSE1pT2xzaVZtVnlhV1pwWTJGMGFXOXVJRzltSUcxbFpHbGpZV3dnY21WamIzSmtJR2x1ZEdWbmNtbDBlU0pkZlYwc0luUjVjQ0k2SWtwWFZDSXNJbmcxWXlJNld5Sk5TVWxHVm5wRFEwRTNLMmRCZDBsQ1FXZEpWVUp1TkROR05FOHhNVEI2Vms1SVVGVjBRbTV1V0dZek0wWlJkM2RFVVZsS1MyOWFTV2gyWTA1QlVVVk1RbEZCZDJkYVZYaERla0ZLUW1kT1ZrSkJXVlJCYkZaVVRWSk5kMFZSV1VSV1VWRkpSRUZ3UkZsWGVIQmFiVGw1WW0xc2FFMVNTWGRGUVZsRVZsRlJTRVJCYkZSWldGWjZXVmQ0Y0dSSE9IaElWRUZpUW1kT1ZrSkJiMDFHUlZZMFdWY3hkMkpIVldkVU0wcHVXVmMxY0dWdFJqQmhWemwxVFZKcmQwWjNXVVJXVVZGRVJFSkNTMkl5YUhWSlJXaG9ZbTFPZGxreWMzTkpSVEZGVFZOTmQwbFJXVXBMYjFwSmFIWmpUa0ZSYTBKR2FGSnhZVWRHZFZreU9XcGhNRUpzWlVkR2RHTkhlR3hNYlRsNVducEJaVVozTUhsT1ZFRXlUV3BWZVUxNlJYbE5lbXhoUm5jd2VVNTZRVEpOVkZWNVRYcEZlVTE2YkdGTlNVZFdUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSVlJOUWtWSFFURlZSVU5CZDB0Uk1rWnpZVmRhZG1OdE5YQlpWRVZUVFVKQlIwRXhWVVZDZDNkS1ZUSkdNV015Um5OaFdGSjJUVkl3ZDBkM1dVUldVVkZMUkVKU1JtVkhSblJqUjNoc1NVVTVlVm95Um5WaFdIQm9aRWRzZG1KcVJWcE5RbU5IUVRGVlJVRjNkMUZUYlRsdlltbENTVmxYTldwaU1rNXlURU5DVGxKRVJXcE5RMFZIUTFOeFIxTkpZak5FVVVWS1FWSlpWV0Z0YUdoaWJVNTJXVEowUVZwWWFHaGlXRUp6V2xNMWRtTnRZM2RuWjBkcFRVRXdSME5UY1VkVFNXSXpSRkZGUWtGUlZVRkJORWxDYW5kQmQyZG5SMHRCYjBsQ1oxRkRZMWh0ZGxnMk1FZEJOVWNyUkd3MGFWSnVPVlJUTDNkVVJqRkdWRWc1VW0xeVVESTVSelpZVTA5MVZrUkZaMGRhZDFWVVNra3ZUMnRTVEZCcUswcFZTM2t2YTAxWk0xbHROREZyTTBwU2NqaE9ja28zVldOcVpqTlVaVEpaTUhwdFVrMW1SMHRQTWxnM2NEQXhTV1E0Y2tkb2JtSnpWR3RYYW5ONlkyTnJha3RQVkdzM1JUUklXRTgzV0ZGdFZuWlNXbUZRY21wdVZsWnplalpoU1ZWdGJWVjVRbVZ0VlhoelVGRjRjV3RrTnpkNlVrdGxNVW9yWmsxaWNHSlRibUZHTWxNMVNEbEpOVWx3VVhVelpYSlRhazUzZFc1MWJVcEJMelZ6VGtGVFRWVm1LMXB5U3pWb2RIZFFabXh2Ym14V1FUbElSVkJ2Tms0MWRFcHpRMDFGV1RWeGExcEJXRVExTlZCVlltWTRTWGh5WkRNcmRERnBXRTVCWjAxa1dGQndPVTVxWm0xcmVtRklUM05TTlVWTU56aHZWbVowUzBnNFdFMW5jemxNSzFoWWFHTnRjQ3RUZFZOaVZWUXJiR0ZSUm01TFdsbzJOakZGUWpoVlZsRkhVR2h6U0dOMVdYbzNUUzhyUjBRM2JHdHRialYzTjJjMmFYcFpNRFZFY3pGMFpIUm9NMmhDSzBVeFpUQldPR0ZzTUN0SVdYaFlkRzFNTWpoUFluSjFjbHAwTlZaUFZEWXpObUZDVjJWaGF6TnRNV3gwSzBwTWFWUlhkMk5KV0hWeWFVcDNXRU5STjFjeVQyaEpjbXhsUW01ME5WbFNaRVl2Vm5kclFXWTFRbkEwTUVSTGNsbFRka0pVTDNnelVHRnlZbU5CY3pWeWRXRTBUV2w2ZEhwM2VrMURRWGRGUVVGaFQwSnVSRU5DYlZSQlNrSm5UbFpJVWsxRlFXcEJRVTFCYzBkQk1WVmtSSGRSUlVGM1NVWTBSRUpuUW1kT1ZraFNSVVZYVkVKWVoyYzVNMlF6WTNWYVdHaG9ZbGhDYzFwVE5YWmpiV1ZuUjFGWlNsbEpXa2xCV1dJMVYzZFJSMjlCZDAxRGFtczFUa1JGZWsxNmEzaE5SRU5IUzFkb01HUklRbnBQYVRoMldsaG9hR0pZUW5OYVV6VjJZMjFqZGxwdGFIQmphVGxSWTIxR2FtUkhiREJoVnpsMVdsaEpkazFVU1hwTlFqQkhRVEZWWkVSblVWZENRbE12ZG1vMVkwSklRa1ZTV0ZwVloxSlBVMnBXT0dWVWFqaDFObnBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUVU5RFFWbEZRV1pIYURCbWRqYzJRazE1VW5CcGJpODJiVEpPYms5QmQxbE9LMWRaVGxjMFduZFJTMUJ2YUN0cFFrWnFaMkl2WjFoYUwzZ3paRzVIY1RKU1ZXNVNTRXg1WW1OMmNVOTNRemgwTkN0TU1sRmhaVzUwZWk5NlUweG1Ra2xwU0ZBcmRuUmhkVlp2TkZscGNsVkxTVVpQTlU1clJHSlFVbmRGV0RaYWNHOTBkMUpGTW5BeldWQlBjVzFNYWxWd2RYRmxla3RTWm5WNlptWkZVMGxIUTFkbU5tSkJXbTlJT0c5MmVYcHJjRlZQZUZReWJHTktUREpaVVdkNFlVVkZLeTlOVDBWRWVHSjVMMGMxZEU1c1NGZFZNWEp0TWpsRVdrUTJhVlJ1T0VaSVRVOWlORVJCWlV4ek4yUmxWVUpMWlVGNGJVTk9NWGN3TjBsaVNHWlVXQ3M0ZUc5NlFtczRiVFJ5VVdGU1NUSkdZV05IWTA0dlV6UlBhMmxwYW14b2ExbHVSR1JpWWpaYWIwbENPVGxCVFVjdk1IUk5aMWRuYURVMVJrTk1PWGN5ZVhWaVJrUnpVaXN4ZVZwWE5YYzJlSFpXZFdKR2IzRXlja3RKV1U0MlFsUk5WRE0wT1U0clMxbEdVbFJpSzNCWU1EUlJSa1pEY1ZneFQzbDFVVVZLYWtwd1dtRjFURWxXVTIxVllWWjZSRmRrWVZKdGREQnlhMHhFTmxJNE5YUm9XVXB3VG5oTmNqUnZRWEp6ZVRWcGJEVk1NRXBXVFVWWVJ6RXhRMmhGZUZKRFNqZFljMmhyZEV4cWFpdEJPR3hrVlZKeVRFaFNWMWh2ZEhnMmJVNVdjRE1yVFc5cFEyaDROMUJtYmtkMlExQmhVSFpyVEZGeGFXVTRaRGh5YXpCbGFrNHpJbDE5Li5rcUZ4VkxwV1VfMU40Yzg2TVJJYXlWQXFneF9JSFBfdk5iWlpGamN3cnRvVDV3ZnhWejQyd2NCZmVtQjlTWWRYaTdUR3BqN2ZzbVNWVEVlUk1qMm1rZ1o0ek95LUF3azFOX0pkSzNvZmFUeWphYkR5NTJyUERndFN6ZzMwdlBqaWJLTFFQVWxXN3doR01Xb2NORWVXNW1yRGdwUjR6UThGMFNJSjNvU2pkbFVoalZuZzRYRmxhZ2lKOHgtZldIQ0JZXzRWTmFZU2tUc0JFMlUxZURXWUgxR2NyVmF1ejh0MlBkOFJ2allkbXg5Z1JoM0x3dVZRNmJVTVpJZS14WDVLUmtVV19KbmJmdG8tM3ZZb2JOZ3gyUVk2enNvR0ZSbzFsWHlrUDZjRDhIS2VmUFRGT0pFNUFuT0U0NjB1SVdJU1lmU3kyMXBDaThaWmYxbFRuZ1ZCME5vZXZJYjd4OFctNldJSXhSX2N2X3JlVlNfcElGb3ZSVS1tTVNDa1Y2akhoX2NDWnMxUDhOSG5oajVPdlhVaHhqajkwWHlOd1BVclJlMDdMbllHSlRxbEtmVnRoMHNfMmtWRU83b20zVVQ4RTJaZHRQdkVpeFIwWkF3RW5Ra1Qxa1BEaXd6ZXotcHpNTTBQbGJ1NWFHQlV1SGZRTFpYVHpteU9pdU0tUG1wRw==\"\n", " }\n", " }\n", "]\n" ] } ], "source": [ "b64_jws = b64encode(signature.encode()).decode()\n", "sig_element = {\n", " \"type\": [ # The Signature.type shall contain the same values as the srCms element.\"\n", " {\n", " \"system\": \"urn:iso-astm:E1762-95:2013\",\n", " \"code\": header['srCms'][0]['commId']['id'].replace('urn:oid:',''), # copy from header\n", " \"display\": header['srCms'][0]['commId']['desc'], # copy from header\n", " }\n", " ],\n", " \"when\": get_timestamp(timezone) if auto_timestamp else '2020-10-23T04:54:56.048+00:00', #system timestamp when signature created\n", " # \"who\" { #Reference to the Practitioner who signed and attested to the Bundle\n", " # \"reference\": \"Practitioner/123\" \n", " # \"display\": \"Practitioner/123\" \n", " # },\n", " # \"onBehalfOf\": { #Reference to the Organization\n", " # \"reference\": \"Organization/123\"\n", " # \"display\": \"Organization/123\"\n", " # },\n", " \"who\": { #Reference to the Practitioner who signed and attested to the Bundle using NPI should be same as SAN\n", " \"identifier\": {\n", " \"system\": \"http://hl7.org/fhir/sid/us-npi\",\n", " \"type\" : {\n", " \"coding\" : [{\n", " \"system\" : \"http://terminology.hl7.org/CodeSystem/v2-0203\",\n", " \"code\" : \"NPI\"\n", " }]\n", " },\n", " \"value\": get_san()['NPI'][0] # extract SAN from certificate\n", " },\n", " \"display\": get_cn() # extract CN from certificate\n", " },\n", " \"onBehalfOf\": {\n", " \"identifier\": {\n", " \"system\": \"http://hl7.org/fhir/sid/us-npi\",\n", " \"value\": \"1234567893\"\n", " }\n", " },\n", " \"targetFormat\" : \"application/fhir+json;canonicalization=http://hl7.org/fhir/canonicalization/json+xml#document\" if cross_format else \"application/fhir+json;canonicalization=http://hl7.org/fhir/canonicalization/json#document\", # The technical format of the signed resources see \n", " #https:// hl7.org/fhir/json.html#canonical. The MIME types can include optional parameters in the format type/subtype;\n", " # parameter=value, as defined in RFC 2045 and RFC 6838. For example, text/plain;charset=utf-8 specifies a character encoding.\n", " \"sigFormat\" : \"application/jose\", # The technical format of the signature\n", " \"data\": b64_jws\n", " }\n", "if not is_individual_cert:\n", " sig_element.pop(\"onBehalfOf\")\n", "if my_obj['resourceType'] == 'Bundle':\n", " my_obj['signature'] = sig_element # update signature\n", "if my_obj['resourceType'] == 'QuestionnaireResponse': # only create new extension for now TODO add ability to append to extension and check for old signatures \n", " my_obj['extension'] = [dict(\n", " url='http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature',\n", " valueSignature=sig_element\n", " )]\n", "try:\n", " print(dumps(my_obj['signature'], indent=2, sort_keys=False))\n", "except KeyError:\n", " print(dumps(my_obj['extension'], indent=2, sort_keys=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using FHIR RESTful create POST to FHIR Server \n", "***Deactivated until until get new AIDBOX***\n", "using AIDBox for now at `https://argopatientlist.aidbox.app/fhir/`\n", "\n" ] }, { "cell_type": "raw", "metadata": { "vscode": { "languageId": "raw" } }, "source": [ "url = \"https://argopatientlist.aidbox.app/fhir/Bundle\"\n", "# url = 'http://test.fhir.org/r4/Bundle'\n", "username = \"basic\"\n", "password = \"secret\"\n", "headers = {\"Accept\": \"application/fhir+json\" , \"Content-Type\": \"application/fhir+json\"}\n", "\n", "r = post(url, auth=(username, password), headers = headers, data = dumps(my_bundle))\n", "print(f'STATUS: {r.status_code}\\nHEADERS:')\n", "for k,v in r.headers.items():\n", " print(f'{k} = {v}')\n", "print(f\"BODY:\\n{dumps(r.json(),indent=2)}\")\n", "# print(dumps(r.json(),indent=2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Alternatively Write to Local File as JSON and YAML" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "=========== remove_nested_for_pub = True ================\n", "=========== detached = True ================\n", "=========== remove_sig_for_pub = False ================\n", "=========== cross_format = False ================\n", "=========== is_individual_cert = True ================\n", "=========== payload_id = cdex-questionnaireresponse-example4 ================\n", "Writing signed object to out_files/signed_object.json\n", "Writing signed object to /Users/ehaas/Documents/FHIR/davinci-ecdx/input/examples-yaml/cdex-questionnaireresponse-example4.yml\n" ] } ], "source": [ "path = Path(r'out_files/signed_object.json') # Fixed output file for json\n", "print(f'=========== remove_nested_for_pub = {remove_nested_for_pub} ================')\n", "print(f'=========== detached = {detached} ================')\n", "print(f'=========== remove_sig_for_pub = {remove_sig_for_pub} ================')\n", "print(f'=========== cross_format = {cross_format} ================')\n", "print(f'=========== is_individual_cert = {is_individual_cert} ================')\n", "print(f'=========== payload_id = {payload_id} ================')\n", "if remove_sig_for_pub:\n", " try:\n", " my_obj['signature'].pop('data')\n", " except KeyError:\n", " my_obj['extension']['valueSignauture'].pop('data') # TODO update for only signature extension\n", "print(f'Writing signed object to {path}')\n", "path.write_text(dumps(my_obj,indent=2,sort_keys=False))\n", "path = Path() /out_path / f\"{out_file_name}.yml\"\n", "print(f'Writing signed object to {path}')\n", "how_to = '''\n", "# steps to create/update dig signature examples\n", "#\n", "# 1. create unsigned Bundle or QuestionnaireResponse example source file with minified Narrative element(s) to prevent the publisher from adding its own.\n", "# 1. for Documents Bundles use the autogenerated narrative instead to get it to verify. ( use only the -i parameter to keep the meta elements )\n", "# 1. optionally create certificate using this script file: https://github.com/HL7/davinci-ecdx/blob/master/CDEX-Signatures/Create_Cert.ipynb\n", "# 1. create signatures using this script file: https://github.com/HL7/davinci-ecdx/blob/master/CDEX-Signatures/Create_Digsign_Bundle_or_QR.ipynb\n", "# - remove elements from payload as defined by the signature canonical and publisher requirements (for example, meta profile elements)\n", "# 1. save to YAML source\n", "# 1. run sushi and publisher again with -ink parameters\n", "# 1. verify signatures using this script file: https://github.com/HL7/davinci-ecdx/blob/master/CDEX-Signatures/Verify_digsign_Bundle_or_QR.ipynb\n", "'''\n", "\n", "path.write_text(f'{how_to}\\n{dumpy(my_obj,indent=2,sort_keys=False)}')\n", "if payload_id == 'cdex-document-digital-sig-example': # update the parameters examples with the signed doc too\n", " path = Path() / in_path / 'cdex-parameters-example2.yml'\n", " print(f'Writing signed object to {path}')\n", " params =loady(path.read_text(), Loader=SafeLoader)\n", " meta_extension = my_obj[\"meta\"].pop(\"extension\") \n", " params['parameter'][7]['part'][2]['resource'] = my_obj\n", " my_obj[\"meta\"][\"extension\"] = meta_extension # add back in so is idempotent cell\n", " path.write_text(dumpy(params,indent=2,sort_keys=False))" ] } ], "metadata": { "kernelspec": { "display_name": "fhir_builds", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 4 }