Code Signing

How DeployYourApp signs update bundles with RSA-4096, how each client verifies the signature, and how to rotate a signing key.


Every bundle you deploy can carry an RSA signature that proves it came from your signing key. The server verifies it on upload, and each client verifies it again before applying the bundle.

Algorithm

Property Value
Key type RSA, 4096-bit modulus
Signature scheme RSASSA-PKCS1-v1_5 (not PSS)
Digest SHA-256
Signature encoding Base64
Public key format on the wire PEM, X.509 SubjectPublicKeyInfo (-----BEGIN PUBLIC KEY-----)
Private key format on disk PEM, PKCS#8 (-----BEGIN PRIVATE KEY-----)

The signature is computed over the exact bytes that are uploaded. When bundle encryption is enabled, those bytes are the ciphertext — encryption runs before checksumming and signing. See Bundle encryption.

Generating keys

Run this from your project directory:

dya keys generate

It creates four files in the current working directory — two key pairs, one for signing and one for encryption:

File Purpose Where it belongs
dya-signing-private.pem Signs bundles during dya deploy Your machine or a CI secret. Never commit it
dya-signing-public.pem Verifies signatures Uploaded to the server, and set as the plugin's publicKey
dya-encryption-public.pem Encrypts bundles during dya deploy Stays with the CLI; also uploaded to the server
dya-encryption-private.pem Decrypts bundles on the device Embedded in your app's native binary

Both private key files are written with mode 0600.

If you are logged in and have an active organization and an app ID in your project config, dya keys generate uploads both public keys to the server automatically. If that upload is skipped or fails, run it explicitly:

dya keys upload --app-id com.example.app

Either path calls POST /api/orgs/:orgId/apps/:appId/keys with publicKey and, when the file exists, encryptionPublicKey. The server stores them on the app record. It never receives a private key.

dya keys generate refuses to run when dya-signing-private.pem already exists. Delete the existing key files first if you intend to replace them.

Configuring the client

The server sends the signature to the device in the update-check response. The client only verifies it if you give the client the public key.

Capacitorcapacitor.config.json:

{
  "plugins": {
    "DeployYourApp": {
      "appId": "com.example.app",
      "publicKey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBg...\n-----END PUBLIC KEY-----"
    }
  }
}

Electron — pass it to the constructor:

import { ElectronUpdateManager } from '@deploy-your-app/electron-update-manager';

const updater = new ElectronUpdateManager({
  appId: 'com.example.app',
  publicKey: process.env.DYA_PUBLIC_KEY,
});

Use the X.509 SPKI form — the contents of dya-signing-public.pem as generated. The Android client parses the public key with X509EncodedKeySpec and will not accept a PKCS#1 (-----BEGIN RSA PUBLIC KEY-----) key.

Signing on deploy

dya deploy signs automatically when dya-signing-private.pem is present in the working directory. The order is fixed:

Build web assets
      |
      v
Bundle and compress (zip)
      |
      v
Encrypt with AES-256-GCM   (only if dya-encryption-public.pem exists)
      |
      v
SHA-256 checksum of the bytes to be uploaded
      |
      v
RSA-SHA256 signature over the same bytes
      |
      v
Upload bundle + checksum + signature (+ wrapped session key)

If no signing key is found, the CLI prints a warning and uploads the bundle with no signature field.

Server-side verification

On POST /api/orgs/:orgId/apps/:appId/bundles the server:

  1. Checks whether the app has a registered signing public key. If it does and the upload has no signature, the upload is rejected with a message naming the missing key file.
  2. Verifies the signature over the uploaded bytes with RSA-SHA256 against the registered public key. An invalid signature is rejected.
  3. Verifies the SHA-256 checksum using a timing-safe comparison. A mismatch is rejected.

An app with no registered public key accepts unsigned uploads. Register a key to close that path.

Client-side verification

All three clients verify in the same order, over the downloaded bytes, before any decryption or unzip:

  1. SHA-256 checksum against the checksum field from the update-check response.
  2. RSA signature against the configured publicKey, if one is configured.
  3. Only then decrypt (when a sessionKey was returned) and unpack.
Client Verification API Digest handling
iOS SecKeyVerifySignature with .rsaSignatureDigestPKCS1v15SHA256 Pre-hashes with CC_SHA256 and passes the digest
Android Signature.getInstance("SHA256withRSA") Passes the full data; the provider hashes
Electron createVerify('SHA256') Passes the full data; Node hashes

A failed signature check aborts the update, emits an update_verify_fail analytics event with reason: "signature_invalid", and leaves the currently active bundle in place.

When verification is skipped

Signature verification is opt-in on the client. If publicKey is not set in the plugin or updater configuration, none of the three clients verify a signature. The only remaining integrity check is the SHA-256 checksum, and that checksum arrives in the same response as the download URL — it protects against transport corruption, not against a compromised update server. Set publicKey in production.

There is a second, narrower gap. If the server returns signature: null for a bundle that was uploaded unsigned:

  • iOS and Android skip verification and apply the bundle. Their condition requires both a configured publicKey and a non-null signature.
  • Electron aborts the download, because its condition tests publicKey alone.

The practical consequence is that a client-side publicKey does not by itself guarantee every bundle was signed. Register a signing public key on the app so the server refuses unsigned uploads in the first place.

Key rotation

Rotate when a signing private key may have been exposed, or on whatever schedule your own policy sets.

  1. Delete dya-signing-private.pem and dya-signing-public.pem from your working directory and from any CI secret store.
  2. Run dya keys generate. It creates a new pair and uploads the new public key, replacing the one on the app record.
  3. Update the publicKey value in your Capacitor config or Electron updater config, and ship a native app release containing it.
  4. Redeploy your bundles with dya deploy so they carry signatures from the new key.

Sequencing matters. The server verifies against a single registered public key, so bundles signed with the old key stop passing upload verification as soon as the new key is registered. Devices still running the old native binary hold the old public key and will reject bundles signed with the new one — so a rotation requires a native release, not only an OTA deploy.

Bundles already downloaded and applied on a device are not recalled by a rotation. If you are rotating because a key leaked, also publish a clean bundle and mark it mandatory.


Previous
Security Overview
Next
Encryption