"""
NUC envelope.
"""
import base64
from hashlib import sha256
import json
from dataclasses import dataclass
from typing import List
from secp256k1 import PublicKey
from nuc.token import NucToken
[docs]
@dataclass
class DecodedNucToken:
"""
A decoded NUC token.
"""
raw_header: str
raw_payload: str
signature: bytes
token: NucToken
[docs]
@staticmethod
def parse(data: str) -> "DecodedNucToken":
"""
Parse a token from its serialized JWT form.
Note that this only parses the token and ensures it is structurally correct. This does not perform
any form of signature validation.
.. note:: Users should use :class:`NucTokenEnvelope` to parse tokens.
"""
parts = data.split(".", 2)
if len(parts) != 3:
raise MalformedNucJwtException("invalid JWT structure")
(raw_header, raw_payload, signature) = parts
header = urlsafe_base64_decode(raw_header)
try:
header = json.loads(header)
except Exception as ex:
raise MalformedNucJwtException("invalid header") from ex
if not isinstance(header, dict):
raise MalformedNucJwtException(
f"invalid JWT header type: {type(header).__name__}"
)
if header.get("alg") != "ES256K":
raise MalformedNucJwtException("invalid JWT algorithm")
if len(header) != 1:
raise MalformedNucJwtException("unexpected keys in header")
payload = urlsafe_base64_decode(raw_payload)
token = NucToken.parse(payload)
signature = urlsafe_base64_decode(signature)
return DecodedNucToken(raw_header, raw_payload, signature, token)
[docs]
def serialize(self) -> str:
"""
Serialize this token as a JWT.
"""
return f"{self.raw_header}.{self.raw_payload}.{urlsafe_base64_encode(self.signature)}"
[docs]
def validate_signature(self) -> None:
"""
Validate the signature in this token.
"""
public_key = PublicKey(self.token.issuer.public_key, raw=True)
payload = f"{self.raw_header}.{self.raw_payload}".encode("utf8")
signature = public_key.ecdsa_deserialize_compact(self.signature)
if not public_key.ecdsa_verify(payload, signature):
raise InvalidSignatureException("signature verification failed")
[docs]
def compute_hash(self) -> bytes:
"""
Compute the hash for this token.
"""
hash_input = self.serialize().encode("utf8")
return sha256(hash_input).digest()
[docs]
class NucTokenEnvelope:
"""
A NUC token envelope, containing a parsed token along with all its proofs
"""
def __init__(self, token: DecodedNucToken, proofs: List[DecodedNucToken]) -> None:
self.token = token
self.proofs = proofs
[docs]
@staticmethod
def parse(data: str) -> "NucTokenEnvelope":
"""
Parse a NUC token envelope from its serialized JWT form.
Note that this only parses the envelope and ensures it is structurally correct. This does not perform
any form of signature validation.
Example
-------
.. code-block:: py3
from nuc.envelope import NucTokenEnvelope
raw_token = "....."
token = NucTokenEnvelope.parse(raw_token)
"""
tokens = data.split("/")
if len(tokens) == 0:
raise MalformedNucJwtException("no tokens found")
token = DecodedNucToken.parse(tokens[0])
proofs = [DecodedNucToken.parse(token) for token in tokens[1:]]
return NucTokenEnvelope(token, proofs)
[docs]
def validate_signatures(self) -> None:
"""
Validate the signature in this envelope.
This will raise an exception is the token or any of its proofs is not signed by its issuer.
"""
for token in [self.token, *self.proofs]:
token.validate_signature()
[docs]
def serialize(self) -> str:
"""
Serialize this envelope as a JWT-like string.
"""
token = self.token.serialize()
if not self.proofs:
return token
proofs = "/".join(proof.serialize() for proof in self.proofs)
return f"{token}/{proofs}"
[docs]
class InvalidSignatureException(Exception):
"""
An exception thrown when signature verification fails.
"""
[docs]
def urlsafe_base64_decode(data: str) -> bytes:
"""
Encode an input as URL safe base64.
"""
# python's urlsafe decoding actually needs `=` which shouldn't actually be there so we append them as necessary
padding = 4 - (len(data) % 4)
data = data + ("=" * padding)
try:
return base64.urlsafe_b64decode(data)
except Exception as ex:
raise MalformedNucJwtException("invalid base64") from ex
[docs]
def urlsafe_base64_encode(data: bytes) -> str:
"""
Encode URL safe base64.
"""
# same as above but for encoding
encoded = base64.urlsafe_b64encode(data)
return encoded.rstrip(b"=").decode("utf8")