Add pgp signature to sensitive client - coordinator messages (#592)

* Minor fixes on dev setup start up

* Add pgp cleartext signatures
This commit is contained in:
Reckless_Satoshi 2023-05-17 13:06:04 +00:00 committed by GitHub
parent 2bb0b4d7bf
commit 516537a38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 139 additions and 31 deletions

View File

@ -474,7 +474,11 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(
max_length=2000, allow_null=True, allow_blank=True, default=None
max_length=15000,
allow_null=True,
allow_blank=True,
default=None,
help_text="Invoice used for payouts. Must be PGP signed with the robot's public key. The expected Armored PGP header is -----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n",
)
routing_budget_ppm = serializers.IntegerField(
default=0,
@ -485,7 +489,11 @@ class UpdateOrderSerializer(serializers.Serializer):
help_text="Max budget to allocate for routing in PPM",
)
address = serializers.CharField(
max_length=100, allow_null=True, allow_blank=True, default=None
max_length=15000,
allow_null=True,
allow_blank=True,
default=None,
help_text="Onchain address used for payouts. Must be PGP signed with the robot's public key. The expected Armored PGP header is -----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n",
)
statement = serializers.CharField(
max_length=500_000, allow_null=True, allow_blank=True, default=None

View File

@ -301,6 +301,29 @@ def validate_pgp_keys(pub_key, enc_priv_key):
return True, None, pub_key, enc_priv_key
def verify_signed_message(pub_key, signed_message):
"""
Verifies a signed cleartext PGP message. Returns whether the signature
is valid (was made by the given pub_key) and the content of the message.
"""
gpg = gnupg.GPG()
# import the public key
import_result = gpg.import_keys(pub_key)
# verify the signed message
verified = gpg.verify(signed_message)
if verified.fingerprint == import_result.fingerprints[0]:
header = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n"
footer = "-----BEGIN PGP SIGNATURE-----"
cleartext_message = signed_message.split(header)[1].split(footer)[0].strip()
return True, cleartext_message
else:
return False, None
def base91_to_hex(base91_str: str) -> str:
bytes_data = decode(base91_str)
return bytes_data.hex()

View File

@ -57,6 +57,7 @@ from api.utils import (
get_lnd_version,
get_robosats_commit,
validate_pgp_keys,
verify_signed_message,
)
from chat.models import Message
from control.models import AccountingDay, BalanceLog
@ -500,9 +501,9 @@ class OrderView(viewsets.ViewSet):
# action is either 1)'take', 2)'confirm', 2.b)'undo_confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
action = serializer.data.get("action")
invoice = serializer.data.get("invoice")
pgp_invoice = serializer.data.get("invoice")
routing_budget_ppm = serializer.data.get("routing_budget_ppm", 0)
address = serializer.data.get("address")
pgp_address = serializer.data.get("address")
mining_fee_rate = serializer.data.get("mining_fee_rate")
statement = serializer.data.get("statement")
rating = serializer.data.get("rating")
@ -544,6 +545,22 @@ class OrderView(viewsets.ViewSet):
# 2) If action is 'update invoice'
elif action == "update_invoice":
# DEPRECATE post v0.5.1.
if "---" not in pgp_invoice:
valid_signature = True
invoice = pgp_invoice
else:
# END DEPRECATE.
valid_signature, invoice = verify_signed_message(
request.user.robot.public_key, pgp_invoice
)
if not valid_signature:
return Response(
{"bad_request": "The PGP signed cleartext message is not valid."},
status.HTTP_400_BAD_REQUEST,
)
valid, context = Logics.update_invoice(
order, request.user, invoice, routing_budget_ppm
)
@ -552,6 +569,22 @@ class OrderView(viewsets.ViewSet):
# 2.b) If action is 'update address'
elif action == "update_address":
# DEPRECATE post v0.5.1.
if "---" not in pgp_address:
valid_signature = True
address = pgp_address
else:
# END DEPRECATE.
valid_signature, address = verify_signed_message(
request.user.robot.public_key, pgp_address
)
if not valid_signature:
return Response(
{"bad_request": "The PGP signed cleartext message is not valid."},
status.HTTP_400_BAD_REQUEST,
)
valid, context = Logics.update_address(
order, request.user, address, mining_fee_rate
)
@ -994,7 +1027,23 @@ class RewardView(CreateAPIView):
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
invoice = serializer.data.get("invoice")
pgp_invoice = serializer.data.get("invoice")
# DEPRECATE post v0.5.1.
if "---" not in pgp_invoice:
valid_signature = True
invoice = pgp_invoice
else:
# END DEPRECATE.
valid_signature, invoice = verify_signed_message(
request.user.robot.public_key, pgp_invoice
)
if not valid_signature:
return Response(
{"bad_request": "The PGP signed cleartext message is not valid."},
status.HTTP_400_BAD_REQUEST,
)
valid, context = Logics.withdraw_rewards(request.user, invoice)

View File

@ -3,3 +3,4 @@ static/rest_framework/**
static/admin/**
static/frontend/**
static/import_export/**
static/drf_spectacular_sidecar/**

View File

@ -12,7 +12,6 @@ import {
Divider,
FormControlLabel,
Grid,
IconButton,
List,
ListItemAvatar,
ListItemButton,
@ -37,6 +36,7 @@ import { getWebln } from '../../utils';
import RobotAvatar from '../RobotAvatar';
import { apiClient } from '../../services/api';
import { type Robot } from '../../models';
import { signCleartextMessage } from '../../pgp';
interface Props {
open: boolean;
@ -90,23 +90,27 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
setBadInvoice('');
setShowRewardsSpinner(true);
apiClient
.post(
baseUrl,
'/api/reward/',
{
invoice: rewardInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
)
.then((data: any) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
setOpenClaimRewards(!data.successful_withdrawal);
setRobot({ ...robot, earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards });
});
signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
apiClient
.post(
baseUrl,
'/api/reward/',
{
invoice: signedInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
)
.then((data: any) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
setOpenClaimRewards(!data.successful_withdrawal);
setRobot({
...robot,
earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards,
});
});
});
e.preventDefault();
};

View File

@ -50,6 +50,7 @@ import { type Order, type Robot, type Settings } from '../../models';
import { type EncryptedChatMessage } from './EncryptedChat';
import CollabCancelAlert from './CollabCancelAlert';
import { Bolt } from '@mui/icons-material';
import { signCleartextMessage } from '../../pgp';
interface loadingButtonsProps {
cancel: boolean;
@ -224,19 +225,23 @@ const TradeBox = ({
const updateInvoice = function (invoice: string) {
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
submitAction({
action: 'update_invoice',
invoice,
routing_budget_ppm: lightning.routingBudgetPPM,
signCleartextMessage(invoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
submitAction({
action: 'update_invoice',
invoice: signedInvoice,
routing_budget_ppm: lightning.routingBudgetPPM,
});
});
};
const updateAddress = function () {
setLoadingButtons({ ...noLoadingButtons, submitAddress: true });
submitAction({
action: 'update_address',
address: onchain.address,
mining_fee_rate: onchain.miningFee,
signCleartextMessage(onchain.address, robot.encPrivKey, robot.token).then((signedAddress) => {
submitAction({
action: 'update_address',
address: signedAddress,
mining_fee_rate: onchain.miningFee,
});
});
};

View File

@ -6,7 +6,9 @@ import {
encrypt,
decrypt,
createMessage,
createCleartextMessage,
readMessage,
sign,
} from 'openpgp/lightweight';
import { sha256 } from 'js-sha256';
@ -82,3 +84,19 @@ export async function decryptMessage(
return { decryptedMessage: decrypted, validSignature: false };
}
}
// Sign a cleartext message
export async function signCleartextMessage(message, privateKeyArmored, passphrase) {
const privateKey = await decryptKey({
privateKey: await readPrivateKey({ armoredKey: privateKeyArmored }),
passphrase,
});
const unsignedMessage = await createCleartextMessage({ text: message });
const signedMessage = await sign({
message: unsignedMessage,
signingKeys: privateKey,
});
return signedMessage;
}