The Android side is being rewritten as a platform-signed system app + device owner (android_packages_apps_PawletProfiled) instead of a native NDK Binder daemon -- most of what these payloads need on Android (DevicePolicyManager, VpnManager, WifiManager, WallpaperManager, KeyChain) is Java-SDK-first and awkward or impossible to reach cleanly from a native process. Drops: Android.bp, main_android.cpp, PawletProfileBinderService.*, platform/android/*.cpp, aidl/, sepolicy/, third_party/openssl-android/, pawletprofiled.rc/.xml. Also strips the now-dead __ANDROID__ branches from SignatureVerifier/ProfileStore. This repo is Linux-only from here on.
pawletprofiled
PawletOS's Linux device management daemon — a native root-run systemd service. One binary, two subsystems.
This repo is Linux-only. The Android implementation is a separate,
independently-written app (platform-signed priv-app + device owner, not a
native daemon — most of what these payloads need on Android is Java-SDK
first): android_packages_apps_PawletProfiled.
Same .vconfig profile format and payload types on both platforms; no
shared code between the two implementations.
Subsystems
Profile Service
D-Bus system service at os.pawlet.ProfiledService.
Parses, verifies, persists, and applies .vconfig profiles.
The allowed payload types are controlled by the separately-installed
pawletprofiled-config-schema package — OEMs can restrict the
configuration surface without touching the daemon binary.
ZTE Client
Zero-Touch Enrollment. Runs as a background thread. Watches NetworkManager for internet connectivity. On first connection, queries a ZTE lookup server with the device's hardware identity. Downloads and installs the enrollment profile automatically via the local D-Bus service. Handles the deferred case: if no network at first boot, enrolls on the first successful connection later.
Package layout
pawletprofiled/
├── CMakeLists.txt Linux CMake build
├── zte.conf.example ZTE client config
│
├── src/
│ ├── main.cpp Entry point
│ ├── PawletProfileService.h/.cpp D-Bus service
│ ├── ProfileParser.h/.cpp YAML parser (libyaml)
│ ├── ProfileStore.h/.cpp Disk persistence + password hashing
│ ├── SignatureVerifier.h/.cpp CMS/PKCS#7 verification (OpenSSL)
│ ├── payloads/
│ │ ├── PayloadHandler.h Handler contract + PayloadUtil.h helpers
│ │ └── PayloadHandlers.cpp Dispatch — forwards to platform/linux/*.cpp
│ └── platform/
│ ├── <Name>.h Per-payload-type contract (apply/revert)
│ └── linux/<Name>.cpp One real implementation per payload type
│
├── src/zte/
│ ├── DeviceIdentity.h/.cpp Hardware identity collection
│ ├── ZTELookupClient.h/.cpp ZTE server HTTPS client (libcurl)
│ └── ConnectivityWatcher.h/.cpp NM D-Bus connectivity watcher
│
├── systemd/pawletprofiled.service Systemd unit
├── dbus/os.pawlet.ProfiledService.conf D-Bus policy
├── apparmor/pawletprofiled AppArmor MAC profile
└── debian/ Debian Trixie packaging
Dependencies
Runtime
| Package | Used for |
|---|---|
pawletprofiled-config-schema |
Schema defining allowed payload types |
libssl3 |
CMS signature verification, PBKDF2 |
libyaml-0-2 |
YAML parsing |
libdbus-1-3 |
D-Bus system service |
libcurl4 |
ZTE HTTPS lookups |
network-manager |
WiFi/Ethernet/VPN keyfile reload |
Recommended (enable specific payload handlers)
| Package | Payload types |
|---|---|
cloud-init |
mdm, first-boot |
nftables |
firewall |
network-manager-strongswan |
vpn (IKEv2) |
network-manager-l2tp |
vpn (L2TP) |
libpam-pwquality |
passcode (complexity) |
libpam-faillock |
passcode (lockout) |
libnss3-tools |
pkcs12 (NSSDB import) |
unattended-upgrades |
software-update |
dconf-cli |
desktop, screensaver, first-boot |
tpm2-tools |
ZTE TPM EK detection |
Building — Linux (Debian Trixie)
# Build dependencies
sudo apt install -y \
cmake libssl-dev libyaml-dev libdbus-1-dev \
libsystemd-dev libcurl4-openssl-dev pkg-config
# Build .deb (includes pawletprofiled-config-schema as a dep)
dpkg-buildpackage -us -uc -b
sudo apt install \
../pawletprofiled-config-schema_1.0.0-1_all.deb \
../pawletprofiled_1.0.0-1_amd64.deb
# Or build without packaging
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
sudo cmake --install build
Android
Not built from this repo — see
android_packages_apps_PawletProfiled,
a platform-signed priv-app + device owner with its own independent
implementation of every payload type in this daemon's schema.
Verifying the installation
# Service status
systemctl status pawletprofiled
# D-Bus connectivity
dbus-send --system --print-reply \
--dest=os.pawlet.ProfiledService \
/os/pawlet/ProfiledService \
os.pawlet.IProfiledService.IsDeviceManaged
# List installed profiles
dbus-send --system --print-reply \
--dest=os.pawlet.ProfiledService \
/os/pawlet/ProfiledService \
os.pawlet.IProfiledService.ListProfiles
# ZTE enrollment state
cat /var/lib/pawletprofiled/zte_state.json
# Live logs
journalctl -u pawletprofiled -f
Installing a profile from the command line
#!/usr/bin/env python3
import dbus, sys
with open(sys.argv[1], 'rb') as f:
data = f.read()
bus = dbus.SystemBus()
obj = bus.get_object('os.pawlet.ProfiledService',
'/os/pawlet/ProfiledService')
svc = dbus.Interface(obj, 'os.pawlet.IProfiledService')
uuid = svc.InstallProfile(dbus.ByteArray(data))
print('Installed profile UUID:', uuid)
Signing profiles
Profiles containing mdm, kiosk, asam, or removal-password payloads
must be CMS/PKCS#7 signed. Unsigned profiles install with a trust warning.
# Generate a development signing key and CA
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout /etc/pawletprofiled/signing_key.pem \
-out /etc/pawletprofiled/profile_ca.pem \
-subj "/CN=PawletOS Profile Signing/O=oxmc"
# Sign a profile
openssl cms -sign \
-in my.vconfig \
-out my.signed.vconfig \
-signer /etc/pawletprofiled/profile_ca.pem \
-inkey /etc/pawletprofiled/signing_key.pem \
-outform DER -nodetach
The CA at /etc/pawletprofiled/profile_ca.pem is trusted automatically.
For a system-wide trusted badge, also install it as a system CA:
cp /etc/pawletprofiled/profile_ca.pem \
/usr/local/share/ca-certificates/pawletos-profile-ca.crt
update-ca-certificates
Preinstalled profiles
Drop signed .vconfig files into /etc/pawletprofiled/preinstalled/
before first boot. The daemon applies them at startup,
before cloud-init-local.service starts (enforced via
Before=cloud-init-local.service in the systemd unit).
This lets image builders bake in WiFi credentials, first-boot GNOME setup suppression, and MDM enrollment — all applied before cloud-init tries to reach its datasource.
Zero-Touch Enrollment
ZTE configuration lives at /etc/pawletprofiled/zte.conf (copy from
zte.conf.example). The key setting is server_url, which points to
your pawlet-zte-server instance.
server_url=https://zte.yourdomain.com
Hardware identity priority used for the ZTE lookup:
| Priority | Source | Path |
|---|---|---|
| 1 | DMI system UUID | /sys/class/dmi/id/product_uuid |
| 2 | Board serial | /sys/class/dmi/id/board_serial |
| 3 | System serial | /sys/class/dmi/id/product_serial |
| 4 | machine-id | /etc/machine-id |
| 5 | Permanent MAC | /sys/class/net/<if>/perm_address |
Bugs fixed vs. previous iterations
| Bug | Fix |
|---|---|
ProfileParser.cpp used android-base/logging.h and LOG() |
Replaced with syslog() throughout |
debian/rules had postinst install -d lines leaked after override_dh_installsystemd |
Removed stray lines |
sepolicy/pawletprofiled.te had typo vesperprofrofiled |
Fixed |
apparmor/pawletprofiled used abstractions/openssl (doesn't exist on Debian) |
Replaced with explicit /etc/ssl/certs/ and network socket rules |
ZTELookupClient called curl_global_init but never curl_global_cleanup |
Added to destructor |
debian/control missing Depends: pawletprofiled-config-schema |
Added |