Skip to content

Add ML-DSA-65 support - #118

Draft
RankoR wants to merge 3 commits into
GrapheneOS:17from
RankoR-GOS:avb-mldsa65
Draft

Add ML-DSA-65 support#118
RankoR wants to merge 3 commits into
GrapheneOS:17from
RankoR-GOS:avb-mldsa65

Conversation

@RankoR

@RankoR RankoR commented Aug 22, 2026

Copy link
Copy Markdown

No description provided.

@RankoR
RankoR force-pushed the avb-mldsa65 branch 2 times, most recently from 921a2a4 to 984e6b0 Compare August 22, 2026 19:56
@chenxiaolong

Copy link
Copy Markdown
Contributor

Out of curiosity, have you been able to successfully register an ML-DSA-65 key with avb_custom_key on the Pixel 11 series?

I got a couple reports (in an unrelated project) where the bootloader rejects ML-DSA public keys with:

Writing 'avb_custom_key'                           (bootloader) avb custom key: flash failed
FAILED (remote: 'failed to flash partition (avb_custom_key): -1')

but RSA public keys still work fine. It seems to only affect fastboot flash avb_custom_key. The bootloader is happy with an RSA-signed vbmeta that uses AVB chain descriptors to verify other ML-DSA-signed partitions.

Seems there might be a bootloader bug in these new devices.

@inthewaves

inthewaves commented Aug 26, 2026

Copy link
Copy Markdown
Member

AFAIK, need to flash the latest OS release and boot it once for citadeld to update the GSC firmware, via init_epic.rc or, for an OTA, epic_postinstall_runner starting vendor.init_citadel_postinstall

The device is not guaranteed to have latest release (CD1A.260714.001.A9) out of the box.

This was from Pixel 11 Pro XL (kodiak) out of the box, where avb_pkmd.bin is assumed to be an ML-DSA-65 key

If fastboot flash avb_custom_key avb_pkmd.bin is done on an older release (flashing the bootloader and radio is not sufficient), an error will occur:

$ fastboot flash avb_custom_key avb_pkmd.bin
Warning: skip copying avb_custom_key image avb footer (avb_custom_key partition size: 0, avb_custom_key image size: 1956).
Sending 'avb_custom_key' (1 KB)                    OKAY [  0.017s]
Writing 'avb_custom_key'                           (bootloader) avb custom key: flash failed
FAILED (remote: 'failed to flash partition (avb_custom_key): -1')
fastboot: error: Command failed

With fastboot oem uart enable, the UART logs from attempting fastboot flash avb_custom_key avb_pkmd.bin under different fastboot oem uart mux [selection], a failure from the GSC AVB app can be observed.


fastboot oem uart mux apc

[  44.138624] <0> [I] [FB] Accept cmd:getvar:has-slot:avb_custom_key
[  44.140751] <0> [I] [FB] FAILvariable (has-slot:avb_custom_key) not found
[  44.141082] <0> [I] [FB] Accept cmd:getvar:max-download-size
[  44.141268] <0> [I] [FB] OKAY0x20000000
[  44.141445] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[  44.144614] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[  44.152072] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[  44.157667] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[  44.164596] <0> [I] [FB] Accept cmd:getvar:partition-size:avb_custom_key
[  44.171083] <0> [I] [FB] FAILvariable (partition-size:avb_custom_key) not found
[  44.178353] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[  44.184482] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[  44.191442] <0> [I] [FB] Accept cmd:download:000007a4
[  44.196255] <0> [I] [FB] Downloading. Download size is 1956 bytes
[  44.202231] <0> [I] [FB] DATA000007a4
[  44.205977] <0> [I] [FB] OKAY
[  44.208772] <0> [I] [FB] Accept cmd:flash:avb_custom_key
[  44.423288] <0> [E] [GSA] GSA: response error 129
[  44.423469] <0> [E] [GSA] mbox_send_cmd failed (-7): rsp = 0x0
[  44.423657] <0> [E] [GSC] mbox_nos_call failed: 4294967289
[  44.423851] <0> [E] [GSC-AVB] GSC AVB app error 0xfffffff9
[  44.424055] <0> [I] [FB] INFOavb custom key: flash failed
[  44.427739] <0> [I] [FB] FAILfailed to flash partition (avb_custom_key): -1

