How to configure serial vault

Hi all,
I am configuring a Serial Vault instance to integrate it with our Private Store.
From IRC conversations with @ondra and @pedronis I understood that the Serial-Vault need to use the same private key used to signing model assertion dropped in a device - i.e the same of the user signing the image burned on the device.
I found that:

  1. snap create key nicolino creates a gpg home in ~/.snap/gpg
  2. I can exporting the key in armored format, public and private part, by name with command

gpg --export-secret-key -u nicolino --homedir ~/.snap/gnupg/ -a # secret key exporting

gpg --export -u nicolino --homedir ~/.snap/gnupg/ -a # public key exporting

Question: what format i need to use for exporting the private and pubblic part of the key?

On the Serial Vault frontend i found the resource http://localhost:8081/models/keypairs/new to upload the signing keys.

Question:
Is it the right place to import the signing keys? If yes, which part of keypair or both part I need to upload - I would say both part of key?

Furthermore, in the settings.yaml I don’t know the role configuration key:

# Valid API keys
apiKeys:

What is its role and requirement on value of this key?

Thanks in advance.
Nicolino

PS : for personal use I write a plantuml diagram for the first boot registration of a device and the check for an authorized device from a store: i built it from information collected so far. If it is useful I would like to share it: how can i share it and permit to upgrade it?

no, it just need to be a key registered by the same brand account

Here’s the process for creating the keys and registering them with the store. The key for signing serial assertions must be passwordless i.e. when prompted for the password just press enter twice. The names of the keys are arbitrary.

# Create the named keys
$ snap create-key brand
$ snap create-key model
$ snap create-key serial     # must be a passwordless key

# List the keys
$ snap keys

# Login to the Ubuntu Store using your vendor SSO Account credentials
$ snapcraft login
Enter your Ubuntu One SSO credentials.
Email: <enter the vendor SSO account email>
Password: <enter the vendor SSO account password>

Login successful.

# Register the keys with the store
snapcraft register-key brand
snapcraft register-key model
snapcraft register-key serial

Once you have created and registered the keys with the store you need to export the ascii-armored private-key for the serial assertions:

$ snap keys
Name     SHA3-384
brand    zzFZWPIapWBVPy8...MY6fvc4xqz5
model    48Vyy2swHzZKnO4...iSo2VfbWtdM
serial   TGfLv_PBYcnUZ81...VjhcQIWweSjR

# Export the serial key to a serial.asc file
$ gpg --homedir ~/.snap/gnupg --armor --export-secret-key serial > serial.asc

You can then upload the serial.asc file to the Serial Vault (you have the correct link). The “Signing Authority” must match the account-id that you have in the Store and the authority-id model assertion. You should be able to check that value on this link:
https://myapps.developer.ubuntu.com/dev/account/

I think that the above answers your first two questions.

For the “apiKeys”, that just adds some security to the Serial Vault API. You can generate a value and enter it there, but then that value will need to be provided in the Gadget Snap prepare-device hook (along with the URL to the Serial Vault):

#!/bin/sh

set -x

exec >> $SNAP_COMMON/prepare-device-hook.log 2>&1

# This will need hardware-observe interface
product_serial=$(cat /sys/devices/virtual/dmi/id/product_serial)

SERIAL_FILE=$SNAP_COMMON/test
echo "product_serial:[$product_serial]" > "$SERIAL_FILE"

if [ ! "$product_serial" ]; then
    echo "Empty product_serial"
    exit 1
fi

# Set the serial number of the device (needed for the serial assertion)
snapctl set registration.proposed-serial="'${product_serial}'"

# Set the location of the Serial Vault and the API key
snapctl set device-service.url="https://serial.example.com/v1/"
snapctl set device-service.headers='{"api-key": "ItsyBitsySpider07"}'

There’s more information in the docs: https://docs.ubuntu.com/core/en/guides/build-device/config-hooks

1 Like

HI @jamesj,
Thanks for the reply.
I followed your instructions for the question (1) and (2) : I’m using the docker compose environment for the first attempt.
I call the Serial Vault web interfare at the url http://localhost:8081/models/keypairs/new.

The attempt is not successful and I have the following log messages:

web_1  | 2017/05/23 19:40:23 Database opened successfully.
web_1  | 2017/05/23 19:40:23 Database opened successfully.




