Skip to content

fix(signing): replace SHA-256 hash with real Ed25519 signature (#12)#74

Open
gbengaeben wants to merge 2 commits into
OrbitChainLabs:mainfrom
gbengaeben:main
Open

fix(signing): replace SHA-256 hash with real Ed25519 signature (#12)#74
gbengaeben wants to merge 2 commits into
OrbitChainLabs:mainfrom
gbengaeben:main

Conversation

@gbengaeben

Copy link
Copy Markdown
Contributor

Closes #12.

SigningRequest::sign_server_side previously produced SHA-256(secret_key || xdr), which is not a digital signature — it can only be re-derived by callers who already hold the secret, so third parties (auditors, dashboards, downstream services) cannot verify it. The struct field was named signature and the issue is labelled security.

This change replaces the hash with a real RFC 8032 Ed25519 signature via ed25519-dalek 1.3. Any holder of the signer's public key can now verify the signature without seeing secret material.

Changes

  • ServerSignedTransaction gains an explicit algorithm (serde-default "ed25519") and signer_public_key (serde-default empty) so the signature scheme and the verifier anchor are unambiguous. The added fields preserve backward-compatible deserialisation of pre-[LOW] Server-side signing uses SHA-256 hash instead of Ed25519 digital signature #12 JSON.
  • New ServerSignedTransaction::verify() returns Ok(true) / Ok(false) / Err with a documented # Errors section and rejects unknown algorithms, missing public keys, malformed strkeys (CRC mismatch), non-hex signatures, and wrong-length signatures.
  • Inline Stellar strkey codec (Crockford Base32 + CRC16-XModem) keeps the new transitive-dependency surface to one crate instead of pulling in both ed25519-dalek and stellar-strkey.
  • SecretKey is zeroized on drop via ed25519_dalek::SecretKey's Zeroize derive.
  • Tests: keep the existing build/validate/roundtrip tests; add strkey roundtrip + bad-CRC rejection, deterministic signing, end-to-end verification roundtrip, XDR tamper rejection, single-bit signature flip rejection, wrong-public-key rejection, unknown-algorithm rejection, missing-public-key rejection, JSON forward roundtrip, and legacy-payload deserialisation.

Verification

CI: cargo fmt --check, cargo clippy, cargo test -p orbitchain-tools. The authoritative check runs on upstream CI; the implementation sandbox did not have a Rust toolchain at coding time, so no local run was possible.

…ChainLabs#12)

The previous `sign_server_side` produced `SHA-256(secret_key || xdr)` —
a value that could only be re-derived by callers that already had the
secret key, so it was not a signature in any usable sense and could
not be verified by a third party.

This change replaces that hash with a proper Ed25519 digital signature
(RFC 8032) using `ed25519-dalek`. Any holder of the signer's public
key can now verify the signature without touching secret material,
which is the property the function name always implied.

Key points
- Adds `ed25519-dalek = "1.3"` (pinned; v1 has the straightforward
  `Keypair::sign -> Signature` API we need).
- `ServerSignedTransaction` gains an explicit `algorithm`
  (serde-default `"ed25519"`) and `signer_public_key` (serde-default
  empty) so verifiers and future migrations are unambiguous. Legacy
  pre-OrbitChainLabs#12 JSON still deserialises.
- New `ServerSignedTransaction::verify()` returns `Ok(true)` /
  `Ok(false)` / `Err` for authentic / forged / malformed payloads and
  is documented with `# Errors`.
- Inline Stellar strkey codec (Crockford Base32 + CRC16-XModem) so the
  new dependency surface stays at one crate instead of pulling in
  both `ed25519-dalek` and `stellar-strkey`.
- Tests: keep the existing build/validate/round-trip tests, add
  strkey roundtrip + bad-CRC rejection, signing determinism,
  signature verification, XDR tamper rejection, single-bit signature
  flip rejection, wrong-public-key rejection, unknown-algorithm
  rejection, missing-public-key rejection, JSON roundtrip, and
  backward-compatible deserialisation of legacy payloads.

Refs: OrbitChainLabs#12
Follow-up to OrbitChainLabs#12. The original PR pinned `ed25519-dalek = "1.3"` but
the crates.io `max_stable_version` is now `2.2.0` (v3.x is pre-release
on crates.io), so the workspace resolver failed in CI:

  error: failed to select a version for the requirement
         `ed25519-dalek = "^1.3"`

This commit:

- Bumps the dep to `ed25519-dalek = "2.2"` so the resolver is happy.
- Migrates the call site to the v2.x API:
    * `Keypair { secret, public }` -> `SigningKey` (single struct).
    * `SecretKey::from_bytes` -> `SigningKey::from_bytes`.
    * `PublicKey::from(&secret)` -> `signing_key.verifying_key()`.
    * `keypair.sign(msg)` -> `signing_key.sign(msg)` (same
      RFC 8032 deterministic Ed25519 path, no RNG required).
    * `public.as_bytes()` -> `verifying_key.to_bytes()`.
- Drops the now-unused `Keypair`, `PublicKey`, `SecretKey` imports.
- Adds a one-line comment on the deterministic-sign property so the
  next reader does not accidentally swap in a randomized signer.

Behavioural surface (test coverage) is unchanged; the new test set
added in OrbitChainLabs#12 already exercises the signing, verification, tampering
rejection, and JSON round-trip paths the verifier depends on.

Refs: OrbitChainLabs#74 (CI feedback)
Refs: OrbitChainLabs#12
@GBOYEE

GBOYEE commented Jun 24, 2026

Copy link
Copy Markdown

I'd like to work on this.

Approach:

  • I'll add the audit logging with proper schema and query support
  • Verify with existing tests + add new ones if needed

Estimated effort: ~1-2 hours. PR incoming shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LOW] Server-side signing uses SHA-256 hash instead of Ed25519 digital signature

2 participants