https://gitlab.com/grapheneos/kernel_pixel_spacecraft/-/blob/17-base/private/google-modules/soc/gs/drivers/soc/google/gsa/gsa_mbox.h?ref_type=heads assigns the recovered numeric values:

enum gsa_mbox_cmd {
    /* ... */
    GSA_MB_CMD_GSC_NOS_CALL = 65,  // 0x41
};

enum gsa_mb_error {
    GSA_MB_OK = 0U,
    /* ... */
    GSA_MB_ERR_GENERIC = 128U,
    GSA_MB_ERR_INTERNAL = 129U,
    GSA_MB_ERR_TIMED_OUT = 130U,
    GSA_MB_ERR_BAD_STATE = 131U,
};

fastboot oem uart mux apc

secure-transport: 1671: GSC: app_id (1) cmd_id (8) returned 0x9
secure-transport: 1671: GSC: app_id (1) cmd_id (8) returned 0x9

fastboot oem uart mux gsc

ROM_EXT:0.2026051500
ownership: OWND
verify: SlotA;key0;H+Pu;prod
entry: 0xa0010428
Build: longjing_v0.0.ab15475410-4cf952d-l 2026-05-22 04:01:23 nos-build
Images: RO-BRW-A
!!!!!!!!Hi We're in the Rust version of the avb!!!!!!!

MEM_ALLOC_FAIL
DECRYPT FAIL
sps_cmd_app_start

MEM_ALLOC_FAIL
DECRYPT FAIL
sps_cmd_app_start

The Build: longjing_v0.0.ab15475410-4cf952d-l 2026-05-22 04:01:23 nos-build indicates this is running an out-of-date build

After downloading the latest factory image for CD1A.260714.001.A9 from https://developers.google.com/android/images#kodiak and running its flash-all.sh script (and booting + completing Google's SetupWizard once), going back to fastboot and attempting fastboot flash avb_custom_key avb_pkmd.bin on the ML-DSA-65 key again results in:

$ fastboot flash avb_custom_key avb_pkmd.bin
Warning: skip copying avb_custom_key image avb footer (avb_custom_key partition size: 0, avb_custom_key image size: 1956).
Sending 'avb_custom_key' (1 KB)                    OKAY [  0.017s]
Writing 'avb_custom_key'                           (bootloader) avb custom key: flash done
OKAY [  0.305s]
Finished. Total time: 0.375s

fastboot oem uart mux apc

[ 870.736831] <0> [I] [FB] Accept cmd:getvar:has-slot:avb_custom_key
[ 870.737044] <0> [I] [FB] FAILvariable (has-slot:avb_custom_key) not found
[ 870.737350] <0> [I] [FB] Accept cmd:getvar:max-download-size
[ 870.737545] <0> [I] [FB] OKAY0x20000000
[ 870.737733] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[ 870.742813] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[ 870.749714] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[ 870.755874] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[ 870.763108] <0> [I] [FB] Accept cmd:getvar:partition-size:avb_custom_key
[ 870.769282] <0> [I] [FB] FAILvariable (partition-size:avb_custom_key) not found
[ 870.776544] <0> [I] [FB] Accept cmd:getvar:is-logical:avb_custom_key
[ 870.782689] <0> [I] [FB] FAILvariable (is-logical:avb_custom_key) not found
[ 870.789632] <0> [I] [FB] Accept cmd:download:000007a4
[ 870.794452] <0> [I] [FB] Downloading. Download size is 1956 bytes
[ 870.800439] <0> [I] [FB] DATA000007a4
[ 870.804157] <0> [I] [FB] OKAY
[ 870.806944] <0> [I] [FB] Accept cmd:flash:avb_custom_key
[ 871.113503] <0> [I] [FB] INFOavb custom key: flash done
[ 871.113702] <0> [I] [FB] OKAY