web_1  | 2017/05/23 19:40:55 GET	/v1/token	86.94µs
web_1  | 2017/05/23 19:40:55 POST	/v1/keypairs	33.184µs
db_1   | ERROR:  relation "settings" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update settings set code=$1, data=$2
db_1   | 			where code=$1
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into settings (code,data)
db_1   | 		select $1, $2
db_1   | 		where not exists (select * from upsert)
db_1   |
web_1  | 2017/05/23 19:40:55 Error updating the database setting: pq: relation "settings" does not exist
db_1   | ERROR:  relation "keypair" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update keypair set authority_id=$1, key_id=$2, sealed_key=$3, assertion=$4
db_1   | 			where authority_id=$1 and key_id=$2
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into keypair (authority_id,key_id,sealed_key,assertion)
db_1   | 		select $1, $2, $3, $4
db_1   | 		where not exists (select * from upsert)
db_1   |
web_1  | 2017/05/23 19:40:55 Error updating the database keypair: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:40:58 GET	/v1/token	1.665µs
web_1  | 2017/05/23 19:40:58 POST	/v1/keypairs	1.556µs
web_1  | 2017/05/23 19:40:58 Error updating the database setting: pq: relation "settings" does not exist
web_1  | 2017/05/23 19:40:58 Error updating the database keypair: pq: relation "keypair" does not exist
db_1   | ERROR:  relation "settings" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update settings set code=$1, data=$2
db_1   | 			where code=$1
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into settings (code,data)
db_1   | 		select $1, $2
db_1   | 		where not exists (select * from upsert)
db_1   |
db_1   | ERROR:  relation "keypair" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update keypair set authority_id=$1, key_id=$2, sealed_key=$3, assertion=$4
db_1   | 			where authority_id=$1 and key_id=$2
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into keypair (authority_id,key_id,sealed_key,assertion)
db_1   | 		select $1, $2, $3, $4
db_1   | 		where not exists (select * from upsert)
db_1   |
web_1  | 2017/05/23 19:41:00 GET	/models/keypairs/new	36.321µs
web_1  | 2017/05/23 19:41:00 GET	/v1/version	4.251µs
web_1  | 2017/05/23 19:41:25 GET	/v1/token	1.456µs
web_1  | 2017/05/23 19:41:25 POST	/v1/keypairs	1.364µs
web_1  | 2017/05/23 19:41:25 Error updating the database setting: pq: relation "settings" does not exist
db_1   | ERROR:  relation "settings" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update settings set code=$1, data=$2
db_1   | 			where code=$1
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into settings (code,data)
db_1   | 		select $1, $2
db_1   | 		where not exists (select * from upsert)
db_1   |
db_1   | ERROR:  relation "keypair" does not exist at character 29
db_1   | STATEMENT:
db_1   | 		WITH upsert AS (
db_1   | 			update keypair set authority_id=$1, key_id=$2, sealed_key=$3, assertion=$4
db_1   | 			where authority_id=$1 and key_id=$2
db_1   | 			RETURNING *
db_1   | 		)
db_1   | 		insert into keypair (authority_id,key_id,sealed_key,assertion)
db_1   | 		select $1, $2, $3, $4
db_1   | 		where not exists (select * from upsert)
db_1   |
web_1  | 2017/05/23 19:41:25 Error updating the database keypair: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:47:12 GET	/models	6.891µs
db_1   | ERROR:  relation "keypair" does not exist at character 57
db_1   | STATEMENT:  select id, authority_id, key_id, active, assertion from keypair order by authority_id, key_id
web_1  | 2017/05/23 19:47:13 GET	/v1/keypairs	1.333µs
web_1  | 2017/05/23 19:47:13 Error retrieving database keypairs: pq: relation "keypair" does not exist
db_1   | ERROR:  relation "model" does not exist at character 155
db_1   | STATEMENT:
db_1   | 		select m.id, brand_id, name, keypair_id, k.authority_id, k.key_id, k.active, user_keypair_id, ku.authority_id, ku.key_id, ku.active, ku.assertion
db_1   | 		from model m
db_1   | 		inner join keypair k on k.id = m.keypair_id
db_1   | 		inner join keypair ku on ku.id = m.user_keypair_id
db_1   | 		order by name
db_1   |
web_1  | 2017/05/23 19:47:13 GET	/v1/models	2.395µs
web_1  | 2017/05/23 19:47:13 GET	/v1/version	5.723µs
web_1  | 2017/05/23 19:47:13 Error retrieving database models: pq: relation "model" does not exist
web_1  | 2017/05/23 19:47:17 GET	/models/new	1.467µs
web_1  | 2017/05/23 19:47:17 GET	/v1/keypairs	2.925µs
db_1   | ERROR:  relation "keypair" does not exist at character 57
db_1   | STATEMENT:  select id, authority_id, key_id, active, assertion from keypair order by authority_id, key_id
web_1  | 2017/05/23 19:47:17 GET	/v1/version	1.655µs
web_1  | 2017/05/23 19:47:17 Error retrieving database keypairs: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:47:43 GET	/accounts	1.303µs
web_1  | 2017/05/23 19:47:43 GET	/v1/accounts	1.718µs
db_1   | ERROR:  relation "account" does not exist at character 41
db_1   | STATEMENT:  select id, authority_id, assertion from account order by authority_id
web_1  | 2017/05/23 19:47:43 Error retrieving database accounts: pq: relation "account" does not exist
db_1   | ERROR:  relation "model" does not exist at character 155
web_1  | 2017/05/23 19:47:43 GET	/v1/models	1.825µs
web_1  | 2017/05/23 19:47:43 Error retrieving database models: pq: relation "model" does not exist
db_1   | STATEMENT:
db_1   | 		select m.id, brand_id, name, keypair_id, k.authority_id, k.key_id, k.active, user_keypair_id, ku.authority_id, ku.key_id, ku.active, ku.assertion
db_1   | 		from model m
db_1   | 		inner join keypair k on k.id = m.keypair_id
db_1   | 		inner join keypair ku on ku.id = m.user_keypair_id
db_1   | 		order by name
db_1   |
web_1  | 2017/05/23 19:47:43 GET	/v1/keypairs	1.893µs
web_1  | 2017/05/23 19:47:43 Error retrieving database keypairs: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:47:43 GET	/v1/version	2.329µs
db_1   | ERROR:  relation "keypair" does not exist at character 57
db_1   | STATEMENT:  select id, authority_id, key_id, active, assertion from keypair order by authority_id, key_id
web_1  | 2017/05/23 19:47:45 GET	/models	76.099µs
db_1   | ERROR:  relation "model" does not exist at character 155
db_1   | STATEMENT:
db_1   | 		select m.id, brand_id, name, keypair_id, k.authority_id, k.key_id, k.active, user_keypair_id, ku.authority_id, ku.key_id, ku.active, ku.assertion
db_1   | 		from model m
db_1   | 		inner join keypair k on k.id = m.keypair_id
db_1   | 		inner join keypair ku on ku.id = m.user_keypair_id
db_1   | 		order by name
db_1   |
db_1   | ERROR:  relation "keypair" does not exist at character 57
db_1   | STATEMENT:  select id, authority_id, key_id, active, assertion from keypair order by authority_id, key_id
web_1  | 2017/05/23 19:47:46 GET	/v1/models	1.496µs
web_1  | 2017/05/23 19:47:46 Error retrieving database models: pq: relation "model" does not exist
web_1  | 2017/05/23 19:47:46 GET	/v1/keypairs	77.612µs
web_1  | 2017/05/23 19:47:46 Error retrieving database keypairs: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:47:46 GET	/v1/version	98.627µs
web_1  | 2017/05/23 19:47:49 GET	/	37.437µs
web_1  | 2017/05/23 19:47:49 GET	/v1/version	1.769µs
web_1  | 2017/05/23 19:47:50 GET	/models	1.533µs
db_1   | ERROR:  relation "model" does not exist at character 155
db_1   | STATEMENT:
db_1   | 		select m.id, brand_id, name, keypair_id, k.authority_id, k.key_id, k.active, user_keypair_id, ku.authority_id, ku.key_id, ku.active, ku.assertion
db_1   | 		from model m
db_1   | 		inner join keypair k on k.id = m.keypair_id
db_1   | 		inner join keypair ku on ku.id = m.user_keypair_id
db_1   | 		order by name
db_1   |
db_1   | ERROR:  relation "keypair" does not exist at character 57
db_1   | STATEMENT:  select id, authority_id, key_id, active, assertion from keypair order by authority_id, key_id
web_1  | 2017/05/23 19:47:50 GET	/v1/models	1.634µs
web_1  | 2017/05/23 19:47:50 Error retrieving database models: pq: relation "model" does not exist
web_1  | 2017/05/23 19:47:50 GET	/v1/keypairs	2.28µs
web_1  | 2017/05/23 19:47:50 Error retrieving database keypairs: pq: relation "keypair" does not exist
web_1  | 2017/05/23 19:47:50 GET	/v1/version	2.26µs

