asfpy/crypto.py (58 lines of code) (raw):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Auxiliary crypto features. Just ED25519 signing for now."""
import binascii
import cryptography.exceptions
import cryptography.hazmat.primitives.asymmetric.ed25519
import cryptography.hazmat.primitives.serialization
import secrets
import base64
# Defaults PEM format for our ED25519 keys
ED25519_ENCODING = cryptography.hazmat.primitives.serialization.Encoding.PEM
ED25519_PRIVKEY_FORMAT = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
ED25519_PUBKEY_FORMAT = cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
ED25519_PEM_ENCRYPTION = cryptography.hazmat.primitives.serialization.NoEncryption()
class ED25519:
def __init__(self, pubkey: str = None, privkey: str = None):
"""Loads an existing ED25519 key or instantiates a new ED25519 key pair.
If pubkey is set, it loads it as a PEM-formatted key, same with privkey.
If no public or private key is passed on, a new keypair is created instead."""
if pubkey:
self._pubkey = cryptography.hazmat.primitives.serialization.load_pem_public_key(pubkey.encode("us-ascii"))
self._privkey = None
elif privkey:
self._privkey = cryptography.hazmat.primitives.serialization.load_pem_private_key(
privkey.encode("us-ascii"), password=None
)
else:
self._privkey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
# Private keys can be used to generate as many public keys as needed, so we can create one for testing.
if self._privkey:
self._pubkey = self._privkey.public_key()
@property
def pubkey(self):
""" "Returns the public key (if present) in PEM format"""
assert self._pubkey, "No public key found, cannot PEM-encode nothing!"
return self._pubkey.public_bytes(encoding=ED25519_ENCODING, format=ED25519_PUBKEY_FORMAT).decode("us-ascii")
@property
def privkey(self):
""" "Returns the private key (if present) in PEM format"""
assert self._privkey, "No public key found, cannot PEM-encode nothing!"
return self._privkey.private_bytes(
encoding=ED25519_ENCODING, format=ED25519_PRIVKEY_FORMAT, encryption_algorithm=ED25519_PEM_ENCRYPTION
).decode("us-ascii")
def sign_data(self, data: str = "", output_b64=False):
"""Signs a string with the private key for authenticity purposes.
The signature includes a nonce for randomizing the response and returns three lines, split by newline:
data-plus-nonce-signature
nonce
data
The blob can be verified by verify_data, which will return the verified data if the signature is valid,
else None.
If output_b64 is True, the signed data is base64-encoded and returned as a single line. This can be
useful for HTTP-based access tokens.
"""
nonce = secrets.token_hex(32)
data_plus_nonce = "\n".join([nonce, data])
data_signature = self._privkey.sign(data_plus_nonce.encode("us-ascii"))
response = "\n".join([base64.b64encode(data_signature).decode("us-ascii"), nonce, data])
if output_b64:
response = base64.b64encode(response.encode('us-ascii')).decode('us-ascii')
return response
def verify_response(self, data: str):
"""Verifies the authenticity of a data blob. If signed by the private key,
returns the original data that was signed, otherwise None"""
try:
if "\n" not in data: # base64-encoded one-liner?
try:
data = base64.b64decode(data).decode('us-ascii')
except binascii.Error: # Not base64!
return None
signature, data_plus_nonce = data.split("\n", 1)
signature = base64.b64decode(signature)
except ValueError: # Bad token format or invalid base64 signature, FAILURE.
return
try:
_nonce, data_verified = data_plus_nonce.split("\n", 1)
self._pubkey.verify(signature, data_plus_nonce.encode("us-ascii"))
return data_verified
except cryptography.exceptions.InvalidSignature:
return