mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 02:46:28 +00:00
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:
parent
2bb0b4d7bf
commit
516537a38e
@ -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
|
||||
|
23
api/utils.py
23
api/utils.py
@ -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()
|
||||
|
55
api/views.py
55
api/views.py
@ -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)
|
||||
|
||||
|
@ -3,3 +3,4 @@ static/rest_framework/**
|
||||
static/admin/**
|
||||
static/frontend/**
|
||||
static/import_export/**
|
||||
static/drf_spectacular_sidecar/**
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user