Weather on the Nokia 8110 4G (KaiOS 2.5 / GerdaOS), using the same open data as Yr. Norway first, but any place in the world works. It's built for a 240x320 screen and a keypad.
- Now: temperature, conditions, wind with a direction arrow, rain over the next hour, and the next four hours
- Rain radar for the next 90 minutes (MET nowcast, Nordic radar area only), in 5-minute steps: "Rain in ~20 min", "Rain stops in ~10 min"
- Hours: hour by hour for 48 hours, with temperature, precipitation and wind
- Days: 9 days, with high/low, total precipitation, strongest wind and a daytime symbol
- Places, worldwide: search finds Norwegian places through Kartverket (down to farms, peaks and lakes) and places anywhere else through Open-Meteo/GeoNames. Norwegian results come first and duplicates are merged. My location uses GPS (named after the municipality in Norway). Up to 9 places, on number keys 1-9.
- Local time: every time and date is shown in the place's own time zone. When that differs
from the phone's, the header says so (e.g.
16:29 UTC-4). The day icon is the 6-hour block nearest local noon. - Letters the keypad lacks: in search, SoftRight opens a picker with æ ø å ä ö ü é è á à ó í ú ñ ç ß.
Typing
aa,oeoraealso finds å, ø and æ names ("Aalesund" finds Ålesund). - Polite caching: forecasts are reused until MET's
Expirestime and revalidated withIf-Modified-Since, which saves data and follows the API terms - Shows the last saved forecast when offline
| Screen | Key | Action |
|---|---|---|
| Weather | Left / Right | Switch Now / Hours / Days |
| Up / Down | Scroll | |
| 1-9 | Switch to saved place 1-9 | |
| SoftLeft | Places | |
| SoftRight | Refresh (only fetches if MET has published newer data) | |
| Places | Up / Down, Enter | Pick a place, My location, or Search |
| SoftRight | Remove the selected place | |
| SoftLeft / Back | Back | |
| Search | Type, then Enter | Search (T9 works; # switches input mode) |
| SoftRight | Letter picker (æ ø å ...): arrows to choose, Enter inserts, SoftLeft closes | |
| Down / Up, Enter | Pick a result to add it |
- Forecasts: MET Norway Locationforecast 2.0 and Nowcast 2.0, CC BY 4.0. Shown in the app as "Data: MET Norway".
- Place names and municipalities in Norway: Kartverket via ws.geonorge.no, CC BY 4.0.
- Places outside Norway and each place's time zone: Open-Meteo geocoding
(data from GeoNames, CC BY 4.0) and forecast API (
timezone=auto), free for non-commercial use, CC BY 4.0. - Weather symbols: metno/weathericons, MIT, © Yr.
The license is in
app/weather/LICENSE.
Kuling is an independent hobby project. It is not affiliated with Yr, NRK or MET Norway.
MET requires an identifying User-Agent, caching per Expires, at most 4 decimals in coordinates,
and modest traffic. The app sets User-Agent: kuling/1.0 https://github.com/hanzhe (possible
because the app is privileged and uses systemXHR). It also rounds coordinates and caches as
described above. MET allows direct calls from low-volume apps. A wider release should go through a
caching proxy instead (see their terms of service).
Out of the box, Kuling showed "No connection" on the 8110, while place search and other apps
worked. The phone log (adb logcat) explained it:
api.met.no:443 uses an invalid security certificate.
Error code: SEC_ERROR_UNKNOWN_ISSUER
api.met.no uses a GÉANT certificate issued by HARICA:
*.api.met.no
<- GEANT TLS RSA 1
<- HARICA TLS RSA Root CA 2021 (cross-signed, sent by the server)
<- Hellenic Academic and Research Institutions RootCA 2015
The phone runs Gecko 48 with a 2016 NSS and a 2016 list of built-in roots. That list has HARICA RootCA 2011 but not RootCA 2015, so the chain cannot reach a trusted root. Plain HTTP is no way around it, because MET redirects to HTTPS. Kartverket (DigiCert) and the radio streams use roots the phone already knows.
Current Firefox, Chrome and macOS already trust this root (valid until 2040). Adding it brings the phone in line with a modern browser for this one CA. It is trusted for websites only.
- Get the genuine root. It was taken from macOS's
/etc/ssl/cert.pemand cross-checked against curl's Mozilla-derivedcacert.pem. Both give SHA-256A0:40:92:9A:02:CE:53:B4:AC:F4:F2:FF:C6:98:1C:E4:49:6F:75:5E:6D:45:FE:0B:2A:69:2B:CD:52:52:3F:36.openssl verifyconfirmed that MET's served chain validates against it. - Find the database b2g really uses.
/proc/<b2g pid>/fdshows/data/b2g/mozilla/<profile>.default/cert9.dbandkey4.db(SQLite NSS databases). - Add the certificate with Mozilla's tools (
brew install nss, thencertutil -A -t "C,,"), working on copies pulled while b2g is stopped (adb shell stop b2g). - First surprise: trust format. NSS 3.131 stores trust as a PKCS#11 3.2
CKO_TRUSTobject (class0x0b). Gecko 48's NSS only reads the legacyCKO_NSS_TRUST(class0xce534353), so the root was present but untrusted. The record was rewritten in the legacy format, mirroring the working ISRG Root X1 record already in the database: server-authCKT_NSS_TRUSTED_DELEGATOR, other usesCKT_NSS_VALID_DELEGATOR, plus the certificate's SHA-1 and MD5 hashes. - Second surprise: signed trust. It still had no effect. Tracing NSS's PKCS#11 calls
(
NSS_DEBUG_PKCS11_MODULE) showed the trust lookup returning nothing, although SQLite matched the row. NSS signs "authenticated attributes" (trust values, cert hashes) and ignores unsigned ones. The signatures sit inkey4.db'smetaDatatable assig_cert_<object id>_<attribute>:A short Python implementation of this reproduced all 14 existing ISRG signatures byte for byte before anything was written. It then signed the seven attributes of the new trust record.pass_key = SHA1(global_salt || password) # empty password on this phone; # global_salt = metaData['password'].item1 key = PBKDF2-HMAC-SHA256(pass_key, salt, iterations=1, 32 bytes) mac = HMAC-SHA256(key, BE32(object id) || BE32(attribute type) || value) stored = DER: PBMAC1 { PBKDF2 { salt, 1, 32, hmacWithSHA256 }, hmacWithSHA256 } + OCTET STRING mac - Verify before pushing. On the local copies,
certutil -Lshows the root asC,c,c.vfychain -pp -u 1reports "Chain is good!" for MET's chain and for a Let's Encrypt control chain. - Push and restart. The script first checked that the phone's databases had not changed since
they were copied, backed both up on the phone, pushed
cert9.db+key4.db, then ranadb shell start b2g. Kuling loaded forecasts right after.
The steps above are automated, and the script refuses to push unless every check passes:
brew install nss # certutil + vfychain
# Kuling's root, from the macOS trust store (also in curl's cacert.pem):
security find-certificate -a -p -c "Hellenic Academic and Research Institutions RootCA 2015" \
/System/Library/Keychains/SystemRootCertificates.keychain > harica2015.pem
# 1. rehearse on copies (phone untouched): pull cert9.db, key4.db, pkcs11.txt into ./nssdb first
python3 scripts/certs/add_root.py harica2015.pem \
--sha256 A0:40:92:9A:02:CE:53:B4:AC:F4:F2:FF:C6:98:1C:E4:49:6F:75:5E:6D:45:FE:0B:2A:69:2B:CD:52:52:3F:36 \
--dry-run ./nssdb
# 2. for real: stops b2g, works on copies, checks, backs up both DBs on the phone, pushes, restarts
python3 scripts/certs/add_root.py harica2015.pem --sha256 A0:40:...:3F:36The script:
- refuses a root whose SHA-256 fingerprint doesn't match
--sha256 - refuses to sign unless it reproduces every signature already in
key4.db, so a master password or a different format stops it - refuses to push unless NSS lists the root as a trusted CA, and unless
--chain LEAF INTER...validates withvfychain(when given) - can be re-run safely: it replaces the root's trust record instead of adding a second one
scripts/certs/nsssig.py holds the signing code and is unit tested in tests/test_nsssig.py. The
script was tested end to end on copies of this phone's original databases. The first, manual fix on
the phone encoded the HMAC algorithm with NULL parameters, which NSS also accepts. The script
writes NSS's exact encoding.
The original and intermediate states are kept on the phone in
/data/b2g/mozilla/<profile>.default/:
| File | State |
|---|---|
cert9.db.bak-1792531491 |
original, before any change |
cert9.db.bak-modern-1792531810 |
root added, trust in the new (unreadable) format |
cert9.db.bak-presign-1792532299, key4.db.bak-presign-1792532299 |
legacy trust, not yet signed |
To undo: adb shell stop b2g, copy the original cert9.db back (and the key4.db backup), then
adb shell start b2g.
- This is a per-phone change to the trust store. It does not ship with the app. Another 8110 needs
the same fix, or a caching proxy for
api.met.noon a host whose certificate the phone trusts. MET recommends a proxy for apps anyway. - The signatures are an integrity check under an empty password, not a lock against the owner. Get them wrong and NSS just ignores the trust (or, at worst, the store fails to load until restored from the backup).
- The same applies to any other site using a CA newer than 2016 that is missing from the phone. The Let's Encrypt roots (ISRG X1/X2) were already in this GerdaOS phone's database, stored the same way (legacy trust record plus signatures, added before this project), which is why those sites work.
- The rain radar covers the Nordic countries only. Elsewhere MET answers 422 and the block is hidden; Kuling remembers that for a day instead of asking again.
- A place's UTC offset is refreshed every 12 hours. Within the 9-day outlook, times after a daylight-saving switch in that place are off by an hour until the next refresh. Gecko 48 has no time zone database for arbitrary zones, so the offset comes from Open-Meteo.
- Nokia 8110 4G (or another KaiOS 2.5 phone) with root adb
- The HARICA RootCA 2015 trusted on the phone (see Certificates)
adb, Python 3, Node 18+ (tests only),zip
npm test # unit tests: app (node, real MET/Kartverket fixtures) + cert signing (python)
npm run build # -> dist/application.zip
npm run install-phone # push, register, grant permissions, restart the phone UI
npm run uninstall-phoneinstall.py reads the app id and type from app/manifest.webapp. It backs up webapps.json and
permissions.sqlite on the phone, writes the permission rows itself (hand-registered apps don't
get them automatically), and restarts b2g. The phone UI reloads in about 20-30 seconds.
Permissions:
systemXHR(privileged): requests without CORS limits, and with a real User-Agentgeolocation: My location
app/
manifest.webapp
index.html, style/app.css
js/symbols.js symbol id -> description (generated from metno/weathericons legend.csv)
js/forecast.js pure: parse MET data, hour/day rows, nowcast text
js/places.js pure: saved places, Kartverket results
js/data.js network + cache (Expires / If-Modified-Since)
js/views.js rendering
js/main.js state, screens, keys
weather/ MET weather symbols (SVG)
icons/ app icons, generated by scripts/make_icons.py
scripts/ build, icons, install, uninstall
scripts/certs/ add_root.py + nsssig.py: trust a new root CA on the phone (see Certificates)
tests/ node --test, with fixtures
All app code is ES5, because the phone runs Gecko 48.
MIT for the code. Weather symbols are MIT © Yr (see app/weather/LICENSE).