@@ -12,14 +12,14 @@ The SCITT API emulator can deny entry based on presence of
12
12
This is a simple way to enable evaluation of claims prior to submission by
13
13
arbitrary policy engines which watch the workspace (fanotify, inotify, etc.).
14
14
15
- [ ![ asciicast-of-simple-decoupled-file-based-policy-engine] ( https://asciinema.org/a/572766 .svg )] ( https://asciinema.org/a/572766 )
15
+ [ ![ asciicast-of-simple-decoupled-file-based-policy-engine] ( https://asciinema.org/a/620587 .svg )] ( https://asciinema.org/a/620587 )
16
16
17
17
Start the server
18
18
19
19
``` console
20
20
$ rm -rf workspace/
21
21
$ mkdir -p workspace/storage/operations
22
- $ scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro
22
+ $ timeout 1s scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro
23
23
Service parameters: workspace/service_parameters.json
24
24
^C
25
25
```
@@ -84,43 +84,66 @@ import os
84
84
import sys
85
85
import json
86
86
import pathlib
87
- import traceback
87
+ import unittest
88
88
89
- import cbor2
89
+ import cwt
90
90
import pycose
91
+ from pycose.messages import Sign1Message
91
92
from jsonschema import validate, ValidationError
92
- from pycose.messages import CoseMessage, Sign1Message
93
93
94
- from scitt_emulator.scitt import ClaimInvalidError, COSE_Headers_Issuer
94
+ from scitt_emulator.scitt import ClaimInvalidError, CWTClaims
95
+ from scitt_emulator.verify_statement import verify_statement
96
+ from scitt_emulator.key_helpers import verification_key_to_object
97
+
95
98
96
- claim = sys.stdin.buffer.read()
99
+ def main ():
100
+ claim = sys.stdin.buffer.read()
97
101
98
- msg = CoseMessage .decode(claim)
102
+ msg = Sign1Message .decode(claim, tag = True )
99
103
100
- if pycose.headers.ContentType not in msg.phdr:
101
- raise ClaimInvalidError(" Claim does not have a content type header parameter" )
102
- if COSE_Headers_Issuer not in msg.phdr:
103
- raise ClaimInvalidError(" Claim does not have an issuer header parameter" )
104
+ if pycose.headers.ContentType not in msg.phdr:
105
+ raise ClaimInvalidError(" Claim does not have a content type header parameter" )
106
+ if not msg.phdr[pycose.headers.ContentType].startswith(" application/json" ):
107
+ raise TypeError (
108
+ f " Claim content type does not start with application/json: { msg.phdr[pycose.headers.ContentType]!r } "
109
+ )
104
110
105
- if not msg.phdr[pycose.headers.ContentType].startswith(" application/json" ):
106
- raise TypeError (
107
- f " Claim content type does not start with application/json: { msg.phdr[pycose.headers.ContentType]!r } "
111
+ verification_key = verify_statement(msg)
112
+ unittest.TestCase().assertTrue(
113
+ verification_key,
114
+ " Failed to verify signature on statement" ,
108
115
)
109
116
110
- SCHEMA = json.loads(pathlib.Path(os.environ[" SCHEMA_PATH" ]).read_text())
117
+ cwt_protected = cwt.decode(msg.phdr[CWTClaims], verification_key.cwt)
118
+ issuer = cwt_protected[1 ]
119
+ subject = cwt_protected[2 ]
111
120
112
- try :
113
- validate(
114
- instance = {
115
- " $schema" : " https://schema.example.com/scitt-policy-engine-jsonschema.schema.json" ,
116
- " issuer" : msg.phdr[COSE_Headers_Issuer ],
117
- " claim" : json.loads(msg.payload.decode()),
118
- },
119
- schema = SCHEMA ,
121
+ issuer_key_as_object = verification_key_to_object(verification_key)
122
+ unittest.TestCase().assertTrue(
123
+ issuer_key_as_object,
124
+ " Failed to convert issuer key to JSON schema verifiable object" ,
120
125
)
121
- except ValidationError as error:
122
- print (str (error), file = sys.stderr)
123
- sys.exit(1 )
126
+
127
+ SCHEMA = json.loads(pathlib.Path(os.environ[" SCHEMA_PATH" ]).read_text())
128
+
129
+ try :
130
+ validate(
131
+ instance = {
132
+ " $schema" : " https://schema.example.com/scitt-policy-engine-jsonschema.schema.json" ,
133
+ " issuer" : issuer,
134
+ " issuer_key" : issuer_key_as_object,
135
+ " subject" : subject,
136
+ " claim" : json.loads(msg.payload.decode()),
137
+ },
138
+ schema = SCHEMA ,
139
+ )
140
+ except ValidationError as error:
141
+ print (str (error), file = sys.stderr)
142
+ sys.exit(1 )
143
+
144
+
145
+ if __name__ == " __main__" :
146
+ main()
124
147
```
125
148
126
149
We'll create a small wrapper to serve in place of a more fully featured policy
@@ -140,21 +163,134 @@ echo ${CLAIM_PATH}
140
163
Example running allowlist check and enforcement.
141
164
142
165
``` console
143
- npm install -g nodemon
144
- nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xe policy_engine.sh $(cat workspace/service_parameters.json | jq -r .insertPolicy) {} \;'
166
+ $ npm install nodemon && \
167
+ DID_WEB_ASSUME_SCHEME=http node_modules/.bin/ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xe policy_engine.sh $(cat workspace/service_parameters.json | jq -r .insertPolicy) {} \;'
145
168
```
146
169
147
170
Also ensure you restart the server with the new config we edited.
148
171
149
172
``` console
150
- scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro
173
+ $ scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro
174
+ ```
175
+
176
+ The current emulator notary (create-statement) implementation will sign
177
+ statements using a generated ephemeral key or a key we provide via the
178
+ ` --private-key-pem ` argument.
179
+
180
+ Since we need to export the key for verification by the policy engine, we will
181
+ first generate it using ` ssh-keygen ` .
182
+
183
+ ``` console
184
+ $ export ISSUER_PORT=" 9000" \
185
+ && export ISSUER_URL="http://localhost:${ISSUER_PORT}" \
186
+ && ssh-keygen -q -f /dev/stdout -t ecdsa -b 384 -N '' -I $RANDOM <<<y 2>/dev/null | python -c 'import sys; from cryptography.hazmat.primitives import serialization; print(serialization.load_ssh_private_key(sys.stdin.buffer.read(), password=None).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode().rstrip())' > private-key.pem \
187
+ && scitt-emulator client create-claim \
188
+ --private-key-pem private-key.pem \
189
+ --issuer "${ISSUER_URL}" \
190
+ --subject "solar" \
191
+ --content-type application/json \
192
+ --payload '{"sun": "yellow"}' \
193
+ --out claim.cose
151
194
```
152
195
153
- Create claim from allowed issuer (` .org ` ) and from non-allowed (` .com ` ).
196
+ The core of policy engine we implemented in ` jsonschema_validator.py ` will
197
+ verify the COSE message generated using the public portion of the notary's key.
198
+ We've implemented two possible styles of key resolution. Both of them require
199
+ resolution of public keys via an HTTP server.
200
+
201
+ Let's start the HTTP server now, we'll populate the needed files in the
202
+ sections corresponding to each resolution style.
203
+
204
+ ``` console
205
+ $ python -m http.server " ${ISSUER_PORT} " &
206
+ $ python_http_server_pid=$!
207
+ ```
208
+
209
+ ### SSH ` authorized_keys ` style notary public key resolution
210
+
211
+ Keys are discovered via making an HTTP GET request to the URL given by the
212
+ ` issuer ` parameter via the ` web ` DID method and de-serializing the SSH
213
+ public keys found within the response body.
214
+
215
+ GitHub exports a users authentication keys at https://github.com/username.keys
216
+ Leveraging this URL as an issuer ` did:web:github.com:username.keys ` with the
217
+ following pattern would enable a GitHub user to act as a SCITT notary.
218
+
219
+ Start an HTTP server with an SSH public key served at the root.
220
+
221
+ ``` console
222
+ $ cat private-key.pem | ssh-keygen -f /dev/stdin -y | tee index.html
223
+ ```
224
+
225
+ ### OpenID Connect token style notary public key resolution
226
+
227
+ Keys are discovered two part resolution of HTTP paths relative to the issuer
228
+
229
+ ` /.well-known/openid-configuration ` path is requested via HTTP GET. The
230
+ response body is parsed as JSON and the value of the ` jwks_uri ` key is
231
+ requested via HTTP GET.
232
+
233
+ ` /.well-known/jwks ` (is typically the value of ` jwks_uri ` ) path is requested
234
+ via HTTP GET. The response body is parsed as JSON. Public keys are loaded
235
+ from the value of the ` keys ` key which stores an array of JSON Web Key (JWK)
236
+ style serializations.
237
+
238
+ ``` console
239
+ $ mkdir -p .well-known/
240
+ $ cat > .well-known/openid-configuration << EOF
241
+ {
242
+ "issuer": "${ISSUER_URL}",
243
+ "jwks_uri": "${ISSUER_URL}/.well-known/jwks",
244
+ "response_types_supported": ["id_token"],
245
+ "claims_supported": ["sub", "aud", "exp", "iat", "iss"],
246
+ "id_token_signing_alg_values_supported": ["ES384"],
247
+ "scopes_supported": ["openid"]
248
+ }
249
+ EOF
250
+ $ cat private-key.pem | python -c ' import sys, json, jwcrypto.jwt; key = jwcrypto.jwt.JWK(); key.import_from_pem(sys.stdin.buffer.read()); print(json.dumps({"keys":[{**key.export_public(as_dict=True),"use": "sig","kid": key.thumbprint()}]}, indent=4, sort_keys=True))' | tee .well-known/jwks
251
+ {
252
+ "keys": [
253
+ {
254
+ "crv": "P-384",
255
+ "kid": "y96luxaBaw6FeWVEMti_iqLWPSYk8cKLzZG8X45PA2k",
256
+ "kty": "EC",
257
+ "use": "sig",
258
+ "x": "ZQazDzYmcMHF5Dstkbw7SwWvR_oXQHFS-TLppri-0xDby8TmCpzHyr6TH03CLBxj",
259
+ "y": "lsIbRskEv06Rf0vttkB3vpXdZ-a50ck74MVyRwOvN55P4s8usQAm3PY1KnAgWtHF"
260
+ }
261
+ ]
262
+ }
263
+ ```
264
+
265
+ ### SCITT SRCAPI transparency configuration public key resolution
266
+
267
+ Keys are discovered via making an HTTP GET request to the URL given by the
268
+ ` issuer ` parameter with ` /.well-known/transparency-configuration ` as the path
269
+ component. Public keys found within the response body's JSON ` jwks.keys ` array.
270
+
271
+ - [ ` https://transparency.example/.well-known/transparency-configuration ` ] ( https://ietf-wg-scitt.github.io/draft-ietf-scitt-scrapi/draft-ietf-scitt-scrapi.html#name-transparency-configuration )
272
+
273
+ To use this method of resolution create the statement using the FQDN of the
274
+ SCITT SCRAPI service as the issuer. Also ensure you use it's private key to
275
+ sign.
276
+
277
+ ``` console
278
+ $ scitt-emulator client create-claim \
279
+ --private-key-pem workspace/storage/service_private_key.pem \
280
+ --issuer "http://localhost:8000" \
281
+ --subject "solar" \
282
+ --content-type application/json \
283
+ --payload '{"sun": "yellow"}' \
284
+ --out claim.cose
285
+ ```
286
+
287
+ ### Policy engine executing allowlist policy on denied issuer
288
+
289
+ Attempt to submit the statement we created. You should see that due to our
290
+ current ` allowlist.schema.json ` the Transparency Service denied the insertion
291
+ of the statement into the log.
154
292
155
293
``` console
156
- $ scitt-emulator client create-claim --issuer did:web:example.com --content-type application/json --payload ' {"sun": "yellow"}' --out claim.cose
157
- A COSE-signed Claim was written to: claim.cose
158
294
$ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor
159
295
Traceback (most recent call last):
160
296
File "/home/alice/.local/bin/scitt-emulator", line 33, in <module>
@@ -174,10 +310,29 @@ Failed validating 'enum' in schema['properties']['issuer']:
174
310
175
311
On instance['issuer']:
176
312
'did:web:example.com'
313
+ ```
314
+
315
+ ### Policy engine executing allowlist policy on allowed issuer
316
+
317
+ Modify the allowlist to ensure that our issuer, aka our local HTTP server with
318
+ our keys, is set to be the allowed issuer.
319
+
320
+ ``` console
321
+ $ export allowlist=" $( cat allowlist.schema.json) " && \
322
+ jq '.properties.issuer.enum = [env.ISSUER_URL, "http://localhost:8000"]' <(echo "${allowlist}") \
323
+ | tee allowlist.schema.json
324
+ ```
177
325
178
- $ scitt-emulator client create-claim --issuer did:web:example.org --content-type application/json --payload ' {"sun": "yellow"}' --out claim.cose
179
- A COSE signed Claim was written to: claim.cose
326
+ Submit the statement from the issuer we just added to the allowlist.
327
+
328
+ ``` console
180
329
$ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor
181
330
Claim registered with entry ID 1
182
331
Receipt written to claim.receipt.cbor
183
332
```
333
+
334
+ Stop the server that serves the public keys
335
+
336
+ ``` console
337
+ $ kill $python_http_server_pid
338
+ ```
0 commit comments