fastboot oem uart mux apc

No logs

fastboot oem uart mux gsc

ROM_EXT:0.2026051500
IMM_SECTION:0.3-a4a34e85
ownership: OWND
owner_page: 2-SEAL/2-SEAL
verify: SlotA;key0;H+Pu;prod
entry: 0xa0010428

M-Mode main!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Build: longjing_v0.0.ab15679664-0ebb176-l 2026-06-18 19:17:18 nos-build
Images: RO-BRW-A
Reset reason: 2
Deep sleep exit
Checking for inactive/staging header corruption...
Checking for an update...
Configuring sfdp
Configuring jedec
M-Mode main exiting!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
RC: 0
Cryptolib Version v1.0.4.15645946p
!!!!!!!!Hi We're in the Rust version of the avb!!!!!!!
Task exited!
Task idle ready
Reporting boot success. Owner version: 17

clear_owner_key erase NotFound error which deems accepted error.
clear_owner_key erase NotFound error which deems accepted error.
set owner key write successfully!

...calling deep sleep hooks...
...sleeping...

After flashing and booting CD1A.260714.001.A9 once, the GSC firmware build is now Build: longjing_v0.0.ab15679664-0ebb176-l 2026-06-18 19:17:18 nos-build which seems to have working ML-DSA support

@inthewaves

inthewaves commented Aug 26, 2026

Copy link
Copy Markdown
Member

Edit: Doesn't seem to boot yet, though seems to be just me using stock kernel on a user build without any other changes to accommodate it:

[    3.530795][  T105] hardened_malloc: fatal allocator error: failed to allocate slab region
[    3.540049][  T105] libc: hardened_malloc: fatal allocator error: failed to allocate slab region

Will need to try on a locked userdebug build since that seems to detect 39-bit VA and switch to scudo

@chenxiaolong

Copy link
Copy Markdown
Contributor

AFAIK, need to flash the latest OS release and boot it once for citadeld to update the GSC firmware, via init_epic.rc or, for an OTA, epic_postinstall_runner starting vendor.init_citadel_postinstall

I noticed in the OTA postinstall script in cd1a.260714.001.a9 (/vendor/bin/pixel_postinstall_vendor), it currently does not run epic_postinstall_runer due to some GSC reboot bug:

# Run epic_postinstall_runner
#
# TODO(b/532226255): Disabled due to reboot communication bug. Causes
# NUGGET_PARAM_AP_IS_REBOOTING call in gscproxy init to fail since an earlier
# call to enable deferred updates staged by the below script will reboot the
# GSC.
#
# Re-enable once:
#  1. Reboot-on-status-clear SPI communication bug is fixed
#  2. Postinstall script is able to determine GSC SPI communication bug is fixed
#     somehow
# /postinstall/bin/hw/epic_postinstall_runner

@inthewaves

Copy link
Copy Markdown
Member

Edit: Doesn't seem to boot yet, though seems to be just me using stock kernel on a user build without any other changes to accommodate it:

Stock kernel is able to boot on userdebug build with locked bootloader and ML-DSA avb_custom_key

@thestinger

Copy link
Copy Markdown
Member

@inthewaves Should I merge this then?

