diff --git a/Makefile b/Makefile
index 02c66a0b8..ebf9efbb9 100644
--- a/Makefile
+++ b/Makefile
@@ -78,6 +78,14 @@ serve:
(sleep 5; python3 -m webbrowser http://127.0.0.1:5000) &
npm run serve
+#Serve the OAS specification with live reload on source edits
+serve-watch:
+ npm run publish
+ npm run watch & WATCH_PID=$$!; \
+ trap "kill $$WATCH_PID 2>/dev/null" EXIT INT TERM; \
+ (sleep 5; python3 -m webbrowser http://127.0.0.1:5000) & \
+ npm run serve
+
#Check dependencies for licensing issues
.check-licenses:
npm run check-licenses
diff --git a/README.md b/README.md
index aa77b1f8e..6c85dad41 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ There are `make` commands that alias some of this functionality:
* `lint` -- Lints the spec and code
* `publish` -- Outputs the specification as a **single file** into the `build/` directory
* `serve` -- Serves a preview of the specification in human-readable format - your browser will automatically open the documentation
+* `serve-watch` -- Serves a preview of the specification in human-readable format that is updated as you make changes
* `build-test-documentation` -- Builds the test documentation that is checked into the repository under `docs/tests`
## Testing
diff --git a/package-lock.json b/package-lock.json
index 9732fb539..edae694ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"license-checker": "^25.0.1",
"minimist": "^1.2.2",
"newman": "^6.2.2",
+ "nodemon": "^3.1.0",
"sinon": "^17.0.1",
"typescript": "^5.5.4"
}
@@ -4466,6 +4467,13 @@
"node": ">= 4"
}
},
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -6182,6 +6190,110 @@
"node": ">=18"
}
},
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nodemon/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
@@ -7278,6 +7390,13 @@
"url": "https://github.com/sponsors/lupomontero"
}
},
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8267,6 +8386,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/simple-websocket": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
@@ -8820,6 +8965,16 @@
"node": ">=8.0"
}
},
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -9058,6 +9213,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
diff --git a/package.json b/package.json
index b079de7d9..7ba0e29a5 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,9 @@
"sourceType": "module",
"scripts": {
"lint": "redocly lint specification/communications-manager.yaml",
- "publish": "mkdir -p build && redocly bundle specification/communications-manager.yaml --dereferenced --remove-unused-components --ext json | poetry run python scripts/set_version.py > build/communications-manager.json",
+ "publish": "mkdir -p build && redocly bundle specification/communications-manager.yaml --dereferenced --remove-unused-components --ext json | poetry run python scripts/set_version.py | poetry run python scripts/inline_assets.py > build/communications-manager.json.tmp && mv build/communications-manager.json.tmp build/communications-manager.json",
"serve": "redocly preview-docs -p 5000 build/communications-manager.json",
+ "watch": "nodemon --watch specification --ext yaml,md,json --ignore build --exec \"npm run publish\"",
"check-licenses": "node_modules/.bin/license-checker --failOn GPL --failOn LGPL --failOn AGPL",
"sandbox-postman-collection": "newman run postman/NhsNotify.Sandbox.postman_collection.json",
"integration-postman-collection": "poetry run python scripts/build_postman_environment.py && newman run postman/NhsNotify.Integration.postman_collection.json -e postman/Integration.test.postman_environment.json",
@@ -46,6 +47,7 @@
"license-checker": "^25.0.1",
"minimist": "^1.2.2",
"newman": "^6.2.2",
+ "nodemon": "^3.1.0",
"sinon": "^17.0.1",
"typescript": "^5.5.4"
},
diff --git a/scripts/inline_assets.py b/scripts/inline_assets.py
new file mode 100644
index 000000000..fa31c34d7
--- /dev/null
+++ b/scripts/inline_assets.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""
+inline_assets.py
+
+Reads an openapi spec on stdin, replaces asset tokens with base64 `data:`
+URIs so documentation download links are self-contained (no external
+hosting), then prints the spec on stdout.
+"""
+import sys
+import os
+import json
+import base64
+
+# Maps a token found in the spec (typically within info.description) to the
+# repo-relative asset whose base64-encoded contents replace it at build time.
+ASSET_TOKENS = {
+ "{{NONPROD_CA_B64}}": "specification/documentation/certs/nonprod-ca.crt",
+ "{{PROD_CA_B64}}": "specification/documentation/certs/prod-ca.crt",
+}
+
+
+def _repo_root():
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+
+def embed_inline_assets(spec):
+ """Replace asset tokens anywhere in the spec with base64 of the asset.
+
+ A token is only resolved if it is present in the serialised spec. If a
+ referenced asset file is missing, the build fails loudly.
+ """
+ serialised = json.dumps(spec)
+ for token, rel_path in ASSET_TOKENS.items():
+ if token not in serialised:
+ continue
+ path = os.path.join(_repo_root(), rel_path)
+ with open(path, "rb") as asset:
+ encoded = base64.b64encode(asset.read()).decode("ascii")
+ serialised = serialised.replace(token, encoded)
+ return json.loads(serialised)
+
+
+def main():
+ """Main entrypoint"""
+ data = json.loads(sys.stdin.read())
+ data = embed_inline_assets(data)
+ sys.stdout.write(json.dumps(data, indent=2))
+ sys.stdout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/specification/callbacks/channel_status.yaml b/specification/callbacks/channel_status.yaml
index e1eaa935a..69b3a2379 100644
--- a/specification/callbacks/channel_status.yaml
+++ b/specification/callbacks/channel_status.yaml
@@ -24,20 +24,7 @@ description: |-
- need to know exactly how a message has performed in real time with each of your recipients
- need to know what happened to a message sent using a secondary cascade (opens in a new tab)
operationId: post-v1-channel-callbacks
-parameters:
- - name: x-hmac-sha256-signature
- in: header
- description: Contains a HMAC-SHA256 signature of the request body using a
- pre-agreed secret
- schema:
- type: string
- example: 9ee8c6aab877a97600e5c0cd8419f52d3dcdc45002e35220873d11123db6486f
- - name: x-api-key
- in: header
- description: Contains the pre-agreed API key.
- schema:
- type: string
- example: 0bb04a0e-d005-42dd-8993-dacf37410a12
+security: []
requestBody:
content:
application/vnd.api+json:
@@ -57,4 +44,4 @@ responses:
'403':
description: Forbidden
'429':
- $ref: ../responses/4xx/callbacks/429_TooManyRequests_Callbacks.yaml
\ No newline at end of file
+ $ref: ../responses/4xx/callbacks/429_TooManyRequests_Callbacks.yaml
diff --git a/specification/callbacks/message_status.yaml b/specification/callbacks/message_status.yaml
index f7b2557d1..e80d9048a 100644
--- a/specification/callbacks/message_status.yaml
+++ b/specification/callbacks/message_status.yaml
@@ -22,20 +22,6 @@ description: |-
If you need additional real time updates when the status of a channel changes, you can also use the channel and supplier status callback.
operationId: post-v1-message-callbacks
-parameters:
- - name: x-hmac-sha256-signature
- in: header
- description: Contains a HMAC-SHA256 signature of the request body using a
- pre-agreed secret
- schema:
- type: string
- example: 9ee8c6aab877a97600e5c0cd8419f52d3dcdc45002e35220873d11123db6486f
- - name: x-api-key
- in: header
- description: Contains the pre-agreed API key.
- schema:
- type: string
- example: 0bb04a0e-d005-42dd-8993-dacf37410a12
requestBody:
content:
application/vnd.api+json:
diff --git a/specification/documentation/APIDescription.md b/specification/documentation/APIDescription.md
index d36639bf4..e86a91c9c 100644
--- a/specification/documentation/APIDescription.md
+++ b/specification/documentation/APIDescription.md
@@ -199,23 +199,32 @@ Errors specific to each API are shown in the Endpoints section, under Response.
## Receive a callback
-You may develop one or many endpoints on your service if you want to receive callbacks from NHS Notify.
+You may develop one or multiple endpoints on your service if you want to receive callbacks from NHS Notify.
We have created an OpenAPI specification detailing the behaviour of the endpoint that consumers should create to subscribe to callbacks.
### Mutual TLS
-**This feature is currently under development and is not yet ready to use.**
+Our new callbacks mechanism uses Mutual TLS (mTLS) to verify the autheticity of the callback to ensure that it has come from NHS Notify.
+
+Your service must request the client certificate (during the callback connection TLS negotiation) and must verify the following:
-We are currently developing a new callbacks service which uses mutual TLS (mTLS).
+1. **Chain of trust**: the certificate must be signed by the correct CA for the environment (see table below).
+2. **Certificate expiry**: the current date/time must not fall outside `notBefore` and `notAfter`.
-If you are using the newer callbacks mechanism, then your service must check the client certificate which is presented to your service during the callback in order to verify that the response has come from NHS Notify. Your service must reject connection attempts where the client certificate can not be verified by the NHS Notify root CA certificate.
+We also recommend that you also verify the following fields:
-If the client certificate can not be verified, your service should respond with `403 Forbidden`.
+* The DN Common Name (CN) should be `NHS Notify Callbacks`
+* The Subject Alternative Name (SAN) URI should be the correct value for the environment (see table below)
+
+| Environment | CA Certificate | SAN URI |
+| --- | --- | --- |
+| Integration | Download | `spiffe://callbacks.nonprod.nhsnotify.national.nhs.uk/int/client` |
+| Production | Download | `spiffe://callbacks.prod.nhsnotify.national.nhs.uk/main/client` |
### HMAC-SHA256 signature checking
-If you are still using the older (original) callbacks mechanism, mTLS is not supported.
+If you are still using our older (original) callbacks mechanism, mTLS is not supported, and neither are the new callbacks for `RecipientResponse` and `ReturnedMail`.
Instead, we will send your API key in the `x-api-key` header. Your service should respond with:
@@ -223,6 +232,7 @@ Instead, we will send your API key in the `x-api-key` header. Your service shoul
* `401 Unauthorized` if the API key is invalid
We will send you a HMAC-SHA256 signature in the `x-hmac-sha256-signature` header. You will need to validate the signature to verify the response has come from NHS Notify.
+
This can be achieved by hashing the request body using the HMAC-SHA256 algorithm with a secret value that is comprised of a concatenation of your APIM application ID and the API key that we provide you. The secret takes the following form `[APPLICATION_ID].[API_KEY]`. If you receive a request with an invalid signature you should ignore it and respond with a `403 Forbidden`.
### Deduplication
diff --git a/specification/documentation/certs/nonprod-ca.crt b/specification/documentation/certs/nonprod-ca.crt
new file mode 100644
index 000000000..d2049b0fa
--- /dev/null
+++ b/specification/documentation/certs/nonprod-ca.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFZDCCA0ygAwIBAgIQNB/CGkV5l+oRKJB8UA9qrTANBgkqhkiG9w0BAQsFADBM
+MQswCQYDVQQGEwJHQjEUMBIGA1UECgwLTkhTIEVuZ2xhbmQxJzAlBgNVBAMMHm5o
+cy1tYWluLWFjY3QtY2xpZW50LWNhbGxiYWNrczAeFw0yNjA5MTAxMzU3MTdaFw0z
+NjA5MTAxNDU3MTdaMEwxCzAJBgNVBAYTAkdCMRQwEgYDVQQKDAtOSFMgRW5nbGFu
+ZDEnMCUGA1UEAwwebmhzLW1haW4tYWNjdC1jbGllbnQtY2FsbGJhY2tzMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAllElBxYL1F+3hRfGKF/Ahnd6ApJZ
+t+Tkfzd1wgLY9MSh0FmKNIdVWN9p8yJ7mDbPHfulXabO9i7xW2pw/RsSnfML47Vl
+8SbhnvQo4BOacbW91UFJaR/G+69ILD4Ez79ns8dug0EzGdCDDMZUnyQiZHJbJ6KX
+Xjv15ARaXY/FUNgF1sjbiRKJVdhodG1txtxLIk/jgcl6eFVLXwsCUBOjlSw7UUpE
+7Pq7xSls4tHEkVAuqP8uGEgpjLrSsHbDYR5t283SyhE4iqCP1a2mzpzF06M+2xXm
+FhJH/GefUJK1zCSx4DbPGNOYE8HeoUuw3BmmVxzjj+TwOQvzrOF4amwlHFUkFiTE
+bhcD014p+5O/j4g6cnGW4Dtb6hC9xHmsobNibPFm8KFTv4wFsmTkwwKwe8dNKpoI
+j2l0mEdmt3Bboi0KI1DYbCmlGDMUcTk+4Wov92pMo6Cb4P4eS0PAdAg2DcxjHvcT
+FJpCLVCKRUKmfpf9VCAiZv1/fEFVAc3Sy2JSAb+AAnJeXAIVhnELqEuVzswHaSot
+9QZGydPfnCv2Sx2IN0Y9n7bfX0Tzum9tJ1JIg0mh3+0pcDgWAFo8/eP3SSjqINmf
+B+uXKl2uQxHemok8oOkfReUz4XgB5h5FcrsocQcFgReaTLirq2XFKHGBS7qH1Pih
++CS+mQyrK185eyUCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+hLjBRZz5cb8+w8zUaRrkG0o2S1UwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+CwUAA4ICAQBQtUky8dq0AmElK/NdGDGlVr+PRUV8OnEaHEhmiInvc1xPpbZb1NnY
+If8oyEH/V3IWMNFcdJ6dAagVo1bl2wcyEEdDlHezCx7hMtjn6qLqjA9G0y8No/2D
+3VRr3ItwsFIvW9exTuk0ENkw9CkZVp5Q7juf3CH62hLbeLGTPNphJheCA+R+yKsi
+lJQVfIfauhHDEc+jFNId2ssUjbU7ktdw6xFhr4N0lT5UPCjp+wn0UZCwvdJeHIVy
+iV7yXqeKvP4WRhSuAz23KhLO6m2t0bMJZv4ZuiHI9XdlIX7ePeNIX6JfCCTn1qeb
+MeF3rGD/DuWM7elWXL9voViq1NVw9p/+SrMjitJyPnz+nupfWh1NdNZiqEJXQiru
+n4wrck5Q8FggCBxteqZgQhi0u+pJBt+F/CMq4Tyof+i4qaNZUGUgWW9WnB7SvIaf
+NYq27GydgaGIQp8AOi0wJ0g7fHkhuwNy7W6J4zVIcR+jKCzesYSY3RJlrOp0gdJn
+h3nzk0a64r6aTHMCTP5KrbxWWZWOu8sXOjBofN3yGvvQcwlyyRpxuMJT9GYduRYp
+gOzVftSsoDevF0jjNeqAN85kHQnzTd7D6cQDW8RJt+/nV+SmuOXSRZuFLHnl+abi
+WIFNedr5zfHbbhZRDgsNVHU/Z7MtBjHypt5Ketg1HNxAyvdcd6Gf7g==
+-----END CERTIFICATE-----
diff --git a/specification/documentation/certs/prod-ca.crt b/specification/documentation/certs/prod-ca.crt
new file mode 100644
index 000000000..4ed01c9b9
--- /dev/null
+++ b/specification/documentation/certs/prod-ca.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFZDCCA0ygAwIBAgIQH91Zw4byissn0eyy+Cj85jANBgkqhkiG9w0BAQsFADBM
+MQswCQYDVQQGEwJHQjEUMBIGA1UECgwLTkhTIEVuZ2xhbmQxJzAlBgNVBAMMHm5o
+cy1tYWluLWFjY3QtY2xpZW50LWNhbGxiYWNrczAeFw0yNjA5MTUxMDMyMDNaFw0z
+NjA5MTUxMTMyMDNaMEwxCzAJBgNVBAYTAkdCMRQwEgYDVQQKDAtOSFMgRW5nbGFu
+ZDEnMCUGA1UEAwwebmhzLW1haW4tYWNjdC1jbGllbnQtY2FsbGJhY2tzMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAk7H5/iAWUSBgQV/xfVlNTNdcjrJk
+UCMt0n70zgi+QQsH6AoAfRHX0Dr6zF+lCLH0OtGXAzuHq68okN9SkYEfXo93bpVe
+DfR3dcLjNoyGiIiwILpHJ+NNNvRbYLXJez4c7fbJBz80Z+mcMR8rGrrFzTldZJ9S
+EqGmS6PXtFUQPdAFkUlbyp9nstJSUTF33mCYMYTbAA1//0Pd+ZKXGCVMv6V6eTqX
+qpkI26zVz9DJ7W28eyMbwmn0433OcKRK+0kblvUDKx0tUqpxgTUeeiDf20EzahtA
+sgjmClTKW+G4xUmqdTOUsJaNbANdCG3eqnK2mOO3/ORrLi2LnG16O42kH7QINSA9
+Y1ChzFSsFixS8DeAMgQcHsXOWPOP4KJZghQqQHJNCHA3hBNS2/oQxtb01N2QQHAy
+VIWansf0Yh/dNSqIvbKMpjfZplbDwMh45s16ejumtE9QnAuz18oxyZLHvFB6Bqyk
+0O3hFaOu4WN5jP42mL8vmTBhEwFszhK1AqsZHM/rJYySPmUDqDfSSb9XMwaRYdHs
+k7cBpXWWvcC8SxZ5zqthlz36avBjk5fCLJwLIylNk2g/RDBnEJo0cRLOuIKvYUlx
+kIvbuGnGbn439V0RSMxihV5BmH8mL6DacsMbsvSWnQDkxMjiBcqshNsqI/+5FNIh
+ItBYJ4GWjABmYbcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+JuqMlb8KGzZn8EPyaOHz9al9x2gwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+CwUAA4ICAQBUbd8ypPD/0x/wR5/4TzwtSeOwMYzdDWd6HaERGJBHabwpuUkxFz/U
++r5lMcbt7hhpK4djhQCfzkNX73zhJlauwt41b+btvpAtQjQp4sWgSzBZRaGLsM7r
+BYQTjyk8cj8NZdGCj0ydRIEYNDI2OPYeB2gCWVrEMvr/6gOTlDDVJbY+bCk1H3zZ
+fy9z5R+qohMEbf6fOFJT9vPvCRYJcIgmhn3Hs3cNfjOvj4Y0Gryqk0m71L4EPlB4
+6vuC43w9Saq7cmi9CVxf1pIKD66971HcoyGc2KQFzGO0Y9jX4EQFZRtVTS2W8+p+
+lQwh7IQd7kks7U1YxzBKt9Eeqe2ZD6hPU2/Gx4UhVoBdNliBCka8aUYv2ac+Zl6w
+ogAAYv7ssbru6VNcQwEaJHbyN6RmIN2lOHNb+lQStBad10ZQRwoPOhJe13ASy0iP
+ok5gR6GYKgUOw9cV0rlLlCagZzjrQRZNiOdyw+JnU9kRh3tGreSwWmhRZW/4Pc2n
+Fb1LuC3J83HdA7RGuiiVvE95IxHp2TV2GXTdvNQEAMw8MtPFOroC+7NoiXHsEZ0e
+jH/8khSoFUuZagTMYM70zL4gH4kVL13a7GlbUVEriPzjGR6bTROMk/uIVUy5gKc1
+sVmE0Q7kkQGyIs4sEQiQrYkOief8o56ts+wo6RuODP8fg+kcqH3PLQ==
+-----END CERTIFICATE-----