Skip to content

Commit a0e179b

Browse files
committed
verify statement: As standalone file
Signed-off-by: John Andersen <[email protected]>
1 parent 3d1020b commit a0e179b

12 files changed

+511
-109
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ jobs:
3838
with:
3939
activate-environment: scitt
4040
environment-file: environment.yml
41-
- run: python -m pytest
41+
- run: |
42+
python -m pip install -e .
43+
python -m pytest
4244
4345
ci-cd-build-and-push-image-container:
4446
name: CI/CD (container)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ They can be used with the built-in server or an external service implementation.
9191

9292
```sh
9393
./scitt-emulator.sh client create-claim \
94-
--issuer did:web:example.com \
9594
--content-type application/json \
9695
--subject 'solar' \
9796
--payload '{"sun": "yellow"}' \
9897
--out claim.cose
9998
```
10099

101-
_**Note:** The emulator generates an ad-hoc key pair to sign the claim and does not verify claim signatures upon submission._
100+
_**Note:** The emulator generates an ad-hoc key pair to sign the claim if
101+
``--issuer`` and ``--public-key-pem`` are not given. See [Registration Policies](docs/registration_policies.md) docs for more deatiled examples_
102102

103103
2. View the signed claim by uploading `claim.cose` to one of the [CBOR or COSE Debugging Tools](#cose-and-cbor-debugging)
104104

docs/registration_policies.md

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -104,108 +104,7 @@ import cryptography.hazmat.primitives.serialization
104104
import jwcrypto.jwk
105105

106106
from scitt_emulator.scitt import ClaimInvalidError, CWTClaims
107-
108-
109-
def did_web_to_url(
110-
did_web_string, scheme=os.environ.get("DID_WEB_ASSUME_SCHEME", "https")
111-
):
112-
return "/".join(
113-
[
114-
f"{scheme}:/",
115-
*[urllib.parse.unquote(i) for i in did_web_string.split(":")[2:]],
116-
]
117-
)
118-
119-
120-
def verify_signature(msg: Sign1Message) -> bool:
121-
"""
122-
- TODOs
123-
- Should we use audiance? I think no, just want to make sure we've
124-
documented why thought if not. No usage makes sense to me becasue we
125-
don't know the intended audiance, it could be federated into
126-
multiple TS
127-
- Can you just pass a whole public key as an issuer?
128-
- Resolve DID keys (since that is what the arch says...)
129-
"""
130-
131-
# Figure out what the issuer is
132-
cwt_cose_loads = cwt.cose.COSE()._loads
133-
cwt_unverified_protected = cwt_cose_loads(
134-
cwt_cose_loads(msg.phdr[CWTClaims]).value[2]
135-
)
136-
unverified_issuer = cwt_unverified_protected[1]
137-
138-
if unverified_issuer.startswith("did:web:"):
139-
unverified_issuer = did_web_to_url(unverified_issuer)
140-
141-
# Load keys from issuer
142-
jwk_keys = []
143-
cwt_cose_keys = []
144-
pycose_cose_keys = []
145-
146-
from cryptography.hazmat.primitives import serialization
147-
148-
cryptography_ssh_keys = []
149-
if "://" in unverified_issuer and not unverified_issuer.startswith("file://"):
150-
# TODO Logging for URLErrors
151-
# Check if OIDC issuer
152-
unverified_issuer_parsed_url = urllib.parse.urlparse(unverified_issuer)
153-
openid_configuration_url = unverified_issuer_parsed_url._replace(
154-
path="/.well-known/openid-configuration",
155-
).geturl()
156-
with contextlib.suppress(urllib.request.URLError):
157-
with urllib.request.urlopen(openid_configuration_url) as response:
158-
if response.status == 200:
159-
openid_configuration = json.loads(response.read())
160-
jwks_uri = openid_configuration["jwks_uri"]
161-
with urllib.request.urlopen(jwks_uri) as response:
162-
if response.status == 200:
163-
jwks = json.loads(response.read())
164-
for jwk_key_as_dict in jwks["keys"]:
165-
jwk_key_as_string = json.dumps(jwk_key_as_dict)
166-
jwk_keys.append(
167-
jwcrypto.jwk.JWK.from_json(jwk_key_as_string),
168-
)
169-
170-
# Try loading ssh keys. Example: https://github.com/username.keys
171-
with contextlib.suppress(urllib.request.URLError):
172-
with urllib.request.urlopen(unverified_issuer) as response:
173-
while line := response.readline():
174-
with contextlib.suppress(
175-
(ValueError, cryptography.exceptions.UnsupportedAlgorithm)
176-
):
177-
cryptography_ssh_keys.append(
178-
cryptography.hazmat.primitives.serialization.load_ssh_public_key(
179-
line
180-
)
181-
)
182-
183-
for cryptography_ssh_key in cryptography_ssh_keys:
184-
jwk_keys.append(
185-
jwcrypto.jwk.JWK.from_pem(
186-
cryptography_ssh_key.public_bytes(
187-
encoding=serialization.Encoding.PEM,
188-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
189-
)
190-
)
191-
)
192-
193-
for jwk_key in jwk_keys:
194-
cwt_cose_key = cwt.COSEKey.from_pem(
195-
jwk_key.export_to_pem(),
196-
kid=jwk_key.thumbprint(),
197-
)
198-
cwt_cose_keys.append(cwt_cose_key)
199-
cwt_ec2_key_as_dict = cwt_cose_key.to_dict()
200-
pycose_cose_key = pycose.keys.ec2.EC2Key.from_dict(cwt_ec2_key_as_dict)
201-
pycose_cose_keys.append((cwt_cose_key, pycose_cose_key))
202-
203-
for cwt_cose_key, pycose_cose_key in pycose_cose_keys:
204-
with contextlib.suppress(Exception):
205-
msg.key = pycose_cose_key
206-
verify_signature = msg.verify_signature()
207-
if verify_signature:
208-
return cwt_cose_key, pycose_cose_key
107+
from scitt_emulator.verify_statement import verify_statement
209108

210109

211110
def main():
@@ -220,7 +119,7 @@ def main():
220119
f"Claim content type does not start with application/json: {msg.phdr[pycose.headers.ContentType]!r}"
221120
)
222121

