diff --git a/.github/workflows/cross-bootstrap-tools.yml b/.github/workflows/cross-bootstrap-tools.yml index 63f94aca4b4..fdb1cc9ead8 100644 --- a/.github/workflows/cross-bootstrap-tools.yml +++ b/.github/workflows/cross-bootstrap-tools.yml @@ -17,8 +17,8 @@ jobs: strategy: fail-fast: false matrix: - target_arch: [ amd64, aarch64 ] - os: [ ubuntu-22.04, ubuntu-24.04, macos-latest ] + target_arch: [ amd64 ] + os: [ ubuntu-24.04, macos-latest ] include: # TODO: both Ubuntu and macOS have bmake packages, we should try them instead of bootstrapping our own copy. - os: ubuntu-24.04 diff --git a/libexec/rc/rc.d/firstboot b/libexec/rc/rc.d/firstboot index 1aacd9e9835..694db0ce68f 100644 --- a/libexec/rc/rc.d/firstboot +++ b/libexec/rc/rc.d/firstboot @@ -30,9 +30,25 @@ firstboot_start() read mportmirror /usr/sbin/mport config set mirror_region ${mportmirror} + # age verifcation is required in some locales, so we ask for it here to ensure the best experience for users in those areas. + echo "Jurisdictions with required age verification include California, Colorado, Illinois, and Brazil. Parental controls will be enabled for all users and can be configured with the agectl utility." + echo "If you do not live in a region with required age verification, you can disable this feature and the associated parental controls." + echo -e "\r${red}MidnightBSD does not support ID checks or AI facial scanning for age verification. You may NOT use MidnightBSD in regions requiring it.${lsuffix}" + echo -e "\r${yellow}Do you live in a region with required age verification or want parental controls enabled? (yes or no)${lsuffix}" + read ageverify + if [ "${ageverify}" = "no" ]; then + /usr/sbin/sysrc aged_enable=NO + else + echo "parental will enable parental controls based on age attestion for apps in the MidnightBSD package manger." + echo -e "\r${yellow}Specify one of the following regions for age attestion (US-CA, US-CO, US-IL, BR, parental)${lsuffix}" + read ageregion + /usr/sbin/agectl -r ${ageregion} + echo "You may change the setting later with the agectl -r command." + fi + echo -e "\r${yellow}Would you like to report your install via bsdstats? (yes or no)${lsuffix}" read installbstat - if [ ${installbstat} = "yes" ]; then + if [ "${installbstat}" = "yes" ]; then if [ ! -f /usr/local/etc/rc.d/bsdstats.sh ]; then /sbin/ipfw disable firewall /usr/sbin/mport install bsdstats diff --git a/usr.sbin/agectl/agectl.8 b/usr.sbin/agectl/agectl.8 index 741ec1545eb..88f57a988e8 100644 --- a/usr.sbin/agectl/agectl.8 +++ b/usr.sbin/agectl/agectl.8 @@ -33,18 +33,20 @@ .Nm .Op Fl a Ar age | Fl b Ar YYYY-MM-DD .Op Ar username +.Nm +.Op Fl r Ar region .Sh DESCRIPTION The .Nm utility communicates with the .Xr aged 8 -daemon to retrieve or set age-related information for users. +daemon to retrieve or set age-related information for users. .Pp When invoked without arguments, .Nm -queries the daemon for the age range associated with the calling user's -effective UID. The daemon verifies the caller's identity using -.Xr getpeereid 2 , +queries the daemon for the age range associated with the calling user's +effective UID. The daemon verifies the caller's identity using +.Xr getpeereid 2 , ensuring users can only access their own data. .Pp The following options are available: @@ -57,6 +59,31 @@ This operation requires root privileges. Set the date of birth for the specified .Ar username . This operation requires root privileges. +.It Fl r Ar region +Set the regulatory region for age verification. This affects how +.Xr aged 8 +handles age verification for all users. This operation requires root privileges. +Valid regions include +.Sy US-AL , +.Sy US-CA , +.Sy US-CO , +.Sy US-IL , +.Sy BR , +.Sy US-NY , +.Sy US-MI , +.Sy US-WA , +.Sy US-LA , +.Sy US-UT , +.Sy US-TX , +.Sy US-FL , +.Sy DE , +.Sy EU , +.Sy UK , +.Sy AU , +.Sy JP , +.Sy null , +and +.Sy parental . .El .Sh EXIT STATUS .Ex -std @@ -72,8 +99,12 @@ Set the age for user "bob" to 32 (as root): Set the date of birth for user "tom" (as root): .Pp .Dl # agectl -b 1995-12-22 tom +.Pp +Set the region to US-CA (as root): +.Pp +.Dl # agectl -r US-CA .Sh PROTOCOL RESPONSES -The output of a query is a comma-delimited string representing the +The output of a query is a comma-delimited string representing the following age brackets: .Bl -tag -width "16,17XX" -compact .It Sy "0,12" @@ -86,6 +117,8 @@ Under 13 years old. 18 years or older. .It Sy "-1,-1" User data not found or age undefined. +.It Sy "-2,-2" +Age verification is not permitted in the configured region. .El .Sh SEE ALSO .Xr aged 8 , diff --git a/usr.sbin/agectl/agectl.c b/usr.sbin/agectl/agectl.c index 74ea26b1dff..50fd76606f5 100644 --- a/usr.sbin/agectl/agectl.c +++ b/usr.sbin/agectl/agectl.c @@ -57,6 +57,7 @@ usage(const char *progname) fprintf(stderr, " Query: %s\n", progname); fprintf(stderr, " Set Age (Root): %s -a \n", progname); fprintf(stderr, " Set DOB (Root): %s -b \n", progname); + fprintf(stderr, " Set Region (Root): %s -r \n", progname); exit(1); } @@ -69,10 +70,10 @@ main(int argc, char *argv[]) int ch; char *set_val = NULL; char *target_user = NULL; - int mode = 0; /* 0 = query, 1 = set age, 2 = set dob */ + int mode = 0; /* 0 = query, 1 = set age, 2 = set dob, 3 = set region */ int update_failed = 0; - while ((ch = getopt(argc, argv, "a:b:")) != -1) { + while ((ch = getopt(argc, argv, "a:b:r:")) != -1) { switch (ch) { case 'a': if (!valid_age(optarg)) @@ -86,15 +87,22 @@ main(int argc, char *argv[]) mode = 2; set_val = optarg; break; + case 'r': + mode = 3; + set_val = optarg; + break; default: usage(argv[0]); } } - if (mode > 0) { + if (mode > 0 && mode < 3) { if (optind >= argc) usage(argv[0]); target_user = argv[optind]; + } else if (mode == 3) { + if (optind < argc) + usage(argv[0]); } if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { @@ -113,6 +121,9 @@ main(int argc, char *argv[]) if (mode == 0) { write(fd, "GET", 3); + } else if (mode == 3) { + snprintf(buf, sizeof(buf), "REG %s", set_val); + write(fd, buf, strlen(buf)); } else { struct passwd *pw = getpwnam(target_user); diff --git a/usr.sbin/aged/aged.8 b/usr.sbin/aged/aged.8 index 22ede038c32..1dac10441e5 100644 --- a/usr.sbin/aged/aged.8 +++ b/usr.sbin/aged/aged.8 @@ -34,8 +34,8 @@ .Sh DESCRIPTION The .Nm -daemon manages a database of User IDs (UIDs) and their corresponding ages or -dates of birth. It provides a secure interface via a Unix Domain Socket +daemon manages a database of User IDs (UIDs) and their corresponding ages or +dates of birth. It provides a secure interface via a Unix Domain Socket to allow applications to query the age range of the currently connected user. .Pp This is required in some jurisdictions for age verification. For instance, @@ -55,14 +55,14 @@ Only the .Pa root user (UID 0) is permitted to write or update records in the database. .It -Non-root users may only query their own age range. Any attempt to query the -age of another UID is blocked by the daemon's logic, as it ignores the +Non-root users may only query their own age range. Any attempt to query the +age of another UID is blocked by the daemon's logic, as it ignores the request payload and retrieves data based solely on the caller's verified UID. .El .Sh PROTOCOL The daemon listens on a Unix Domain Socket at .Pa /var/run/aged/aged.sock . -The socket is created with 666 permissions to allow all system users to +The socket is created with 666 permissions to allow all system users to initiate a connection. .Ss Writing Data (Root Only) To store or update a user's information, send a string in the following format: @@ -75,9 +75,48 @@ is either .Ic age or .Ic dob . +.Ss Setting Region (Root Only) +To set the regulatory region, send a string in the following format: +.Pp +.Dl "REG " +.Pp +Where +.Ar region +is one of the following valid values: +.Sy US-AL , +.Sy US-CA , +.Sy US-CO , +.Sy US-IL , +.Sy BR , +.Sy US-NY , +.Sy US-MI , +.Sy US-WA , +.Sy US-LA , +.Sy US-UT , +.Sy US-TX , +.Sy US-FL , +.Sy DE , +.Sy EU , +.Sy UK , +.Sy AU , +.Sy JP , +.Sy null , +or +.Sy parental . +.Pp +If the region is set to +.Sy US-CA , +.Sy US-CO , +.Sy US-IL , +.Sy BR , +or +.Sy parental , +GET operations will return an age range. Any other value will cause GET +operations to return +.Sy -2,-2 . .Ss Querying Data (All Users) -Any data sent to the socket by a non-root user triggers a lookup for that -user's specific UID. The daemon returns a comma-delimited string representing +Any data sent to the socket by a non-root user triggers a lookup for that +user's specific UID. The daemon returns a comma-delimited string representing the age range: .Bl -tag -width "16,17XX" -compact .It Sy "0,12" @@ -90,6 +129,8 @@ At least 16 and under 18. 18 years of age or older. .It Sy "-1,-1" Age is undefined or user not found. +.It Sy "-2,-2" +Age verification is not permitted in the configured region. .El .Sh FILES .Bl -tag -width "/var/run/aged/aged.sockXX" -compact diff --git a/usr.sbin/aged/aged.c b/usr.sbin/aged/aged.c index 35e88c5bcd8..68474bfff3c 100644 --- a/usr.sbin/aged/aged.c +++ b/usr.sbin/aged/aged.c @@ -44,10 +44,15 @@ #define SOCKET_PATH "/var/run/aged/aged.sock" #define DB_PATH "/var/db/aged/aged.db" #define RUN_USER "aged" +#define MAX_REGION_LEN 10 static int calculate_age(const char *); static void get_range(int, char *); static void init_db(void); +static int is_valid_region(const char *); +static void load_region_setting(sqlite3 *); + +static char *current_region = NULL; int main(void) @@ -159,6 +164,8 @@ main(void) close(client_fd); continue; } + + load_region_setting(db); if (client_uid == 0 && strncmp(buf, "SET ", 4) == 0) { char *p = buf + 4; @@ -216,22 +223,63 @@ main(void) syslog(LOG_INFO, "User information updated for uid %d", target_uid); write(client_fd, "OK\n", 3); } + } else if (client_uid == 0 && strncmp(buf, "REG ", 4) == 0) { + char *region = buf + 4; + char *newline = strchr(region, '\n'); + if (newline) *newline = '\0'; + + if (is_valid_region(region)) { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, "UPDATE settings SET value = ? WHERE key = 'region';", -1, &stmt, 0); + if (strcmp(region, "null") == 0) { + sqlite3_bind_null(stmt, 1); + } else { + sqlite3_bind_text(stmt, 1, region, -1, SQLITE_STATIC); + } + sqlite3_step(stmt); + sqlite3_finalize(stmt); + syslog(LOG_INFO, "Region updated to %s", region); + write(client_fd, "OK\n", 3); + } else { + syslog(LOG_WARNING, "Invalid region value: %s", region); + write(client_fd, "ERR\n", 4); + } } else { - sqlite3_stmt *stmt; - sqlite3_prepare_v2(db, "SELECT age FROM users WHERE uid = ?;", -1, &stmt, 0); - sqlite3_bind_int(stmt, 1, (int)client_uid); - int age = (sqlite3_step(stmt) == SQLITE_ROW) ? sqlite3_column_int(stmt, 0) : -1; - sqlite3_finalize(stmt); - - /* user accounts start at 1000 */ - if (client_uid < 1000 && age == -1) { - age = 18; /* assume 18+ for root and service accounts if undefined. With root, kids could circumvent any protections anyway. */ + int region_allowed = 0; + if (current_region != NULL) { + /* + These are places that age attenstion is required or will be. + It does not include locales with required ID checks since we don't provide that level of verification. + Not legal advice, best effort. + */ + if (strncmp(current_region, "US-CA", MAX_REGION_LEN) == 0 || + strncmp(current_region, "US-CO", MAX_REGION_LEN) == 0 || + strncmp(current_region, "US-IL", MAX_REGION_LEN) == 0 || + strncmp(current_region, "BR", MAX_REGION_LEN) == 0 || // Doing nothing appears worse in brazil legally so we keep it on. + strncmp(current_region, "parental", MAX_REGION_LEN) == 0) { + region_allowed = 1; + } } - char response[16]; + if (!region_allowed) { + write(client_fd, "-2,-2\n", 6); + } else { + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, "SELECT age FROM users WHERE uid = ?;", -1, &stmt, 0); + sqlite3_bind_int(stmt, 1, (int)client_uid); + int age = (sqlite3_step(stmt) == SQLITE_ROW) ? sqlite3_column_int(stmt, 0) : -1; + sqlite3_finalize(stmt); + + /* user accounts start at 1000 */ + if (client_uid < 1000 && age == -1) { + age = 18; /* assume 18+ for root and service accounts if undefined. With root, kids could circumvent any protections anyway. */ + } - get_range(age, response); - write(client_fd, response, strlen(response)); + char response[16]; + + get_range(age, response); + write(client_fd, response, strlen(response)); + } } sqlite3_close(db); } @@ -263,6 +311,54 @@ calculate_age(const char *dob_str) return age; } +static int +is_valid_region(const char *region) { + /* places that require age verification or will be. Not all can be supported */ + const char *valid_regions[] = { + "US-AL", "US-CA", "US-CO", "US-IL", "BR", "US-NY", "US-MI", "US-WA", + "US-LA", "US-UT", "US-TX", "US-FL", "DE", "EU", "UK", "AU", + "JP", "null", "parental", NULL + }; + + if (region == NULL) return 0; + + for (int i = 0; valid_regions[i] != NULL; i++) { + if (strncmp(region, valid_regions[i], MAX_REGION_LEN) == 0) { + return 1; + } + } + return 0; +} + +static void +load_region_setting(sqlite3 *db) +{ + sqlite3_stmt *stmt; + const char *sql = "SELECT value FROM settings WHERE key = 'region';"; + + if (current_region) { + free(current_region); + current_region = NULL; + } + + int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "Failed to prepare statement: %s", sqlite3_errmsg(db)); + return; + } + + if (sqlite3_step(stmt) == SQLITE_ROW) { + const unsigned char *value = sqlite3_column_text(stmt, 0); + if (value) { + current_region = strdup((const char *)value); + } + } + + sqlite3_finalize(stmt); + stmt = NULL; +} + + void init_db(void) { @@ -285,7 +381,25 @@ init_db(void) syslog(LOG_ERR, "SQL error: %s", err_msg); sqlite3_free(err_msg); } + + const char *sql_settings = "CREATE TABLE IF NOT EXISTS settings(key TEXT NOT NULL PRIMARY KEY, value TEXT);"; + rc = sqlite3_exec(db, sql_settings, 0, 0, &err_msg); + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "SQL error: %s", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + exit(1); + } + + const char *sql_insert_region = "INSERT OR IGNORE INTO settings (key, value) VALUES ('region', NULL);"; + rc = sqlite3_exec(db, sql_insert_region, 0, 0, &err_msg); + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "SQL error: %s", err_msg); + sqlite3_free(err_msg); + } + sqlite3_close(db); + db = NULL; } void