Source code for keri.core.signing

# -*- coding: utf-8 -*-
"""
keri.core.signing module

Provides support Signer class
"""
from dataclasses import dataclass, astuple, asdict
from collections import namedtuple

import pysodium

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric import ec, utils

from ..kering import (EmptyMaterialError, InvalidCodeError, InvalidSizeError,
                      InvalidValueError, InvalidTypeError)

from .coring import Matter, MtrDex, Verfer, Cigar
from .indexing import IdrDex, Siger


DSS_SIG_MODE = "fips-186-3"
ECDSA_256r1_SEEDBYTES = 32
ECDSA_256k1_SEEDBYTES = 32

# secret derivation security tier
Tierage = namedtuple("Tierage", 'low med high')

Tiers = Tierage(low='low', med='med', high='high')


[docs] class Signer(Matter): """ Signer is Matter subclass with method to create signature of serialization using: - .raw as signing (private) key seed, - .code as cipher suite for signing - .verfer whose property .raw is public key for signing. If not provided .verfer is generated from private key seed using .code as cipher suite for creating key-pair. See Matter for inherited attributes and properties: Inherited Properties: - code (str): hard part of derivation code to indicate cypher suite - both (int): hard and soft parts of full text code - size (int): Number of triplets of bytes including lead bytes (quadlets of chars) of variable sized material. Value of soft size, ss, part of full text code. Otherwise None. - rize (int): number of bytes of raw material not including lead bytes - raw (bytes): private signing key crypto material only without code - qb64 (str): private signing key Base64 fully qualified with derivation code + crypto mat - qb64b (bytes): private signing keyBase64 fully qualified with derivation code + crypto mat - qb2 (bytes): private signing key binary with derivation code + crypto material - transferable (bool): True means transferable derivation code False otherwise - digestive (bool): True means digest derivation code False otherwise Properties: - .verfer is Verfer object instance of public key derived from private key seed which is .raw Methods: - sign: create signature """
[docs] def __init__(self, raw=None, code=MtrDex.Ed25519_Seed, transferable=True, **kwa): """Assign signing cipher suite function to ._sign Parameters: See Matter for inherted parameters. raw (bytes): crypto material for signing seed from which to generate private key code (str): derivation code of signing seed transferable (bool): True means make verifier code transferable False make non-transferable """ try: super(Signer, self).__init__(raw=raw, code=code, **kwa) except EmptyMaterialError as ex: if code == MtrDex.Ed25519_Seed: raw = pysodium.randombytes(pysodium.crypto_sign_SEEDBYTES) super(Signer, self).__init__(raw=raw, code=code, **kwa) elif code == MtrDex.ECDSA_256r1_Seed: raw = pysodium.randombytes(ECDSA_256r1_SEEDBYTES) super(Signer, self).__init__(raw=bytes(raw), code=code, **kwa) elif code == MtrDex.ECDSA_256k1_Seed: raw = pysodium.randombytes(ECDSA_256k1_SEEDBYTES) super(Signer, self).__init__(raw=bytes(raw), code=code, **kwa) else: raise ValueError("Unsupported signer code = {}.".format(code)) if self.code == MtrDex.Ed25519_Seed: self._sign = self._ed25519 verkey, sigkey = pysodium.crypto_sign_seed_keypair(self.raw) verfer = Verfer(raw=verkey, code=MtrDex.Ed25519 if transferable else MtrDex.Ed25519N) elif self.code == MtrDex.ECDSA_256r1_Seed: self._sign = self._secp256r1 d = int.from_bytes(self.raw, byteorder="big") sigkey = ec.derive_private_key(d, ec.SECP256R1()) verkey = sigkey.public_key().public_bytes(encoding=Encoding.X962, format=PublicFormat.CompressedPoint) verfer = Verfer(raw=verkey, code=MtrDex.ECDSA_256r1 if transferable else MtrDex.ECDSA_256r1N) elif self.code == MtrDex.ECDSA_256k1_Seed: self._sign = self._secp256k1 d = int.from_bytes(self.raw, byteorder="big") sigkey = ec.derive_private_key(d, ec.SECP256K1()) verkey = sigkey.public_key().public_bytes(encoding=Encoding.X962, format=PublicFormat.CompressedPoint) verfer = Verfer(raw=verkey, code=MtrDex.ECDSA_256k1 if transferable else MtrDex.ECDSA_256k1N) else: raise ValueError("Unsupported signer code = {}.".format(self.code)) self._verfer = verfer
@property def verfer(self): """Property verfer: Returns Verfer instance Assumes ._verfer is correctly assigned """ return self._verfer
[docs] def sign(self, ser, index=None, only=False, ondex=None, **kwa): """Returns either Cigar or Siger (indexed) instance of cryptographic signature material on bytes serialization ser If index is None return Cigar instance Else return Siger instance Parameters: ser (bytes): serialization to be signed index (int): main index of associated verifier key in event keys only (bool): True means main index only list, ondex ignored False means both index lists (default), ondex used ondex (int | None): other index offset into list such as prior next """ return (self._sign(ser=ser, seed=self.raw, verfer=self.verfer, index=index, only=only, ondex=ondex, **kwa))
@staticmethod def _ed25519(ser, seed, verfer, index, only=False, ondex=None, **kwa): """Returns signature as either Cigar or Siger instance as appropriate for Ed25519 digital signatures given index and ondex values The seed's code determins the crypto key-pair algorithm and signing suite The signature type, Cigar or Siger, and when indexed the Siger code may be completely determined by the seed and index values (index, ondex) by assuming that the index values are intentional. Without the seed code its more difficult for Siger to determine when for the Indexer code value should be changed from the than the provided value with respect to provided but incompatible index values versus error conditions. Parameters: ser (bytes): serialization to be signed seed (bytes): raw binary seed (private key) verfer (Verfer): instance. verfer.raw is public key index (int |None): main index offset into list such as current signing None means return non-indexed Cigar Not None means return indexed Siger with Indexer code derived from index, conly, and ondex values only (bool): True means main index only list, ondex ignored False means both index lists (default), ondex used ondex (int | None): other index offset into list such as prior next """ # compute raw signature sig using seed on serialization ser sig = pysodium.crypto_sign_detached(ser, seed + verfer.raw) if index is None: # Must be Cigar i.e. non-indexed signature return Cigar(raw=sig, code=MtrDex.Ed25519_Sig, verfer=verfer) else: # Must be Siger i.e. indexed signature # should add Indexer class method to get ms main index size for given code if only: # only main index ondex not used ondex = None if index <= 63: # (64 ** ms - 1) where ms is main index size code = IdrDex.Ed25519_Crt_Sig # use small current only else: code = IdrDex.Ed25519_Big_Crt_Sig # use big current only else: # both if ondex == None: ondex = index # enable default to be same if ondex == index and index <= 63: # both same and small code = IdrDex.Ed25519_Sig # use small both same else: # otherwise big or both not same so use big both code = IdrDex.Ed25519_Big_Sig # use use big both return Siger(raw=sig, code=code, index=index, ondex=ondex, verfer=verfer) @staticmethod def _secp256r1(ser, seed, verfer, index, only=False, ondex=None, **kwa): """Returns signature as either Cigar or Siger instance as appropriate for Ed25519 digital signatures given index and ondex values The seed's code determins the crypto key-pair algorithm and signing suite The signature type, Cigar or Siger, and when indexed the Siger code may be completely determined by the seed and index values (index, ondex) by assuming that the index values are intentional. Without the seed code its more difficult for Siger to determine when for the Indexer code value should be changed from the than the provided value with respect to provided but incompatible index values versus error conditions. Parameters: ser (bytes): serialization to be signed seed (bytes): raw binary seed (private key) verfer (Verfer): instance. verfer.raw is public key index (int |None): main index offset into list such as current signing None means return non-indexed Cigar Not None means return indexed Siger with Indexer code derived from index, conly, and ondex values only (bool): True means main index only list, ondex ignored False means both index lists (default), ondex used ondex (int | None): other index offset into list such as prior next """ # compute raw signature sig using seed on serialization ser d = int.from_bytes(seed, byteorder="big") sigkey = ec.derive_private_key(d, ec.SECP256R1()) der = sigkey.sign(ser, ec.ECDSA(hashes.SHA256())) (r, s) = utils.decode_dss_signature(der) sig = bytearray(r.to_bytes(32, "big")) sig.extend(s.to_bytes(32, "big")) if index is None: # Must be Cigar i.e. non-indexed signature return Cigar(raw=sig, code=MtrDex.ECDSA_256r1_Sig, verfer=verfer) else: # Must be Siger i.e. indexed signature # should add Indexer class method to get ms main index size for given code if only: # only main index ondex not used ondex = None if index <= 63: # (64 ** ms - 1) where ms is main index size code = IdrDex.ECDSA_256r1_Crt_Sig # use small current only else: code = IdrDex.ECDSA_256r1_Big_Crt_Sig # use big current only else: # both if ondex == None: ondex = index # enable default to be same if ondex == index and index <= 63: # both same and small code = IdrDex.ECDSA_256r1_Sig # use small both same else: # otherwise big or both not same so use big both code = IdrDex.ECDSA_256r1_Big_Sig # use use big both return Siger(raw=sig, code=code, index=index, ondex=ondex, verfer=verfer,) @staticmethod def _secp256k1(ser, seed, verfer, index, only=False, ondex=None, **kwa): """ Returns signature as either Cigar or Siger instance as appropriate for secp256k1 digital signatures given index and ondex values The seed's code determins the crypto key-pair algorithm and signing suite The signature type, Cigar or Siger, and when indexed the Siger code may be completely determined by the seed and index values (index, ondex) by assuming that the index values are intentional. Without the seed code its more difficult for Siger to determine when for the Indexer code value should be changed from the than the provided value with respect to provided but incompatible index values versus error conditions. Parameters: ser (bytes): serialization to be signed seed (bytes): raw binary seed (private key) verfer (Verfer): instance. verfer.raw is public key index (int |None): main index offset into list such as current signing None means return non-indexed Cigar Not None means return indexed Siger with Indexer code derived from index, conly, and ondex values only (bool): True means main index only list, ondex ignored False means both index lists (default), ondex used ondex (int | None): other index offset into list such as prior next """ # compute raw signature sig using seed on serialization ser d = int.from_bytes(seed, byteorder="big") sigkey = ec.derive_private_key(d, ec.SECP256K1()) der = sigkey.sign(ser, ec.ECDSA(hashes.SHA256())) (r, s) = utils.decode_dss_signature(der) sig = bytearray(r.to_bytes(32, "big")) sig.extend(s.to_bytes(32, "big")) if index is None: # Must be Cigar i.e. non-indexed signature return Cigar(raw=sig, code=MtrDex.ECDSA_256k1_Sig, verfer=verfer) else: # Must be Siger i.e. indexed signature # should add Indexer class method to get ms main index size for given code if only: # only main index ondex not used ondex = None if index <= 63: # (64 ** ms - 1) where ms is main index size code = IdrDex.ECDSA_256k1_Crt_Sig # use small current only else: code = IdrDex.ECDSA_256k1_Big_Crt_Sig # use big current only else: # both if ondex == None: ondex = index # enable default to be same if ondex == index and index <= 63: # both same and small code = IdrDex.ECDSA_256k1_Sig # use small both same else: # otherwise big or both not same so use big both code = IdrDex.ECDSA_256k1_Big_Sig # use use big both return Siger(raw=sig, code=code, index=index, ondex=ondex, verfer=verfer,)
[docs] class Salter(Matter): """Salter is Matter subclass to maintain random salt for secrets (private keys) Its .raw is random salt, .code as cipher suite for salt To initialize with deterministic salt pass in 16 bytes for raw: salter = Salter(raw=b'0123456789abcdef') To create a deterministic secret, seed, or private key from salt call .signer:: signer = salter.signer(code=MtrDex.Ed25519_Seed, transferable=True, path="", tier=None, temp=False) To create a deterministic set of secrets or seeds or private keys from salt call signers:: signers = salter.signers(count=1, start=0, path="", code=MtrDex.Ed25519_Seed, transferable=True, tier=None, temp=False) Attributes: .level is str security level code. Provides default level Inherited Properties .pad is int number of pad chars given raw .code is str derivation code to indicate cypher suite .raw is bytes crypto material only without code .index is int count of attached crypto material by context (receipts) .qb64 is str in Base64 fully qualified with derivation code + crypto mat .qb64b is bytes in Base64 fully qualified with derivation code + crypto mat .qb2 is bytes in binary with derivation code + crypto material .transferable is Boolean, True when transferable derivation code False otherwise Properties: Methods: Hidden: ._pad is method to compute .pad property ._code is str value for .code property ._raw is bytes value for .raw property ._index is int value for .index property ._infil is method to compute fully qualified Base64 from .raw and .code ._exfil is method to extract .code and .raw from fully qualified Base64 """ Tier = Tiers.low
[docs] def __init__(self, raw=None, code=MtrDex.Salt_128, tier=None, **kwa): """ Initialize salter's raw and code Inherited Parameters: raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes Parameters: """ try: super(Salter, self).__init__(raw=raw, code=code, **kwa) except EmptyMaterialError as ex: if code == MtrDex.Salt_128: raw = pysodium.randombytes(pysodium.crypto_pwhash_SALTBYTES) super(Salter, self).__init__(raw=raw, code=code, **kwa) else: raise ValueError("Unsupported salter code = {}.".format(code)) if self.code not in (MtrDex.Salt_128,): raise ValueError("Unsupported salter code = {}.".format(self.code)) self.tier = tier if tier is not None else self.Tier
[docs] def stretch(self, *, size=32, path="", tier=None, temp=False): """ Returns (bytes): raw binary seed (secret) derived from path and .raw and stretched to size given by code using argon2d stretching algorithm. Parameters: size (int): number of bytes in stretched seed path (str): unique chars used in derivation of seed (secret) tier (str): value from Tierage for security level of stretch temp is Boolean, True means use quick method to stretch salt for testing only, Otherwise use time set by tier to stretch """ tier = tier if tier is not None else self.tier if temp: opslimit = 1 # pysodium.crypto_pwhash_OPSLIMIT_MIN memlimit = 8192 # pysodium.crypto_pwhash_MEMLIMIT_MIN else: if tier == Tiers.low: opslimit = 2 # pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE memlimit = 67108864 # pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE elif tier == Tiers.med: opslimit = 3 # pysodium.crypto_pwhash_OPSLIMIT_MODERATE memlimit = 268435456 # pysodium.crypto_pwhash_MEMLIMIT_MODERATE elif tier == Tiers.high: opslimit = 4 # pysodium.crypto_pwhash_OPSLIMIT_SENSITIVE memlimit = 1073741824 # pysodium.crypto_pwhash_MEMLIMIT_SENSITIVE else: raise ValueError("Unsupported security tier = {}.".format(tier)) # stretch algorithm is argon2id seed = pysodium.crypto_pwhash(outlen=size, passwd=path, salt=self.raw, opslimit=opslimit, memlimit=memlimit, alg=pysodium.crypto_pwhash_ALG_ARGON2ID13) return (seed)
[docs] def signer(self, *, code=MtrDex.Ed25519_Seed, transferable=True, path="", tier=None, temp=False): """ Returns Signer instance whose .raw secret is derived from path and salter's .raw and stretched to size given by code. The signers public key for its .verfer is derived from code and transferable. Parameters: code is str code of secret crypto suite transferable is Boolean, True means use transferace code for public key path is str of unique chars used in derivation of secret seed for signer tier is str Tierage security level temp is Boolean, True means use quick method to stretch salt for testing only, Otherwise use more time to stretch """ seed = self.stretch(size=Matter._rawSize(code), path=path, tier=tier, temp=temp) return (Signer(raw=seed, code=code, transferable=transferable))
[docs] def signers(self, count=1, start=0, path="", **kwa): """ Returns list of count number of Signer instances with unique derivation path made from path prefix and suffix of start plus offset for each count value from 0 to count - 1. See .signer for parameters used to create each signer. """ return [self.signer(path=f"{path}{i + start:x}", **kwa) for i in range(count)]
# Codes for for ciphers of variable sized sniffable QB2 or QB64 plain text
[docs] @dataclass(frozen=True) class CipherX25519VarStrmCodex: """ CipherX25519VarCodex is codex all variable sized cipher bytes derivation codes for sealed box encryped ciphertext. Plaintext is Sniffable CESR Stream. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 0 X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 1 X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXVarStrmDex = CipherX25519VarStrmCodex() # Make instance # Codes for for ciphers of variable sized QB64 plain text
[docs] @dataclass(frozen=True) class CipherX25519VarQB64Codex: """ CipherX25519VarQB64Codex is codex all variable sized cipher bytes derivation codes for sealed box encryped ciphertext. Plaintext is QB64. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 X25519_Cipher_QB64_L1: str = '5D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 1 X25519_Cipher_QB64_L2: str = '6D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 2 X25519_Cipher_QB64_Big_L0: str = '7AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 X25519_Cipher_QB64_Big_L1: str = '8AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 X25519_Cipher_QB64_Big_L2: str = '9AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXVarQB64Dex = CipherX25519VarQB64Codex() # Make instance # Codes for for ciphers of fixed sized QB64 plain text
[docs] @dataclass(frozen=True) class CipherX25519FixQB64Codex: """ CipherX25519FixQB64Codex is codex all fixed sized cipher bytes derivation codes for sealed box encryped ciphertext. Plaintext is B64. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_Seed: str = 'P' # X25519 sealed box 124 char qb64 Cipher of 44 char qb64 Seed X25519_Cipher_Salt: str = '1AAH' # X25519 sealed box 100 char qb64 Cipher of 24 char qb64 Salt def __iter__(self): return iter(astuple(self))
CiXFixQB64Dex = CipherX25519FixQB64Codex() # Make instance # Codes for for ciphers of all sizes fixed and variable of QB64 plain text
[docs] @dataclass(frozen=True) class CipherX25519AllQB64Codex: """ CipherX25519AllQB64Codex is codex all both fixed and variable sized cipher bytes derivation codes for sealed box encryped ciphertext. Plaintext is B64. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_Seed: str = 'P' # X25519 sealed box 124 char qb64 Cipher of 44 char qb64 Seed X25519_Cipher_Salt: str = '1AAH' # X25519 sealed box 100 char qb64 Cipher of 24 char qb64 Salt X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 X25519_Cipher_QB64_L1: str = '5D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 1 X25519_Cipher_QB64_L2: str = '6D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 2 X25519_Cipher_QB64_Big_L0: str = '7AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 X25519_Cipher_QB64_Big_L1: str = '8AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 X25519_Cipher_QB64_Big_L2: str = '9AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXAllQB64Dex = CipherX25519AllQB64Codex() # Make instance # Codes for for ciphers of variable sized QB2 plain text
[docs] @dataclass(frozen=True) class CipherX25519QB2VarCodex: """ CipherX25519QB2VarCodex is codex all variable sized cipher bytes derivation codes for sealed box encryped ciphertext. Plaintext is B2. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_QB2_L0: str = '4E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 0 X25519_Cipher_QB2_L1: str = '5E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 1 X25519_Cipher_QB2_L2: str = '6E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 2 X25519_Cipher_QB2_Big_L0: str = '7AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 0 X25519_Cipher_QB2_Big_L1: str = '8AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 1 X25519_Cipher_QB2_Big_L2: str = '9AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXVarQB2Dex = CipherX25519QB2VarCodex() # Make instance # Codes for for ciphers of all varibale sizes and all types of plain text
[docs] @dataclass(frozen=True) class CipherX25519AllVarCodex: """ CipherX25519AllVarCodex is codex all variable size codes of cipher bytes for sealed box encryped ciphertext. Plaintext maybe sniffable CESR stream or qb64 or qb2. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 0 X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 1 X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 2 X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 X25519_Cipher_QB64_L1: str = '5D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 1 X25519_Cipher_QB64_L2: str = '6D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 2 X25519_Cipher_QB64_Big_L0: str = '7AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 X25519_Cipher_QB64_Big_L1: str = '8AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 X25519_Cipher_QB64_Big_L2: str = '9AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 X25519_Cipher_QB2_L0: str = '4E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 0 X25519_Cipher_QB2_L1: str = '5E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 1 X25519_Cipher_QB2_L2: str = '6E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 2 X25519_Cipher_QB2_Big_L0: str = '7AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 0 X25519_Cipher_QB2_Big_L1: str = '8AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 1 X25519_Cipher_QB2_Big_L2: str = '9AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXVarDex = CipherX25519AllVarCodex() # Make instance # Codes for for ciphers of all sizes and all types of plain text
[docs] @dataclass(frozen=True) class CipherX25519AllCodex: """ CipherX25519AllCodex is codex all codes and types of cipher bytes for sealed box encryped ciphertext. Plaintext maybe sniffable or qb64 or qb2. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 0 X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 1 X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 2 X25519_Cipher_Seed: str = 'P' # X25519 sealed box 124 char qb64 Cipher of 44 char qb64 Seed X25519_Cipher_Salt: str = '1AAH' # X25519 sealed box 100 char qb64 Cipher of 24 char qb64 Salt X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 X25519_Cipher_QB64_L1: str = '5D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 1 X25519_Cipher_QB64_L2: str = '6D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 2 X25519_Cipher_QB64_Big_L0: str = '7AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 X25519_Cipher_QB64_Big_L1: str = '8AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 X25519_Cipher_QB64_Big_L2: str = '9AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 X25519_Cipher_QB2_L0: str = '4E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 0 X25519_Cipher_QB2_L1: str = '5E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 1 X25519_Cipher_QB2_L2: str = '6E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 2 X25519_Cipher_QB2_Big_L0: str = '7AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 0 X25519_Cipher_QB2_Big_L1: str = '8AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 1 X25519_Cipher_QB2_Big_L2: str = '9AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 2 def __iter__(self): return iter(astuple(self))
CiXDex = CipherX25519AllCodex() # Make instance
[docs] class Cipher(Matter): """ Cipher is Matter subclass holding a cipher text of a secret that may be either a secret seed (private key) or secret salt with appropriate CESR code to indicate which kind (which indicates size). The cipher text is created with assymetric encryption using an unrelated (public, private) encryption/decryption key pair. The public key is used for encryption the private key for decryption. The default is to use X25519 sealed box encryption. The Cipher instances .raw is the raw binary encrypted cipher text and its .code indicates what type of plain text has been encrypted. The cipher suite used for the encryption/decryption is implied by the context where the cipher is used. See Matter for inherited attributes and properties """ Codex = CiXDex Codes = asdict(CiXDex) # map code name to code
[docs] def __init__(self, raw=None, code=None, **kwa): """ Inherited Parameters: (see Matter) Parmeters: raw (bytes | str): cipher text (not plain text) code (str): cipher suite """ # default when raw is not None and code is None is to use fixed size # code given by raw size. Otherwise provided code fixed or variable size # is handled by Matter superclass. if raw is not None and code is None: if len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Salt): code = MtrDex.X25519_Cipher_Salt elif len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Seed): code = MtrDex.X25519_Cipher_Seed else: raise InvalidSizeError(f"Unsupported fixed raw size" f" {len(raw)} for {code=}.") if hasattr(raw, "encode"): raw = raw.encode("utf-8") # ensure bytes not str super(Cipher, self).__init__(raw=raw, code=code, **kwa) if self.code not in CiXDex: raise InvalidCodeError(f"Unsupported cipher code = {self.code}.")
[docs] def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, bare=False, **kwa): """ Returns plain text as klas instance (Matter, Indexer, Streamer). When klas is None then klas default is based on .code. Maybe Salter, Signer, or Streamer. Encrypted plain text is fully qualified (qb64) via self so derivaton code of plain text preserved through encryption/decryption round trip. The created Decrypter uses either decryption key given by prikey or when prikey missing derives prikey from signing key derived from private seed. Returns: decrypted (Matter | Indexer | Streamer): instance of decrypted cipher text of .raw which is encrypted qb64, qb2, or sniffable stream depending on .code when bare is False. Otherwise returns plaintext itself. Keyword Parameters: See Matter because created Decrypter is Matter subclass. Parameters: prikey (str | bytes): qb64 or qb64b serialization of private decryption key. Must be fully qualified with code. seed (str | bytes): qb64 or qb64b serialization of private signing key seed used to derive private decryption key. Must be fully qualified with code. klas (Matter | Indexer | Streamer): Class used to create instance from decrypted serialization. transferable (bool): Modifier of klas instance creation. When klas init (such as Signer) supports transferabe parm, True means verfer of returned signer is transferable. False means non-transferable. bare (bool): False (default) means returns instance holding plaintext. True means returns plaintext itself. """ decrypter = Decrypter(qb64b=prikey, seed=seed, **kwa) return decrypter.decrypt(cipher=self, klas=klas, transferable=transferable, bare=bare)
[docs] class Encrypter(Matter): """ Encrypter is Matter subclass with method to create a cipher text of a fully qualified (qb64) private key/seed where private key/seed is the plain text. Encrypter uses assymetric (public, private) key encryption of a serialization (plain text). Using its .raw as the encrypting (public) key and its .code to indicate the cipher suite for the encryption operation. For example .code == MtrDex.X25519 indicates that X25519 sealed box encyrption is used. The encryption key may be derived from an Ed25519 signing public key that associated with a nontransferable or basic derivation self certifying identifier. This allows use of the self certifying identifier to track or manage the encryption/decryption key pair. And could be used to provide additional authentication operations for using the encryption/decryption key pair. Support for this is provided at init time with the verkey parameter which allows deriving the encryption public key from the fully qualified verkey (signature verification key). See Matter for inherited attributes and properties: Methods: encrypt: returns cipher text """
[docs] def __init__(self, raw=None, code=MtrDex.X25519, verkey=None, **kwa): """ Assign encrypting cipher suite function to ._encrypt Parameters: See Matter for inherted parameters such as qb64, qb64b raw (bytes): public encryption key qb64b (bytes): fully qualified public encryption key qb64 (str): fully qualified public encryption key code (str): derivation code for public encryption key verkey (Union[bytes, str]): qb64b or qb64 of verkey used to derive raw """ if not raw and verkey: verfer = Verfer(qb64b=verkey) if verfer.code not in (MtrDex.Ed25519N, MtrDex.Ed25519): raise InvalidValueError(f"Unsupported verkey derivation code =" f" {verfer.code}.") # convert signing public key to encryption public key raw = pysodium.crypto_sign_pk_to_box_pk(verfer.raw) super(Encrypter, self).__init__(raw=raw, code=code, **kwa) if self.code == MtrDex.X25519: self._encrypt = self._x25519 else: raise InvalidValueError(f"Unsupported encrypter code = {self.code}.")
[docs] def verifySeed(self, seed): """ Returns: Boolean: True means private signing key seed corresponds to public signing key verkey used to derive encrypter's .raw public encryption key. Parameters: seed (Union(bytes,str)): qb64b or qb64 serialization of private signing key seed """ signer = Signer(qb64b=seed) verkey, sigkey = pysodium.crypto_sign_seed_keypair(signer.raw) pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey) return (pubkey == self.raw)
[docs] def encrypt(self, *, ser=None, prim=None, code=None): """ Returns: Cipher instance of cipher text encryption of plain text serialization provided by either ser or prim as CESR primitive instance. Parameters: ser (str | bytes | bytearray | memoryview): qb64b or qb64 or sniffable stream serialization of plain text prim (Matter | Indexer | Streamer): CESR primitive instance whose serialization is qb64 or qb2 or sniffable stream and is to be encrypted based on code code (str): code of plain text type for resultant encrypted cipher """ if not ser: if not prim: raise EmptyMaterialError(f"Neither bar serialization or primitive " f"are provided.") if not code: if prim.code == MtrDex.Salt_128: # future other salt codes code = MtrDex.X25519_Cipher_Salt elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes code = MtrDex.X25519_Cipher_Seed else: raise InvalidValueError(f"Unsupported primitive with code =" f" {prim.code} when cipher code is " f"missing.") if code in CiXAllQB64Dex: ser = prim.qb64b elif code in CiXVarQB2Dex: ser = prim.qb2 elif code in CiXVarStrmDex: ser = prim.stream else: raise InvalidCodeError(f"Invalid primitive cipher {code=} not " f"qb64 or qb2.") if not code: # assumes default is sniffable stream code = CiXDex.X25519_Cipher_L0 if hasattr(ser, "encode"): ser = ser.encode() # convert str to bytes if not isinstance(ser, bytes): ser = bytes(ser) # convert bytearray and memoryview to bytes # encrypting cesr primitive qb64 or qb2 or cesr stream as plain # text with proper cipher code ensures primitive round trip through eventual # decryption. return (self._encrypt(ser=ser, pubkey=self.raw, code=code))
@staticmethod def _x25519(ser, pubkey, code): """ Returns cipher text as Cipher instance Parameters: ser (Union[bytes, str]): qb64b or qb64 serialization of seed or salt to be encrypted. pubkey (bytes): raw binary serialization of encryption public key code (str): cipher derivation code """ raw = pysodium.crypto_box_seal(ser, pubkey) return Cipher(raw=raw, code=code)
[docs] class Decrypter(Matter): """ Decrypter is Matter subclass with method to decrypt the plain text from a ciper text of a fully qualified (qb64) private key/seed where private key/seed is the plain text. Decrypter uses assymetric (public, private) key decryption of the cipher text using its .raw as the decrypting (private) key and its .code to indicate the cipher suite for the decryption operation. For example .code == MtrDex.X25519 indicates that X25519 sealed box decyrption is used. The decryption key may be derived from an Ed25519 signing private key that is associated with a nontransferable or basic derivation self certifying identifier. This allows use of the self certifying identifier to track or manage the encryption/decryption key pair. And could be used to provide additional authentication operations for using the encryption/decryption key pair. Support for this is provided at init time with the sigkey parameter which allows deriving the decryption private key from the fully qualified sigkey (signing key). See Matter for inherited attributes and properties: Attributes: Properties: Methods: decrypt: create cipher text """
[docs] def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): """ Assign decrypting cipher suite function to ._decrypt Inherited Parameters: (see Matter) Parameters: See Matter for inherited parameters. code (str): derivation code for private decryption key seed (str | bytes | bytearray | memoryview | None): qb64b or qb64 of signing key seed used to derive raw which is private decryption key """ try: super(Decrypter, self).__init__(code=code, **kwa) except EmptyMaterialError as ex: if seed: signer = Signer(qb64b=seed) if signer.code not in (MtrDex.Ed25519_Seed,): raise ValueError("Unsupported signing seed derivation code = {}." "".format(signer.code)) # verkey, sigkey = pysodium.crypto_sign_seed_keypair(signer.raw) sigkey = signer.raw + signer.verfer.raw # sigkey is raw seed + raw verkey raw = pysodium.crypto_sign_sk_to_box_sk(sigkey) # raw private encrypt key super(Decrypter, self).__init__(raw=raw, code=code, **kwa) else: raise if self.code == MtrDex.X25519_Private: self._decrypt = self._x25519 else: raise ValueError("Unsupported decrypter code = {}.".format(self.code))
[docs] def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, transferable=False, bare=False, **kwa): """Returns plain text as klas instance (Matter, Indexer, Streamer). When klas is None then klas default is based on cipher.code or inferred from qb64 or qb2 code. Default maybe Salter, Signer, or Streamer. Cipher's encrypted plain text is fully qualified (qb64) so derivaton code of plain text preserved through encryption/decryption round trip. Returns: decrypted (Matter | Indexer | Streamer | bytes): When bare is False returns instance of decrypted cipher text of .raw which is encrypted qb64, qb2, or sniffable stream depending on .code hhen Bare is True. Otherwise returns decrypted serialization plaintext whatever that may be. Keyword Parameters: See Matter because created Decrypter is Matter subclass. Parameters: cipher (Cipher): instance. One of cipher, qb64, or qb2 required. qb64 (str | bytes | bytearray | memoryview | None ): serialization of cipher text as fully qualified base64. When str, encodes as utf-8. When bytearray and strip in kwa is True then strips. qb2 (bytes | bytearray | memoryview | None ): serialization of cipher text as fully qualified base2. Strips when bytearray and strip in kwa is True. klas (Matter | Indexer | Streamer): Class used to create instance from decrypted serialization. transferable (bool): Modifier of klas instance creation. When klas init (such as Signer) supports transferabe parm, True means verfer of returned signer is transferable. False means non-transferable. bare (bool): False (default) means returns instance holding plaintext. True means returns plaintext itself. """ if not cipher: if qb64: # create cipher from qb64 cipher = Cipher(qb64b=qb64, **kwa) elif qb2: cipher = Cipher(qb2=qb2, **kwa) else: raise EmptyMaterialError(f"Need one of cipher, qb64, or qb2.") return (self._decrypt(cipher=cipher, prikey=self.raw, klas=klas, transferable=transferable, bare=bare))
@staticmethod def _x25519(cipher, prikey, klas=None, transferable=False, bare=False): """Returns plain text as Salter or Signer instance depending on the cipher code and the embedded encrypted plain text derivation code. Parameters: cipher (Cipher): instance of encrypted seed or salt prikey (bytes): raw binary decryption private key derived from signing seed or sigkey klas (Matter, Indexer, Streamer | None): Class used to create instance from decrypted serialization. Default depends on cipher.code. transferable (bool): Modifier of Klas instance creation. When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable bare (bool): False (default) means CESR instance holding plaintext True means plaintext """ # assumes raw plain text is qb64b or qb64 or sniffable stream # so it's round trippable pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) plain = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) # qb64b if bare: return plain else: if not klas: if cipher.code == CiXFixQB64Dex.X25519_Cipher_Salt: klas = Salter elif cipher.code == CiXFixQB64Dex.X25519_Cipher_Seed: klas = Signer elif cipher.code in CiXVarStrmDex: klas = Streamer else: raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}" f" when klas missing.") if cipher.code in CiXAllQB64Dex: return klas(qb64b=plain, transferable=transferable) elif cipher.code in CiXVarQB2Dex: return klas(qb2=plain) elif cipher.code in CiXVarStrmDex: return klas(stream=plain) else: raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}.")
[docs] class Streamer: """ Streamer is CESR sniffable stream class Has the following public properties: Properties: stream (bytearray): sniffable CESR stream Methods: Hidden: _verify() -> bool """
[docs] def __init__(self, stream, verify=False): """Initialize instance Holds sniffable CESR stream as byte like string either (bytes, bytearray, or memoryview) Parameters: stream (str | bytes | bytearray | memoryview): sniffable CESR stream verify (bool): When True raise error if .stream is not sniffable. """ if hasattr(stream, "encode"): stream = bytearray(stream.encode()) # convert str to bytearray if not isinstance(stream, (bytes, bytearray, memoryview)): raise InvalidTypeError(f"Invalid stream type, not byteable.") self._stream = stream
def _verify(self): """Returns True if .stream is sniffable, False otherwise Returns: sniffable (bool): True when .stream is sniffable. False otherwise. Only works for ver 2 CESR because need for all count codes to be pipelineable in order to simply parse stream """ return False @property def stream(self): """stream property getter """ return self._stream @property def text(self): """expanded stream where all primitives and groups in stream are individually expanded to qb64. Requires parsing full depth to ensure expanded consistently. Returns: stream (bytes): expanded text qb64 version of stream Only works for ver 2 CESR because need for all count codes to be pipelineable in order to simply parse and expand stream """ return self._stream @property def binary(self): """compacted stream where all primitives and groups in stream are individually compacted to qb2. Requires parsing full depth to ensure compacted consistently Returns: stream (bytes): compacted binary qb2 version of stream Only works for ver 2 CESR because need for all count codes to be pipelineable in order to simply parse and compact stream """ return self._stream @property def texter(self): """stream as Texter instance. Texter(text=self.stream) Returns: texter (Texter): Texter primitive of stream suitable wrapping """ return self._stream @property def bexter(self): """stream as Bexter instance. Bexter of expanded text version of stream. First expand to text which requires parsing then create bexter:: Bexter(bext=self.text) Because sniffable stream MUST NOT start with 'A' then there is no length ambiguity. The only tritet collison of 'A' is with '-' but the remaining 5 bits are guaranteed to always be different. So bexter must check not just the starting tritet but the full starting byte to ensure not 'A' as first byte. Requires parsing to ensure qb64 Returns: bexter (Bexter): Bexter primitive of stream suitable wrapping """ return self._stream