223-
cwt_cose_key, _pycose_cose_key = verify_signature(msg)
122+
cwt_cose_key, _pycose_cose_key = verify_statement(msg)
224123
unittest.TestCase().assertTrue(
225124
cwt_cose_key,
226125
"Failed to verify signature on statement",

environment.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ dependencies:
4040
- PyJWT==2.8.0
4141
- werkzeug==2.2.2
4242
- cwt==2.7.1
43+
- py-multibase==1.0.3
44+
- py-multicodec==0.2.1

pytest.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[pytest]
2+
# https://docs.pytest.org/en/7.1.x/how-to/doctest.html#using-doctest-options
3+
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
4+
# Alternatively, options can be enabled by an inline comment in the doc test itself:
5+
# >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL
6+
# Traceback (most recent call last):
7+
# ValueError: ...
8+
addopts = --doctest-modules

scitt_emulator/did_helpers.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import os
2+
import ast
3+
import sys
4+
import base64
5+
import inspect
6+
import urllib.parse
7+
from typing import Optional, Callable, Dict, Tuple, Union
8+
9+
import multibase
10+
import multicodec
11+
import cryptography.hazmat.primitives.asymmetric.ec
12+
13+
14+
def did_web_to_url(
15+
did_web_string: str,
16+
*,
17+
scheme: Optional[str] = None,
18+
):
19+
if scheme is None:
20+
scheme = os.environ.get("DID_WEB_ASSUME_SCHEME", "https")
21+
return "/".join(
22+
[
23+
f"{scheme}:/",
24+
*[urllib.parse.unquote(i) for i in did_web_string.split(":")[2:]],
25+
]
26+
)
27+
28+
29+
class DIDKeyInvalidPublicKeyLengthError(ValueError):
30+
"""
31+
If the byte length of rawPublicKeyBytes does not match the expected public
32+
key length for the associated multicodecValue, an invalidPublicKeyLength
33+
error MUST be raised.
34+
"""
35+
36+
37+
class DIDKeyDecoderNotFoundError(NotImplementedError):
38+
"""
39+
Raised when we don't have a function implemented to decode the given key
40+
"""
41+
42+
43+
class DIDKeyDecoderError(Exception):
44+
"""
45+
Raised when we failed to decode a key from a did:key DID method
46+
"""
47+
48+
49+
class DIDKeyInvalidPublicKeyError(DIDKeyDecoderError):
50+
"""
51+
Raised when the raw bytes of a key are invalid during decode
52+
"""
53+
54+
55+
DID_KEY_METHOD = "did:key:"
56+
57+
58+
def did_key_decode_public_key(multibase_value: str) -> Tuple[bytes, bytes]:
59+
# 3.1.2.3
60+
# Decode multibaseValue using the base58-btc multibase alphabet and set
61+
# multicodecValue to the multicodec header for the decoded value.
62+
multibase_value_decoded = multibase.decode(multibase_value)
63+
# Implementers are cautioned to ensure that the multicodecValue is set to
64+
# the result after performing varint decoding.
65+
multicodec_value = multicodec.extract_prefix(multibase_value_decoded)
66+
# Set the rawPublicKeyBytes to the bytes remaining after the multicodec
67+
# header.
68+
raw_public_key_bytes = multicodec.remove_prefix(multibase_value_decoded)
69+
# Return multicodecValue and rawPublicKeyBytes as the decodedPublicKey.
70+
return multicodec_value, raw_public_key_bytes
71+
72+
73+
class _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
74+
pass
75+
76+
77+
MULTICODEC_VALUE_NOT_FOUND_IN_TABLE = _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE()
78+
79+
# Multicodec hexadecimal value, public key, byte length, Description
80+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY = 0xE7
81+
MULTICODEC_HEX_X25519_PUBLIC_KEY = 0xEC
82+
MULTICODEC_HEX_ED25519_PUBLIC_KEY = 0xED
83+
MULTICODEC_HEX_P256_PUBLIC_KEY = 0x1200
84+
MULTICODEC_HEX_P384_PUBLIC_KEY = 0x1201
85+
MULTICODEC_HEX_P521_PUBLIC_KEY = 0x1202
86+
MULTICODEC_HEX_RSA_PUBLIC_KEY = 0x1205
87+
88+
MULTICODEC_VALUE_TABLE = {
89+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY: 33, # secp256k1-pub - Secp256k1 public key (compressed)
90+
MULTICODEC_HEX_X25519_PUBLIC_KEY: 32, # x25519-pub - Curve25519 public key
91+
MULTICODEC_HEX_ED25519_PUBLIC_KEY: 32, # ed25519-pub - Ed25519 public key
92+
MULTICODEC_HEX_P256_PUBLIC_KEY: 33, # p256-pub - P-256 public key (compressed)
93+
MULTICODEC_HEX_P384_PUBLIC_KEY: 49, # p384-pub - P-384 public key (compressed)
94+
MULTICODEC_HEX_P521_PUBLIC_KEY: None, # p521-pub - P-521 public key (compressed)
95+
MULTICODEC_HEX_RSA_PUBLIC_KEY: None, # rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
96+
}
97+
98+
99+
def did_key_signature_method_creation(
100+
multibase_value: hex,
101+
raw_public_key_bytes: bytes,
102+
) -> Union[cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey]:
103+
# 3.1.2 https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
104+
# Initialize verificationMethod to an empty object.
105+
verification_method = {}
106+
107+
# Set multicodecValue and rawPublicKeyBytes to the result of passing
108+
# multibaseValue and options to § 3.1.3 Decode Public Key Algorithm.
109+
# Ensure the proper key length of rawPublicKeyBytes based on the
110+
# multicodecValue table
111+
public_key_length_MUST_be = MULTICODEC_VALUE_TABLE.get(
112+
multibase_value, MULTICODEC_VALUE_NOT_FOUND_IN_TABLE
113+
)
114+
if public_key_length_MUST_be is MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
115+
raise DIDKeyDecoderNotFoundError(
116+
f"multibase_value {multibase_value!r} not in MULTICODEC_VALUE_NOT_FOUND_IN_TABLE {MULTICODEC_VALUE_NOT_FOUND_IN_TABLE!r}"
117+
)
118+
119+
# If the byte length of rawPublicKeyBytes does not match the expected public
120+
# key length for the associated multicodecValue, an invalidPublicKeyLength
121+
# error MUST be raised.
122+
if public_key_length_MUST_be is not None and public_key_length_MUST_be != len(
123+
raw_public_key_bytes
124+
):
125+
raise DIDKeyInvalidPublicKeyLengthError(
126+
f"public_key_length_MUST_be: {public_key_length_MUST_be } != len(raw_public_key_bytes): {len(raw_public_key_bytes)}"
127+
)
128+
129+
# Ensure the rawPublicKeyBytes are a proper encoding of the public key type
130+
# as specified by the multicodecValue. This validation is often done by a
131+
# cryptographic library when importing the public key by, for example,
132+
# ensuring that an Elliptic Curve public key is a specific coordinate that
133+
# exists on the elliptic curve. If an invalid public key value is detected,
134+
# an invalidPublicKey error MUST be raised.
135+
#
136+
# SPEC ISSUE: Request for feedback on implementability: It is not clear if
137+
# this particular check is implementable across all public key types. The
138+
# group is accepting feedback on the implementability of this particular
139+
# feature.
140+
try:
141+
if multibase_value in (
142+
MULTICODEC_HEX_P256_PUBLIC_KEY,
143+
MULTICODEC_HEX_P384_PUBLIC_KEY,
144+
MULTICODEC_HEX_P521_PUBLIC_KEY,
145+
):
146+
public_key = cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey.from_encoded_point(
147+
cryptography.hazmat.primitives.asymmetric.ec.SECP384R1(),
148+
raw_public_key_bytes,
149+
)
150+
else:
151+
raise DIDKeyDecoderNotFoundError(
152+
f"No importer for multibase_value {multibase_value!r}"
153+
)
154+
except Exception as e:
155+
raise DIDKeyInvalidPublicKeyError(
156+
f"invalid raw_public_key_bytes: {raw_public_key_bytes!r}"
157+
) from e
158+
159+
return public_key
160+
161+
162+
def did_key_to_cryptography_key(
163+
did_key: str,
164+
) -> Union[cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey]:
165+
"""
166+
References
167+
168+
- https://w3c-ccg.github.io/did-method-key/#p-384
169+
- RFC7515: JSON Web Key (JWK): https://www.rfc-editor.org/rfc/rfc7517
170+
- RFC8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE): https://www.rfc-editor.org/rfc/rfc8037
171+
172+
Examples
173+
174+
- P-384: https://github.com/w3c-ccg/did-method-key/blob/f5abee840c31e92cd1ac11737e0b62103ab99d21/test-vectors/nist-curves.json#L112-L166
175+
176+
>>> did_key_to_cryptography_key("did:key:invalid")
177+
Traceback (most recent call last):
178+
DIDKeyDecoderNotFoundError: ...
179+
>>> public_key = did_key_to_cryptography_key("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9")
180+
>>> public_key.__class__
181+
<class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey'>
182+
"""
183+
try:
184+
multibase_value, raw_public_key_bytes = did_key_decode_public_key(
185+
did_key.replace(DID_KEY_METHOD, "", 1)
186+
)
187+
except Exception as e:
188+
raise DIDKeyDecoderNotFoundError(did_key) from e
189+
190+
try:
191+
return did_key_signature_method_creation(multibase_value, raw_public_key_bytes)
192+
except Exception as e:
193+
raise DIDKeyDecoderError(did_key) from e
194+
195+
raise DIDKeyDecoderNotFoundError(did_key)

0 commit comments

Comments
 (0)