The hardest part of the challenge is figuring out what the description is trying to say :)
```#!/usr/bin/env pythonimport os
from aiohttp import webfrom stellar_sdk import (    Keypair,    Account,    TransactionBuilder,    Asset,    Network,    TransactionEnvelope,)from stellar_sdk.exceptions import BadSignatureError
app = web.Application()routes = web.RouteTableDef()
STELLAR_SECRET = os.environ.get("STELLAR_SECRET")FLAG = os.environ.get("FLAG")if STELLAR_SECRET is None or FLAG is None:    raise EnvironmentError("Secrets are not set")
# Disabled for now# @routes.post("/prepareorder")async def prepare_order(request: web.Request):    data = await request.post()    if "address" not in data:        return web.Response(status=500, body="Missing destination address")
    keypair = Keypair.from_secret(STELLAR_SECRET)    account = Account(account=keypair.public_key, sequence=1)
    transaction = (        TransactionBuilder(            source_account=account,            network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,            base_fee=100,        )        .append_payment_op(            data["address"], Asset("PIZZA", keypair.public_key), "1"        )        .build()    )    transaction.sign(keypair)    return web.Response(body=transaction.to_xdr())
@routes.post("/submit")async def submit_transaction(request: web.Request):    data = await request.post()    if "tx" not in data:        return web.Response(status=500, body="Missing tx")    envelope = TransactionEnvelope.from_xdr(        data["tx"], Network.PUBLIC_NETWORK_PASSPHRASE    )    if len(envelope.signatures) != 1:        return web.Response(status=500, body="Invalid envelope")    keypair = Keypair.from_secret(STELLAR_SECRET)    try:        keypair.verify(envelope.hash(), envelope.signatures[0].signature)    except BadSignatureError:        return web.Response(status=500, body="Invalid signature")    # server = Server(horizon_url="https://horizon.stellar.org")    # response = server.submit_transaction(envelope)    # return response["flag"]    return web.Response(body=FLAG)
@routes.get("/publickey")async def public_key(_: web.Request):    keypair = Keypair.from_secret(STELLAR_SECRET)    return web.Response(body=keypair.public_key)
@routes.get("/")async def index(request):    return web.FileResponse("./index.html")
if __name__ == "__main__":    app.add_routes(routes)    web.run_app(app, port=25519)```
The code seems solid, so decrypting the description is critical. Fortunately, this is a part of two-series challenge, and the second part has an unusual code that wraps a call to `Keypair.from_secret(STELLAR_SECRET).public_key` in a loop and bails out with 500 if any two iterations disagree with the calculated value. Let's check whether the server always outputs the same value from `/publickey`... there is indeed more than one value.
ed25519 private keys are not actually elliptic-curve private keys (as in "multiplier modulo curve order"); elliptic-curve private key is generated as a hash of ed25519 private key. "Some bytes are erased" may mean "some bytes are replaced by 0x00" or "some bytes are replaced by 0xFF". If hashing does this, the correct multiplier is `x` and a fault changes a single byte, then a faulty multiplier could be `x-256**k*(k-th byte of x)` or `x+256**k*(0xFF-(k-th byte of x))` for some value of `k`; the corresponding faulty public key differs from the correct public key by `256**k*(some byte)*G` (with `G` being the generator point). This gives `32*256` possible points, and it is easy to build a map from these points back to pairs of `(k,byte)`. If the difference `(correct public key) - (faulty public key)` (using elliptic-point subtraction) is in the map, "erasing" replaces with zeroes and we get `k`-th byte of the private multiplier; if `(faulty public key) - (correct public key)` is in the map, "erasing" replaces with FFs and we get 0xFF minus `k`-th byte of the private multiplier which is just as good. One sample shows that the first case happens.
Step 1: collect 33 different outputs of `/publickey`, one true and 32 for every possible index:```b'GCET4VA7665VKBXEY7TQKC64AJ4CZPWBTJ2IKKUSAGZXI5KHMKQL6N7Z' 91b'GASPS2IITBNTPDPKYAKZ7XEMM7WFM267SWFI23CR6B24XRCOMU35OYVB' 7b'GD7AUFO3EIHLLUGIKOFBXYREVBCT66FQJSM5IGSEQETQX5KLDXRTEZJ3' 4b'GDXSGVIKLHLSZLZ7SNVPWNXTWYK2GQAVMPDVGB5Y72ESEFOP3MMDXXDO' 5b'GBZTJZAMFWBVD5ICNDBE5G6Q7HYGU4AFBBFB4TDUFIQW742NKHFNGWAI' 3b'GDV2JQXAGJ3UWC3UJWO4ZYM7E2NTD5TPPKOWV6PG46ZYF33PG5QUHX4B' 3b'GBTRIPZRA74R3BVVWOEWZIVNSOP7OFFS6BPGXSLK7KZ5EWYHNXM4QZI5' 2b'GBIPR4LONIFBGC43DBAGPTKES66TWB5GQIEMYHH3MPFIKRSAGH6A7LSW' 2b'GDMVFYLS4CWSEI6RW3YJNDV6DV36BQXSXDPTRYKRY3UQO64GJLTKMWYP' 3b'GBMT3463RD2J4AAE4S54ZIBR2LYPDAX6KCJ5DB4A3YLEUZDMUO5POV6R' 6b'GCH5OUNOQA5SOXVPTHNNIS3G46PJVZZMBXRLZFDRMXFKV52JCKWYOXZU' 4b'GAXOBBWLIXPOZFRXYBW7HIDWE2QQ6DI6ZDI3U3GVNY2GQZMYPCG3VBQR' 4b'GDY26KIEQJGF5LMZZMSW5PCACI22UTLRVNLTF3HGROOECFONYFEZF3LN' 3b'GAKE4IHQKZ77X67XRGHNK3OK4GVSMKCJOWM4TX7KB7GNRWL3TMMKLTY5' 6b'GCQZYKPIBESD7XUMRVOALOO7UI57WC4H7AODDYD62A2FLI32XV3ZSL6H' 3b'GDF6NHXIGCQ4U2ZS77Q4X7X7Q6D5IIWAFPHWHGSP3KDWVFQLQ3NJHIVK' 1b'GDXXXSYSTW5GV6JXC5K7JEPKL2YRAANNPWKGJ3AR55VELSPO42MHHVTN' 4b'GCJGWW76NYGVOLKFW6U74GMJI7MYNBS4S2FD3HHLXIRDP5GGM4LODGKR' 3b'GBCOTX4GQ3VTZCYM24TOEGX3O4NTD4WSGEEBK7GLSF5XYPJT3ZJ4PPST' 2b'GCML3R42IOVZ7ZFELLJCGV766D4SSL7CWAGM6YB7LV4ZGPWUDOLBLBQD' 3b'GDAHHDXFAEAAPQZIZCVEEFWPJFCWAMFHTAQPQQRU3W4IUL6K6SDGFZLW' 1b'GAS3DVITSEHP4R7QTDPPQLGCVIKWZ63X2JPSEVPTJMFJYGSJAIPEZ3K2' 2b'GDFRYLFO4MRKZQX722NOGHC22G5MA4ERJOIAUSO2TPAKXW7BHQHCCVSD' 3b'GCUPPCGW5CQ45KXY4A3DNNZSJTGHI6CRYXLOOJMPTK7AT6GWMALWAZY6' 2b'GCRD6S7KWAWNGTLSBK4K2VSL2KOIB2YHEFDTKY2IYJUUY4BHPYHZCW5W' 2b'GDLZRKDM5M6W4F5HEZ3XEYSXUTEE7ETDW6AN6FZJ4VI7G6Z3VQK36EJL' 2b'GDNATYJXRJRIP4FMN2RS7PHXVW7UOE4DVZOKJPCVTCFENP57DQGWO7GR' 3b'GASR6SEGRBS7JMVGJIRDU7VCJTZQSFQYVVWR6LVVZRWYNJ322TUBTTCJ' 1b'GA36VBVGF6AAXQZTR6ZJ5B42VCA3TQZEOCIOMKC6JH6SDF7RPPBHO5FF' 6b'GAOJM6HR6AIEYK45PSACEDZ5JA5SEIRIEGHU43XFXWUTGSWA6M6BYQPW' 1b'GDBQOSXNP4NKFAHGB5ADDQLCWC56AHQZXDHKZTUPIXHX6KXD2YCVKMJ6' 1b'GBBS374RPG55QYS73S4V32YDTFY5MLCSCOSYEUIXBSCGG4JKJYRLULLU' 1b'GCZR3XVMG5EDC5NGYAEKJPJSA37V7Y7YZGAUJORSPK2IHJD63DHUDRCJ' 1```(the number shows how many times the particular output has appeared; they show which key is the true one and are rather useless otherwise).
Step 2: build a map of possible points `256**(index)*(byte)*G` and match differences between the true public key and all faulty keys against that map. `EllipticCurve` from SageMath does not handle Edwards curves, so I have taken [https://ed25519.cr.yp.to/python/ed25519.py](https://ed25519.cr.yp.to/python/ed25519.py) (with minor fixes for Python2 -> Python3) for elliptic operations. Stellar public keys have a constant identifier as the first byte and some signature as two last bytes, drop them to obtain elliptic public keys:```import hashlibimport base64import sysimport ed25519
sys.stdout.write("creating mapping")sys.stdout.flush()mapping = {}cur = ed25519.Bfor i in range(32): base = cur for j in range(1,256):  mapping[(cur[0],cur[1])] = (i,j)  cur = ed25519.edwards(cur,base) sys.stdout.write('.') sys.stdout.flush()assert cur == ed25519.scalarmult(ed25519.B, 1<<(8*32))print("done")
basekey = 'GCET4VA7665VKBXEY7TQKC64AJ4CZPWBTJ2IKKUSAGZXI5KHMKQL6N7Z'basepoint = ed25519.decodepoint(base64.b32decode(basekey)[1:-2])results = []for faultkey in ['GASPS2IITBNTPDPKYAKZ7XEMM7WFM267SWFI23CR6B24XRCOMU35OYVB','GD7AUFO3EIHLLUGIKOFBXYREVBCT66FQJSM5IGSEQETQX5KLDXRTEZJ3','GDXSGVIKLHLSZLZ7SNVPWNXTWYK2GQAVMPDVGB5Y72ESEFOP3MMDXXDO','GBZTJZAMFWBVD5ICNDBE5G6Q7HYGU4AFBBFB4TDUFIQW742NKHFNGWAI','GDV2JQXAGJ3UWC3UJWO4ZYM7E2NTD5TPPKOWV6PG46ZYF33PG5QUHX4B','GBTRIPZRA74R3BVVWOEWZIVNSOP7OFFS6BPGXSLK7KZ5EWYHNXM4QZI5','GBIPR4LONIFBGC43DBAGPTKES66TWB5GQIEMYHH3MPFIKRSAGH6A7LSW','GDMVFYLS4CWSEI6RW3YJNDV6DV36BQXSXDPTRYKRY3UQO64GJLTKMWYP','GBMT3463RD2J4AAE4S54ZIBR2LYPDAX6KCJ5DB4A3YLEUZDMUO5POV6R','GCH5OUNOQA5SOXVPTHNNIS3G46PJVZZMBXRLZFDRMXFKV52JCKWYOXZU','GAXOBBWLIXPOZFRXYBW7HIDWE2QQ6DI6ZDI3U3GVNY2GQZMYPCG3VBQR','GDY26KIEQJGF5LMZZMSW5PCACI22UTLRVNLTF3HGROOECFONYFEZF3LN','GAKE4IHQKZ77X67XRGHNK3OK4GVSMKCJOWM4TX7KB7GNRWL3TMMKLTY5','GCQZYKPIBESD7XUMRVOALOO7UI57WC4H7AODDYD62A2FLI32XV3ZSL6H','GDF6NHXIGCQ4U2ZS77Q4X7X7Q6D5IIWAFPHWHGSP3KDWVFQLQ3NJHIVK','GDXXXSYSTW5GV6JXC5K7JEPKL2YRAANNPWKGJ3AR55VELSPO42MHHVTN','GCJGWW76NYGVOLKFW6U74GMJI7MYNBS4S2FD3HHLXIRDP5GGM4LODGKR','GBCOTX4GQ3VTZCYM24TOEGX3O4NTD4WSGEEBK7GLSF5XYPJT3ZJ4PPST','GCML3R42IOVZ7ZFELLJCGV766D4SSL7CWAGM6YB7LV4ZGPWUDOLBLBQD','GDAHHDXFAEAAPQZIZCVEEFWPJFCWAMFHTAQPQQRU3W4IUL6K6SDGFZLW','GAS3DVITSEHP4R7QTDPPQLGCVIKWZ63X2JPSEVPTJMFJYGSJAIPEZ3K2','GDFRYLFO4MRKZQX722NOGHC22G5MA4ERJOIAUSO2TPAKXW7BHQHCCVSD','GCUPPCGW5CQ45KXY4A3DNNZSJTGHI6CRYXLOOJMPTK7AT6GWMALWAZY6','GCRD6S7KWAWNGTLSBK4K2VSL2KOIB2YHEFDTKY2IYJUUY4BHPYHZCW5W','GDLZRKDM5M6W4F5HEZ3XEYSXUTEE7ETDW6AN6FZJ4VI7G6Z3VQK36EJL','GDNATYJXRJRIP4FMN2RS7PHXVW7UOE4DVZOKJPCVTCFENP57DQGWO7GR','GASR6SEGRBS7JMVGJIRDU7VCJTZQSFQYVVWR6LVVZRWYNJ322TUBTTCJ','GA36VBVGF6AAXQZTR6ZJ5B42VCA3TQZEOCIOMKC6JH6SDF7RPPBHO5FF','GAOJM6HR6AIEYK45PSACEDZ5JA5SEIRIEGHU43XFXWUTGSWA6M6BYQPW','GDBQOSXNP4NKFAHGB5ADDQLCWC56AHQZXDHKZTUPIXHX6KXD2YCVKMJ6','GBBS374RPG55QYS73S4V32YDTFY5MLCSCOSYEUIXBSCGG4JKJYRLULLU','GCZR3XVMG5EDC5NGYAEKJPJSA37V7Y7YZGAUJORSPK2IHJD63DHUDRCJ',]:	faultpoint = ed25519.decodepoint(base64.b32decode(faultkey)[1:-2])	diff = ed25519.edwards(basepoint, [ed25519.q-faultpoint[0], faultpoint[1]])	results.append(mapping[(diff[0],diff[1])])results.sort()assert all(results[i][0] == i for i in range(32))priv = bytes(results[i][1] for i in range(32))print(priv.hex())#print(ed25519.encodepoint(basepoint).hex())#print(ed25519.publickey(priv).hex())assert basepoint == ed25519.scalarmult(ed25519.B,int.from_bytes(priv,'little')+(1<<254))```
Step 3: sign something. ed25519 private key remains hidden behind the hash. Elliptic private key is sufficient for signing, but it has to be done manually rather than providing a secret to existing library (stellar_sdk very conveniently provides a method that adds an externally-calculated signature - it seems to be intended for some protocol feature rather than hacking, but works for our purposes too):```import stellar_sdkimport ed25519
pub = 'GCET4VA7665VKBXEY7TQKC64AJ4CZPWBTJ2IKKUSAGZXI5KHMKQL6N7Z'account = stellar_sdk.Account(account=pub, sequence=1)transaction = stellar_sdk.TransactionBuilder(source_account=account, network_passphrase=stellar_sdk.Network.PUBLIC_NETWORK_PASSPHRASE, base_fee=100).build()
h = bytes.fromhex('9082a0b5fa7848f1af878c241fd572bf20f8d28373fda96f05ea472fc0a4f735')message = transaction.hash()a = int.from_bytes(h, 'little') + 2**254r = ed25519.Hint(h[256//8:256//4] + message)R = ed25519.scalarmult(ed25519.B, r)S = (r + ed25519.Hint(ed25519.encodepoint(R) + base64.b32decode(pub)[1:-2] + message) * a) % ed25519.lsignature = ed25519.encodepoint(R) + ed25519.encodeint(S)
transaction.sign_hashx(signature)print(transaction.to_xdr())```
Step 4: submit the result to the server```curl -D - http://stellar-radiation.donjon-ctf.io:25519/submit -d 'tx=AAAAAgAAAACJPlQf97tVBuTH5wUL3AJ4LL7BmnSFKpIBs3R1R2KgvwAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAABf7GHxgAAAEAY1TioiU8UyPM1OP351UCHxmpPt4vfBHAB1zlgb5sKXBtNlpKP/YFO4qaJ4wnoCsRNbsnDGK36BqLnwcgloewL'```to obtain the flag `CTF{a035b4e7444f8d3660d6bdce44f5d40823a730a70a53e64fb54305ab9700a647}`.