Could I have missed something ?

Cheers,
Nicolino

Hey @NCuralli,

It looks as though the database tables have not been created. The docker compose environment was used by one of our internal teams early in the project (for testing), so it probably is missing the step that creates/updates the database schema.

You can run this manually via:

go run tools/createdb.go -config=/path/to/config/file

For our deployments, use and recommend the Juju bundle (details in the Deployment.md doc). That installs the Serial Vault from a snap and the snap automatically runs then database update step on service restart. You can see that in bin/snap-run-service.

Hi @jamesj,
I try to use the juju bundle, but it don’t seems to work.
I hit a problem with the add-relation phase of the deploy.
I start from a situation as following:

ubuntu@ubuntu-xenial:/var/lib$ juju status
Model    Controller  Cloud/Region         Version
default  lxd-test    localhost/localhost  2.1.3

App           Version  Status       Scale  Charm         Store       Rev  OS      Notes
postgresql    9.5.6    active           1  postgresql    jujucharms  148  ubuntu
serial-vault           maintenance      1  serial-vault  jujucharms   10  ubuntu

Unit             Workload     Agent  Machine  Public address  Ports     Message
postgresql/1*    active       idle   5        10.71.192.16    5432/tcp  Live master (9.5.6)
serial-vault/1*  maintenance  idle   6        10.71.192.37    8080/tcp  Waiting for database

