# Draft - PVS: Pantavisor (State Format) Signatures
## Implementation Status
**DONE**:
* Pantavisor runtime:
* parses `pvs@2` signatures state json
* validates pvs@s signatures if `secureboot.mode` is not `disabled`
* enforce valid signatures at install time - if `secureboot.mode` is `lenient` or `strict`
* enforce valid signature of json at run time of the system/container - if `secureboot.mode` is `lenient` or `strict`
* enforces that all elements of system state must be covered/explained by signatures either through include or exclude - if `secureboot.mode` is `strict`
* enforces that objects and json files on disk must not be modified compared to what was validated alongside the system state json
* RS256 JWS signatures
* PVR tool:
* Examples from this spec (see below) are supported
* `pvr sig <subcommand>` command is available in develop and latest release
* `pvr sig add` allows to add signatures to system state for parts or customly selected elements of system state
* `pvr sig update` allows to refresh signatures that might be outdated
* `pvr sig ls` allows to list files protectecd/excluded or not-covered by signatures in system state
* RS256 support; ES256 prototype available also
* `pvr deploy` supports deploying in canonical format so that integrity can be enforced (needed by mfgprofile mix and match developer tooling)
* `pvr checkout|clone` supports creating files in canonical format so that integrity can be enforced (needed by pantavisor bsp build system)
* Pantavisor SDK:
* Build now supports producing bsps/ and factory images using canonical on disk format so that it can be validated
* include trusted public key in pantavisor during build
* support adding trusted key as a BSP addon (this allows for adding the trusted key as a post processing step or by developers)
*INPROGRESS*:
* Finish support for ES256 PVR and Pantavisor
* Improve PVR tooling so that the embedded signatures themselves will be properly handled by operations such as pvr get/merge/export etc.
* Review all object and file usage cases to ensure tall are validated before they are installed/used.
*TODO*:
* Smoothen the developer iteration flow
* Define CI and Release flow and Signatures
* Discuss single validation key requirement at this stage (do we need trust db?)
* Improve error reporting in case of validation failure to give more details which signatures failed etc.
* Add Developer Key Management basics to pvr itself so we dont need to use openssl
## Intro
For secure boot scenarios Pantavisor offers the ability to protect runtime code and configs of apps and bsps with cryptographic signatures that get enforced at both, install and runtime.
Pantavisor system state format offers the option to embed [JWS JSON](https://datatracker.ietf.org/doc/html/rfc7515) formatted signatures in the state json like outlined below.
## Approach
A Pantavisor system state (see [System State Spec](https://docs.pantahub.com/pantavisor-state-format-v2/)) json can embed one or many PVS Signatures in the form of a flattened JWS JSON ([see RFC7515 - section 3.2](https://datatracker.ietf.org/doc/html/rfc7515#section-3.2)).
Each PVS Signature protects a subset of elements (e.g. the Signature Payload) of the Pantavisor state format.
PVS Signatures will not duplicate the payload content ([see RFC7515 - appendix F](https://datatracker.ietf.org/doc/html/rfc7515#appendix-F)) as it can always be reconstructed from the surrounding system state document in which the signature is embedded. To support such payload reconstruction from context, PVS Signatures come with a match rule inside the protected 'pvs' JWS header extension.
The `pvs` JWS header extension ([see RFC7515 - section 4.1.11](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11)) offers the following header options to express such match rule:
* `include` - a list of path globs of state.json elements to protect; default `$part/*`
* `exclude` - a list of path globs of state.json elements to exclude from those matched in include; default `<empty>`
A `pvs` signature is a state.json document with `"#spec" : "pvs@2"`. A state.json can contain multiple of such signatures that each indidivdually sign a subset of elements as expressed in the matchrule.
pvs signatures have a name and are by convention located grouped by the _sigs/ prefix.
## Showcase
To implement the case of protecting the runtime and config artifacts of an app (e.g. 'part') pvs signatures are expected to be embedded in system state format using a special crafted key named: '_sigs/$part.json'.
The PVS Signature is a standard JWS in JSON format as you can see here:
```
[ ... ]
"bsp/run.json": {
[ "..." ]
},
"_sigs/bsp.json": {
"#spec": "pvs@1",
"protected":"eyJhbGciOiJSUzI1NiIsImNyaXQiOlsicHZzIl0sInB2cyI6eyJpbmNsdWRlIjpbInJ1bi5qc29uIiwibHhjLmNvbnRhaW5lci5jb25mIl0sInNlbGVjdCI6Im5hbWUifSwidHlwIjoiUFZTIn0"
"signature":"OH6EQTBHL3QyYjZzzr9ZhMUB6SaZhfJ06kG2s6ZKmxM__ctiBSvRQ5tyHHlNBZNFXE1VjTpk6wBs7rhWAfTBmDlsxwJOZT4zApwZB2SfM16203B6Kz0Wc6dIhqpVfsq2ker0SqSvNLXh6BwMzg7IOsJ49U2WQRHBzbwl8GPtsD0x4OrYgfC6OZM6CBv_7gI7O0qZSozfHMwZ0hiOd6Y2pmGR3Eza7efXx7_EIarTlj53o_Nmg0Q-75TycIZRcZqStQKHbsGPkDUcgv-9N7VdJ073RXsCuZtX2oucs637cJ7eufamn1EKRjCdAEuUrGJe6XL7llo-zDNVIqIyDsu3042ZKbho08o7-qIlCf-VqwUTFda4nY5xAXUubWlhhLV0gRoP33yK6ezy2aYNNESg5SobtMrpQkjgeje6EGUp1ADqcBUCYYJcsfaSdijcy2mzbQwa6PREbzMBou4rs1PzReOHJ76F8-blC7mlvVDyHXFBDRuxkHxgCbBizglfykWwWN9jBiD-29zR1DhEdOdUCgqQEyyHlyJRT9oxHwjhC5ytGoZw-uLU3nAJeO4vb9CvhUgv0zHAawy-qDkMq6ZGwhnjbPmLrpuEGVdqBzM3cpKsqvKssZZQJ0cdKiLpyoRLQ5UnNalsvxD6CAET0XpUkHMPcMJa4ancsGQ0WqDI1-4"
}
...
```
In the above example one can observe the match rule in the 'pvs' extension of the protected header field by decoding it using base64:
```
# Example: a decoded protected pvs header
{
"alg": "RS256",
"pvs": {
"include" ["bsp/*"],
"exclude" ["bsp/src.json"],
},
"typ": "PVS"
}
```
## Sign Flow
PVS signatures are expected to be created or updated:
1. update - reusing existing parameters to refresh signatures of potentially changed binaries in a prisitine state
2. add - add new signatures that span the json entries of
Some pseudo code showing how such signature could be created and updated is as follows:
```
func sign (args, doc, app, privkey){
var header
if update && exists "_sigs/${app}.json" {
header = parseJwsProtected("$_sigs/${app}.json")
} else if update {
error "no jws header info found for updating"
} else {
// parse --include and --exclude args to construct the header.
header = parseAppArgs(app, args)
}
// sign
signature = createJws(privkey, doc, selectElements(header))
// detach payload element
signature.delete("payload")
// embed by writing and committing to disk
write (signature, "_sigs/$app.json")
pvrAdd("_sigs/$app.json")
pvrCommitNoSign()
}
```
## Verify Flow
For each JWS Token file name `*/pvs.json` with PVS extension in the state json, Pantavisor will validate the the payload selected by the match rule in the `pvs` extension header.
Example pseudo code verifying:
```
RS256.Verify(signature, rsaPubKey,
'{' concate(each json item matched) '}')
```
To calculate the payload, the verifier is expected to introspect the protected header _before_ verification and select the elements matched by the rule encoded within.
To verify the payload, it will use the public key encoded inside the protected header. In trusted mode the verifier will check that the public key matches the public key explicitely known in a trust DB.
Note: In initial iteration pantavisor will only look at trustdb to match the key - not considering the embbeded jwk.
## Runtime Support
Pantavisor runtime will use the pvs.json file to validate that the files matched by the signature have not been modified at install time and right when provisioning these for use at runtime.
Pantavisor will also keep track of files not protected through signature and will use a global policy setting to determine whether a runtime state with unprotected files can be booted at all.
Pantavisor will offer a config to set the validation mode from "default" to "strict". The config key is named `secureboot.mode=disabled|lenient|strict`
In `lenient` mode all parts that have a signature must have valid signature.
In `strict` mode all parts must have a signature that is valid and for which the public key is
Pantavisor will also offer a config to configure whether to run secureboot in trusted mode or `secureboot.trusted=on`. If trusted mode is on Pantavisor will check that the pubkey is known in the /var/lib/pantavisor/trustdb.pem file
## Algorithm Support
The JWS standard offers a vast choice of algorithms; due to nature of our primary use case Pantavisor will only support the RS256 algorithm that uses pub/priv key. Other variants can be added as needed.
## Tools Support
PVS tool support is made available as part of the `pvr sig` subcommand.
Also it is noteworthy that pvs signatures will be preserved in exports done through `pvr export` tool. This allows us to use PVS also for making signatures that can be shipped embedded in Pantavisor apps and bsp packages in a decoupled way (like in an app store).
### `pvr sig`
```
$ pvr sig --help
NAME:
pvr sig - manage sig usage
USAGE:
pvr sig command [command options] [arguments...]
COMMANDS:
add, a embed a signature protecting the json document elements of the provided part using matchrule. By default we include all elements startings with _config/parts unless --noconfig is provided
update, up update a pvs@2 signature by using the encoded matchrule
ls verify and list content protected by _sigs/<NAME>.json for all arguments; by default it matches *
OPTIONS:
--key value, -k value private key in PEM format to use for signing [$PVR_SIG_KEY]
--pubkey value, -p value pubkey in PEM format to use for signing [$PVR_SIG_PUBKEY]
--help, -h show help
```
### `pvr sig add`
```
$ pvr sig add --help
NAME:
pvr sig add - embed a signature protecting the json document elements of the provided part using matchrule. By default we include all elements startings with _config/parts unless --noconfig is provided
USAGE:
pvr sig add [command options] [arguments...]
OPTIONS:
--part value, -p value select elements of part
--raw value, -r value select elements using raw include/exclude; with this, noconfig does not apply
--include value, -i value include files by glob pattern (default: "**")
--exclude value, -e value exclude files by glob patterns (default: "src.json")
--noconfig, -n exclude _config parts from signature
```
### `pvr sig update`
```
$ pvr sig ls --help
NAME:
pvr sig ls - verify and list content protected by _sigs/<NAME>.json for all arguments; by default it matches *
USAGE:
pvr sig ls [command options] [<NAME>] ...
OPTIONS:
--part value, -p value select elements of part [$PVR_SIG_ADD_PART]
```
# Examples
Create PVS Keys (currently with openssl):
```
# rs256
openssl genrsa -out ~/.pvr/keys/priv.rs256.pem 2048
# es256
openssl ecparam -genkey -name prime256v1 -noout -out ~/.pvr/keys/priv.es256.pem
```
Split public key out
```
# rs256
openssl rsa -in ~/.pvr/keys/priv.rs256.pem -outform PEM -pubout -out ~/.pvr/keys/pub.rs256.pem
# es256
openssl ec -in ~/.pvr/keys/priv.es256.pem -pubout -out ~/.pvr/keys/pub.es256.pem
```
Sign awconnect app
```
pvr sig --key ~/.pvr/keys/priv.es256.pem add --part awconnect
```
Sign custom set of json elements selected by include/exclude.
The example below signs all top level elements with keys that are not inside a "part":
```
pvr sig --key ~/.pvr/keys/priv.es256.pem add --raw _root_ --include '*' --exclude="_hostconfig/**"
```
Update one signature of a part
```
pvr sig --key ~/.pvr/keys/priv.es256.pem update awconnect
Updating pvs signature @ _sigs/awconnect.json [DONE]
```
Update signature pvs@2 json by path
```
$ pvr sig --key ~/.pvr/keys/priv.es256.pem update _sigs/awconnect.json
Updating pvs signature @ _sigs/awconnect.json [DONE]
```
Update signature for all
```
$ pvr sig --key ~/.pvr/secrets/priv.es256.pem update
Updating pvs signature @ _sigs/awconnect.json [DONE]
Updating pvs signature @ _sigs/bsp.json [DONE]
Updating pvs signature @ _sigs/pv-avahi.json [DONE]
Updating pvs signature @ _sigs/pvr-sdk.json [DONE]
```
Verify signature for awconnect
```
$ pvr sig --pubkey ~/.pvr/secrets/pub.all.pem ls _sigs/*
{
"protected": [
"#spec",
"awconnect/lxc.container.conf",
"awconnect/root.squashfs",
"awconnect/root.squashfs.docker-digest",
"awconnect/run.json",
"bsp/addon-plymouth.cpio.xz4",
"bsp/build.json",
"bsp/firmware.squashfs",
"bsp/kernel.img",
"bsp/modules.squashfs",
"bsp/pantavisor",
"bsp/run.json",
"network-mapping.json",
"pv-avahi/lxc.container.conf",
"pv-avahi/root.squashfs",
"pv-avahi/root.squashfs.docker-digest",
"pv-avahi/run.json",
"pvr-sdk/lxc.container.conf",
"pvr-sdk/root.squashfs",
"pvr-sdk/root.squashfs.docker-digest",
"pvr-sdk/run.json",
"storage-mapping.json"
],
"excluded": [
"_hostconfig/pvr/docker.json",
"awconnect/src.json",
"bsp/src.json",
"pv-avahi/src.json",
"pvr-sdk/src.json"
],
"notseen": [
"_sigs/awconnect.json",
"_sigs/bsp.json",
"_sigs/pv-avahi.json",
"_sigs/pvr-sdk.json"
]
}
```
Verify all signature and show aggregated state of protected/excluded/notseen:
```
```
# References
[1] - JWS https://datatracker.ietf.org/doc/html/rfc7515 (JWS spec)
[2] - JWA https://datatracker.ietf.org/doc/html/rfc7518 (algorithms)
[3] - System State Spec https://docs.pantahub.com/pantavisor-state-format-v2/