Comment thread generate-release.sh
Comment on lines 17 to 25
# decrypt keys in advance for improved performance and modern algorithm support
KEY_DIR=$(mktemp -d /dev/shm/generate-release.XXXXXXXXXX)
trap "rm -rf \"$KEY_DIR\" && rm -f \"$PWD/$RELEASE_OUT/keys\"" EXIT
cp "$PERSISTENT_KEY_DIR"/* "$KEY_DIR"
script/decrypt-keys "$KEY_DIR"

OLD_PATH="$PATH"
export PATH="$PWD/prebuilts/build-tools/linux-x86/bin:$PATH"
export PATH="$PWD/prebuilts/build-tools/path/linux-x86:$PATH"

@inthewaves inthewaves Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to move these PATH exports above script/decrypt-keys so that prebuilts/build-tools/linux-x86/bin/openssl can be used for script/decrypt-keys. Certain operating systems that are officially supported for building (https://grapheneos.org/build#build-dependencies) such as Ubuntu 24.04 LTS can have outdated OpenSSL versions that don't support ML-DSA

@RankoR RankoR Aug 26, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@inthewaves should similar be done in generate-delta.sh, encrypt-keys, generate-keys?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we should change those too

@inthewaves

inthewaves commented Aug 27, 2026

Copy link
Copy Markdown
Member

I noticed in the OTA postinstall script in cd1a.260714.001.a9 (/vendor/bin/pixel_postinstall_vendor), it currently does not run epic_postinstall_runer due to some GSC reboot bug:

I tested on another new device (11 Pro Fold) and this time updated using stock OS updater (stock OS Settings > System > Software updates > System update), and it'll just do the firmware update on the next boot

After updating from CD1A.260618.001.A9 (out of the box) to CD1A.260714.001.A9, the citadeld updates will look like this in logcat during the first boot into new update, especially indicated by Is RW in file newer: yes, Uploading firmware...

08-26 20:19:09.786  1139  1139 I init_citadel: init_citadel: Starting init_citadel
...
08-26 20:19:09.828  1139  1139 I init_citadel: init_citadel: Running RW version: longjing_v0.0.ab15475410-4cf952d-l 2026-05-22 04:01:23 nos-build
...
08-26 20:19:09.932  1139  1139 I init_citadel: init_citadel: Image file: /vendor/firmware/epic/pi01.ec.bin
...
08-26 20:19:10.079  1139  1139 I init_citadel: init_citadel: Is RW in file newer: yes
08-26 20:19:10.079  1139  1139 I init_citadel: init_citadel: Trying to update RW
08-26 20:19:10.079  1139  1139 I init_citadel: init_citadel: Checking RW firmware version...
...
08-26 20:19:10.106  1139  1139 I init_citadel: init_citadel: RW_A:  * 0.16/longjing_v0.0.ab15475410-4cf952d-l ok
08-26 20:19:10.106  1139  1139 I init_citadel: init_citadel: RW_B:    0.16/longjing_v0.0.ab15475410-4cf952d-l ok
08-26 20:19:10.106  1139  1139 I init_citadel: init_citadel: Uploading firmware...
...
08-26 20:19:10.141   667   683 D libnos_datagram: App 0x00 returning 0x20
08-26 20:19:10.141   667   683 E citadeld: App 0 returned Unkown error code: 32
08-26 20:19:10.141   667   683 I citadeld: reportCitadelAppCalled app_id =  0 arg = 1 app_status = 32
08-26 20:19:10.141   667   683 I citadeld: Recorded atom for RPC call
08-26 20:19:10.142   667   683 D libnos_datagram: Calling App 0x00 with params 0x0001
...
08-26 20:19:10.183   667   683 D libnos_datagram: App 0x00 returning 0x0
08-26 20:19:10.183   667   683 I citadeld: reportCitadelAppCalled app_id =  0 arg = 1 app_status = 0
08-26 20:19:10.183   667   683 I citadeld: Recorded atom for RPC call
08-26 20:19:10.183   667   683 D libnos_datagram: Calling App 0x00 with params 0x0001
...
08-26 20:19:15.244   667   667 D libnos_datagram: App 0x00 returning 0x0
08-26 20:19:15.244   667   667 I citadeld: reportCitadelAppCalled app_id =  0 arg = 10 app_status = 0
08-26 20:19:15.244   667   667 I citadeld: Recorded atom for RPC call
...
08-26 20:19:15.245  1139  1139 I init_citadel: init_citadel: All done

@inthewaves

Copy link
Copy Markdown
Member

https://android.googlesource.com/platform/external/nos/host/generic/+/android-9.0.0_r22/citadel/updater/updater.cpp#138 has some more context albeit in an outdated branch, since this seems to now be under Google's proprietary trees

Also readable via

$ adb root
$ adb shell /vendor/bin/hw/citadel_updater -hh

Usage: citadel_updater [actions] [image.bin]

Titan-M firmware boots in two stages. The first stage bootloader ("RO") is
provided by gChips and seldom changes. The application image ("RW") is
invoked by the RO image. There are two copies (A/B) of each stage, so that
the active copy can be protected while the unused copy may be updated. At
boot, the newer (valid) copy of each stage is selected.

The firmware image file is the same size of the internal flash, and contains
all four firmware components (RO_A, RW_A, RO_B, RW_B) located at the correct
offsets. Only the inactive copy (A/B) of each stage (RO/RW) can be modified.
The tool will update the correct copies automatically.

You must specify the actions to perform. With no actions, this help message
is displayed.

Actions:

  -v, --version            Display the running RW version
  -l, --long_version       Display the long version info, repeat for more

  --rw                     Update RW firmware from the image FILE
  --ro                     Update RO firmware from the image FILE
  --enable_deferred        Defer image enablement for provided headers. Any
                           headers deferred for enablement can be enabled
                           later with an empty password. Attempting to write
                           any flash blocks or enabling any headers will clear
                           and currently deferred enablement. 
  --enable_ro              Mark new RO image as good (requires password)
  --enable_rw              Mark new RW image as good (requires password)
  --trigger_deferred       Trigger any deferred updates pending on device
  --reboot                 Ask the chip to reboot

  --force_reset            Pulse the chip's reset line

  --id[d]                  Display the Device ID regs as DEV_ID0:DEV_ID1
                             Use --idd to decode the fields
  -V SECTION               Show header for RO_A | RO_B | RW_A | RW_B
  -F SECTION               Show FILE header for RO_A | RO_B | RW_A | RW_B
  -f                       Show FILE version info, repeat for more

  --stats                  Display Low Power stats
  --statsd                 Display Low Power stats cached by citadeld

  -D, --debug              Display the raw GSC debug info
  --event [NUM]            Get NUM pending event reports (default 1)
  --big_event [NUM]        Get NUM pending big event reports (default 1)
  --has_feature NUM        Query the feature supporting status

  --is_newer_ro            Is the FILE RO image newer than the running RO?
  --is_newer_rw            Is the FILE RW image newer than the running RW?

  --ap_uart [0|1]          Get the AP UART passthru setting. It can only be
                           set from the bootloader.
  --trigger [0|1]          Get/Set the trigger pin level (TEST_IMAGE only)

  --erase=CODE             Erase all user secrets and reboot. This skips all
                           other actions.

  -h, --help               Show this help message. Repeat for more.


Developer actions:

  --bootloader             Send AP_IS_REBOOTING command
  --stt                    Send SECURE_TRANSPORT_TEST command


Deprecated actions:

  --repo_snapshot          Show the repo sha1sums for the running image
  --change_pw [OLD NEW]    Change the update password
  --suzyq [0|1]            Get/Set the SuzyQable detection setting
  --board_id [TYPE FLAG]   Get/Set board ID values

  --selftest [ARGS]        Run one or more selftests. With no ARGS, it runs
                           a default suite. This command will consume all
                           following args, so run it alone for best results.

Options:

  --rw_ofs NUM             Search for RW headers at this hex offset in each
                           half of an image file. The default is immediately
                           after the smallest possible RO.

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.

4 participants