Machine  State    DNS           Inst id        Series  AZ
5        started  10.71.192.16  juju-a26a2b-5  xenial
6        started  10.71.192.37  juju-a26a2b-6  xenial

Relation     Provides    Consumes    Type
replication  postgresql  postgresql  peer

After running the add-relation command juju add-relation serial-vault:database postgresql:db-admin I obtain the following status:

ubuntu@ubuntu-xenial:/var/lib$ juju status
Model    Controller  Cloud/Region         Version
default  lxd-test    localhost/localhost  2.1.3

App           Version  Status  Scale  Charm         Store       Rev  OS      Notes
postgresql    9.5.6    active      1  postgresql    jujucharms  148  ubuntu
serial-vault           error       1  serial-vault  jujucharms   10  ubuntu

Unit             Workload  Agent  Machine  Public address  Ports     Message
postgresql/1*    active    idle   5        10.71.192.16    5432/tcp  Live master (9.5.6)
serial-vault/1*  error     idle   6        10.71.192.37    8080/tcp  hook failed: "database-relation-changed" for postgresql:db-admin

Machine  State    DNS           Inst id        Series  AZ
5        started  10.71.192.16  juju-a26a2b-5  xenial
6        started  10.71.192.37  juju-a26a2b-6  xenial

Relation     Provides    Consumes      Type
replication  postgresql  postgresql    peer
database     postgresql  serial-vault  regular

then something fails in serial-vault service configuration.
Indeed in the log of juju I found this:

......
unit-postgresql-1: 13:37:50 INFO juju.worker.uniter.operation ran "db-admin-relation-joined" hook
unit-postgresql-1: 13:37:50 INFO juju.worker.uniter no operations in progress; waiting for changes
unit-serial-vault-1: 13:37:50 INFO unit.serial-vault/1.juju-log database:9: Reactive main running for hook database-relation-changed
unit-postgresql-1: 13:37:51 INFO unit.postgresql/1.juju-log db-admin:9: Reactive main running for hook db-admin-relation-changed
unit-serial-vault-1: 13:37:51 INFO unit.serial-vault/1.juju-log database:9: Invoking reactive handler: reactive/serial-vault.py:82:db_relation_changed
unit-postgresql-1: 13:37:51 DEBUG unit.postgresql/1.juju-log db-admin:9: Coordinator: Using charms.coordinator.SimpleCoordinator coordinator
unit-postgresql-1: 13:37:51 INFO unit.postgresql/1.juju-log db-admin:9: Initializing Apt Layer
unit-postgresql-1: 13:37:51 INFO unit.postgresql/1.juju-log db-admin:9: Initializing Snap Layer
unit-postgresql-1: 13:37:51 INFO unit.postgresql/1.db-admin-relation-changed lxc
unit-postgresql-1: 13:37:51 INFO unit.postgresql/1.juju-log db-admin:9: Initializing Leadership Layer (is leader)
unit-postgresql-1: 13:37:52 INFO unit.postgresql/1.juju-log db-admin:9: preflight handler: reactive/workloadstatus.py:57:initialize_workloadstatus_state
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.juju-log database:9: Making dir  root:root 755
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.juju-log database:9: Writing file settings.yaml root:root 444
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed sudo: /snap/bin/serial-vault.config: command not found
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed Traceback (most recent call last):
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/var/lib/juju/agents/unit-serial-vault-1/charm/hooks/database-relation-changed", line 19, in <module>
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     main()
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/local/lib/python3.5/dist-packages/charms/reactive/__init__.py", line 78, in main
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     bus.dispatch()
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/local/lib/python3.5/dist-packages/charms/reactive/bus.py", line 415, in dispatch
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     _invoke(hook_handlers)
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/local/lib/python3.5/dist-packages/charms/reactive/bus.py", line 406, in _invoke
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     handler.invoke()
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/local/lib/python3.5/dist-packages/charms/reactive/bus.py", line 280, in invoke
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     self._action(*args)
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/var/lib/juju/agents/unit-serial-vault-1/charm/reactive/serial-vault.py", line 84, in db_relation_changed
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     configure_service()
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/var/lib/juju/agents/unit-serial-vault-1/charm/reactive/serial-vault.py", line 136, in configure_service
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     update_config(database)
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/var/lib/juju/agents/unit-serial-vault-1/charm/reactive/serial-vault.py", line 145, in update_config
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     'cat settings.yaml | sudo /snap/bin/serial-vault.config', shell=True)
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/lib/python3.5/subprocess.py", line 626, in check_output
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     **kwargs).stdout
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed   File "/usr/lib/python3.5/subprocess.py", line 708, in run
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed     output=stdout, stderr=stderr)
unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed subprocess.CalledProcessError: Command 'cat settings.yaml | sudo /snap/bin/serial-vault.config' returned non-zero exit status 1
unit-postgresql-1: 13:37:52 DEBUG unit.postgresql/1.juju-log db-admin:9: Coordinator: Loading state
unit-serial-vault-1: 13:37:52 ERROR juju.worker.uniter.operation hook "database-relation-changed" failed: exit status 1
unit-serial-vault-1: 13:37:52 INFO juju.worker.uniter awaiting error resolution for "relation-changed" hook

