mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +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):
|
class UpdateOrderSerializer(serializers.Serializer):
|
||||||
invoice = serializers.CharField(
|
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(
|
routing_budget_ppm = serializers.IntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
@ -485,7 +489,11 @@ class UpdateOrderSerializer(serializers.Serializer):
|
|||||||
help_text="Max budget to allocate for routing in PPM",
|
help_text="Max budget to allocate for routing in PPM",
|
||||||
)
|
)
|
||||||
address = serializers.CharField(
|
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(
|
statement = serializers.CharField(
|
||||||
max_length=500_000, allow_null=True, allow_blank=True, default=None
|
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
|
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:
|
def base91_to_hex(base91_str: str) -> str:
|
||||||
bytes_data = decode(base91_str)
|
bytes_data = decode(base91_str)
|
||||||
return bytes_data.hex()
|
return bytes_data.hex()
|
||||||
|
55
api/views.py
55
api/views.py
@ -57,6 +57,7 @@ from api.utils import (
|
|||||||
get_lnd_version,
|
get_lnd_version,
|
||||||
get_robosats_commit,
|
get_robosats_commit,
|
||||||
validate_pgp_keys,
|
validate_pgp_keys,
|
||||||
|
verify_signed_message,
|
||||||
)
|
)
|
||||||
from chat.models import Message
|
from chat.models import Message
|
||||||
from control.models import AccountingDay, BalanceLog
|
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'
|
# 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'
|
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
|
||||||
action = serializer.data.get("action")
|
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)
|
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")
|
mining_fee_rate = serializer.data.get("mining_fee_rate")
|
||||||
statement = serializer.data.get("statement")
|
statement = serializer.data.get("statement")
|
||||||
rating = serializer.data.get("rating")
|
rating = serializer.data.get("rating")
|
||||||
@ -544,6 +545,22 @@ class OrderView(viewsets.ViewSet):
|
|||||||
|
|
||||||
# 2) If action is 'update invoice'
|
# 2) If action is 'update invoice'
|
||||||
elif action == "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(
|
valid, context = Logics.update_invoice(
|
||||||
order, request.user, invoice, routing_budget_ppm
|
order, request.user, invoice, routing_budget_ppm
|
||||||
)
|
)
|
||||||
@ -552,6 +569,22 @@ class OrderView(viewsets.ViewSet):
|
|||||||
|
|
||||||
# 2.b) If action is 'update address'
|
# 2.b) If action is 'update address'
|
||||||
elif action == "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(
|
valid, context = Logics.update_address(
|
||||||
order, request.user, address, mining_fee_rate
|
order, request.user, address, mining_fee_rate
|
||||||
)
|
)
|
||||||
@ -994,7 +1027,23 @@ class RewardView(CreateAPIView):
|
|||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
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)
|
valid, context = Logics.withdraw_rewards(request.user, invoice)
|
||||||
|
|
||||||
|
@ -3,3 +3,4 @@ static/rest_framework/**
|
|||||||
static/admin/**
|
static/admin/**
|
||||||
static/frontend/**
|
static/frontend/**
|
||||||
static/import_export/**
|
static/import_export/**
|
||||||
|
static/drf_spectacular_sidecar/**
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
|
||||||
List,
|
List,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
@ -37,6 +36,7 @@ import { getWebln } from '../../utils';
|
|||||||
import RobotAvatar from '../RobotAvatar';
|
import RobotAvatar from '../RobotAvatar';
|
||||||
import { apiClient } from '../../services/api';
|
import { apiClient } from '../../services/api';
|
||||||
import { type Robot } from '../../models';
|
import { type Robot } from '../../models';
|
||||||
|
import { signCleartextMessage } from '../../pgp';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -90,23 +90,27 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
|||||||
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
|
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
|
||||||
setBadInvoice('');
|
setBadInvoice('');
|
||||||
setShowRewardsSpinner(true);
|
setShowRewardsSpinner(true);
|
||||||
|
signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
|
||||||
apiClient
|
apiClient
|
||||||
.post(
|
.post(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
'/api/reward/',
|
'/api/reward/',
|
||||||
{
|
{
|
||||||
invoice: rewardInvoice,
|
invoice: signedInvoice,
|
||||||
},
|
},
|
||||||
{ tokenSHA256: robot.tokenSHA256 },
|
{ tokenSHA256: robot.tokenSHA256 },
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
setBadInvoice(data.bad_invoice ?? '');
|
setBadInvoice(data.bad_invoice ?? '');
|
||||||
setShowRewardsSpinner(false);
|
setShowRewardsSpinner(false);
|
||||||
setWithdrawn(data.successful_withdrawal);
|
setWithdrawn(data.successful_withdrawal);
|
||||||
setOpenClaimRewards(!data.successful_withdrawal);
|
setOpenClaimRewards(!data.successful_withdrawal);
|
||||||
setRobot({ ...robot, earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards });
|
setRobot({
|
||||||
});
|
...robot,
|
||||||
|
earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ import { type Order, type Robot, type Settings } from '../../models';
|
|||||||
import { type EncryptedChatMessage } from './EncryptedChat';
|
import { type EncryptedChatMessage } from './EncryptedChat';
|
||||||
import CollabCancelAlert from './CollabCancelAlert';
|
import CollabCancelAlert from './CollabCancelAlert';
|
||||||
import { Bolt } from '@mui/icons-material';
|
import { Bolt } from '@mui/icons-material';
|
||||||
|
import { signCleartextMessage } from '../../pgp';
|
||||||
|
|
||||||
interface loadingButtonsProps {
|
interface loadingButtonsProps {
|
||||||
cancel: boolean;
|
cancel: boolean;
|
||||||
@ -224,19 +225,23 @@ const TradeBox = ({
|
|||||||
|
|
||||||
const updateInvoice = function (invoice: string) {
|
const updateInvoice = function (invoice: string) {
|
||||||
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
|
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
|
||||||
submitAction({
|
signCleartextMessage(invoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
|
||||||
action: 'update_invoice',
|
submitAction({
|
||||||
invoice,
|
action: 'update_invoice',
|
||||||
routing_budget_ppm: lightning.routingBudgetPPM,
|
invoice: signedInvoice,
|
||||||
|
routing_budget_ppm: lightning.routingBudgetPPM,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAddress = function () {
|
const updateAddress = function () {
|
||||||
setLoadingButtons({ ...noLoadingButtons, submitAddress: true });
|
setLoadingButtons({ ...noLoadingButtons, submitAddress: true });
|
||||||
submitAction({
|
signCleartextMessage(onchain.address, robot.encPrivKey, robot.token).then((signedAddress) => {
|
||||||
action: 'update_address',
|
submitAction({
|
||||||
address: onchain.address,
|
action: 'update_address',
|
||||||
mining_fee_rate: onchain.miningFee,
|
address: signedAddress,
|
||||||
|
mining_fee_rate: onchain.miningFee,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,9 @@ import {
|
|||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
createMessage,
|
createMessage,
|
||||||
|
createCleartextMessage,
|
||||||
readMessage,
|
readMessage,
|
||||||
|
sign,
|
||||||
} from 'openpgp/lightweight';
|
} from 'openpgp/lightweight';
|
||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
|
|
||||||
@ -82,3 +84,19 @@ export async function decryptMessage(
|
|||||||
return { decryptedMessage: decrypted, validSignature: false };
|
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