oasisprotocol / sapphire-paratime

Oasis Sapphire - the confidential EVM-compatible ParaTime for the Oasis Network
https://oasisprotocol.org/sapphire
Apache License 2.0
35 stars 26 forks source link

Verify calldata publickey signature #366

Open CedarMist opened 2 weeks ago

CedarMist commented 2 weeks ago

A long-term keypair is used to sign the validity of the calldata pubic key.

This keypair can be hard-coded into the clients, and used to verify it.

This means the Oasis RPC servers won't be in a trusted position.

Return the runtime signing (public) key from consensus:

status, err := cons.KeyManager().Secrets().GetStatus(ctx, &registry.NamespaceQuery{Height: consensus.HeightLatest, ID: keymanagerRuntimeID})
// ...
// Get the Runtime Signing (public) Key from: status.RSK

In go code, see: https://github.com/oasisprotocol/oasis-core/blob/050a01f97be8afa6d079fda07952330035b790c1/go/keymanager/secrets/api.go#L295-L302

In rust code, see: https://github.com/oasisprotocol/oasis-core/blob/050a01f97be8afa6d079fda07952330035b790c1/keymanager/src/crypto/types.rs#L157-L170

Verification happens via:

const PUBLIC_KEY_SIGNATURE_CONTEXT: &[u8] = b"oasis-core/keymanager: pk signature";

...

        let body = Self::body(
            self.key,
            &self.checksum,
            runtime_id,
            key_pair_id,
            epoch,
            self.expiration,
        );

        self.signature
            .verify(pk, PUBLIC_KEY_SIGNATURE_CONTEXT, &body)

...
// Signature message is constructed like such

    fn body(
        key: x25519::PublicKey,
        checksum: &[u8],
        runtime_id: Namespace,
        key_pair_id: KeyPairId,
        epoch: Option<EpochTime>,
        expiration: Option<EpochTime>,
    ) -> Vec<u8> {
        let mut body = key.0.as_bytes().to_vec();
        body.extend_from_slice(checksum);
        body.extend_from_slice(runtime_id.as_ref());
        body.extend_from_slice(key_pair_id.as_ref());
        if let Some(epoch) = epoch {
            body.extend_from_slice(&epoch.to_be_bytes());
        }
        if let Some(expiration) = expiration {
            body.extend_from_slice(&expiration.to_be_bytes());
        }
        body
    }

...

    /// Compute a digest of the passed slices of bytes.
    pub fn digest_bytes_list(data: &[&[u8]]) -> Hash {
        let mut ctx = Sha512_256::new();
        for datum in data {
            ctx.update(datum);
        }

        let mut result = [0u8; 32];
        result[..].copy_from_slice(ctx.finalize().as_ref());

        Hash(result)
    }

...

// Domain separation is added when verifying signature
    pub fn verify(&self, pk: &PublicKey, context: &[u8], message: &[u8]) -> Result<()> {
        // Apply the Oasis core specific domain separation.
        //
        // Note: This should be Ed25519ctx based but "muh Ledger".
        let digest = Hash::digest_bytes_list(&[context, message]);

        self.verify_raw(pk, digest.as_ref())
    }

The hash here is a 32-bute SHA-512/256.

Note, we need the key_pair_id and runtime_id from somewhere to verify the signature, these are both 32-bytes each.

Note: Sign(sk, (key || checksum || runtime id || key pair id || epoch || expiration epoch))

CedarMist commented 2 weeks ago

If we're verifying ed25519 should probably migrate to @noble/curves, and a function like:


function verifyRuntimePublicKey(pk:CallDataPublicKey, runtime_id:Uint8Array, key_pair_id:Uint8Array)
{
  const PUBLIC_KEY_SIGNATURE_CONTEXT = new TextEncoder().encode("oasis-core/keymanager: pk signature");

  let body = new Uint8Array([
    ...pk.key,
    ...pk.checksum,
    ...runtime_id,
    ...key_pair_id
  ]);

  if( pk.epoch !== undefined ) {
    body = new Uint8Array([...body, ...u64tobytes(pk.epoch)]);
  }

  if( pk.expiration !== undefined ) {
    body = new Uint8Array([...body, ...u64tobytes(pk.expiration)]);
  }

  const ctx = sha512_256.create();
  ctx.update(PUBLIC_KEY_SIGNATURE_CONTEXT);
  ctx.update(body);
  const digest = ctx.digest();

  // TODO: verify pk.signature with digest and pk.key
}

Trying to determine the differences between https://github.com/oasisprotocol/oasis-core/blob/050a01f97be8afa6d079fda07952330035b790c1/runtime/src/common/crypto/signature.rs#L125 and the @noble/curves ed25519 verification. It should be straightforward, but need to figure out the details.

@noble/curves specifies ( https://github.com/paulmillr/noble-curves/blob/c1eb761af14b811822abf971b15669dacf7feec6/src/ed25519.ts#L134C1-L142C2 )

function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
  if (ctx.length > 255) throw new Error('Context is too big');
  return concatBytes(
    utf8ToBytes('SigEd25519 no Ed25519 collisions'),
    new Uint8Array([phflag ? 1 : 0, ctx.length]),
    ctx,
    data
  );
}

So, presumably we need to provide our own domain function. but ed25519Defaults isn't exported, and we can't modify the domain after:

export const ed25519ctx = /* @__PURE__ */ (() =>
  twistedEdwards({
    ...ed25519Defaults,
    domain: ed25519_domain,
  }))();