.......

Please note the row :

unit-serial-vault-1: 13:37:52 INFO unit.serial-vault/1.database-relation-changed sudo: /snap/bin/serial-vault.config: command not found

Could you fix it?

Cheers,
Nicolino

PS:

the command

juju deploy serial-vault serial-vault # The signing service

present in the instructions, don’t works.
The working command seems to be:

juju deploy cs:~jamesj/serial-vault-10 serial-vault

Hi @NCuralli,

You can either deploy and connect the individual services (harder) or use the Juju bundle, which deploys a a number of connected units. It looks as though you’ve gone through the difficult route of deploying individual services.

To deploy the bundle, you should take a look at the Deployment instructions:
https://github.com/CanonicalLtd/serial-vault/blob/master/docs/Deployment.md

You’ll see there that to deploy the bundle, you need this command:

juju deploy cs:~jamesj/bundle/serial-vault-bundle

That’s a much simpler approach as it deploys the serial-vault services connected to a postgresql database with an haproxy front-end.

However, looking at your juju status output, the real cause of the issues is that you are deploying the serial-vault on a local lxd containers. There is a bug on lxd containers that prevent snaps from being installed on lxd containers, which is why you’re seeing:
/snap/bin/serial-vault.config: command not found.
Essentially, the serial-vault snap failed to install an apparmor restriction, I believe.

It is possible to get around this, but you need to look at a later, non-LTS version of Ubuntu.

Hi @NCuralli,

I’ve just been contacted by someone that said that you were installing the services on a private LAN. The recommended approach is to use the Juju bundle, but if you are running into that lxd issue there is another approach.

Within the serial-vault source there is a “snap bundle” that installs all the serial-vault services, along with an apache front-end, haproxy and postgresql. The snap is not as yet published in the store, but the instructions for building and installing the snap are documented:

https://github.com/CanonicalLtd/serial-vault/tree/master/snaps/serial-vault-services

Thanks

James

I didn’t notice this until today, but this seems to have the double quoting bug where serial will end up containg single quotes, unless I’m confused

If I remember correctly, this was a workaround with an issue in the way that snapd parsed the serial. Without the quotes there was an error because the parsing thought it was an integer and a serial number of ABC123 failed. There was a lot of experimentation at the time, but this was the only solution.

You are right in saying that the single quotes get retained - it’s not really ideal, but was the only solution at the time. I’m not sure if this has been fixed in snapd since that time.

that’s still the case (if the serial can be made of only numbers we need quoting), I think a possible approach is:

# Set the serial number of the device (needed for the serial assertion)
snapctl set registration.proposed-serial="\"${product_serial}\""

I believe that was tried at the time. When I wrote the document I tried a number of variations and the single-quote approach was the only one that worked. I think that the best solution is to fix this in snapd’s parser - I’m not sure if a bug was filed at the time.

It’s just shell quoting, unless product_serial was very odd, not sure why it wouldn’t work. Anyway I don’t think snapd parsing here will change afaiu, it behaves this way (expecting a JSON value) consistently across all “set” commands.