Attestation: Add swimlane diagrams

This commit is contained in:
Nicolas Williams 2021-05-08 16:21:18 -05:00
parent fd056d8ca2
commit 1ba6ee7746
8 changed files with 406 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -0,0 +1,20 @@
@startuml
participant TPM as T
participant Client as C
participant Server as S
participant ServerSimTPM as ST
title Four-message (two round trips) attestation protocol
C -> C: timestamp = gettimeofday();
C --> T: TPM2_Quote(AK, set-of-all-PCRs, timestamp)
T --> C: quote=Signed_AK({hash-of-PCRs, misc, timestamp})
C -> S: [ID], EKpub, [EKcert], AKpub,\nPCRs, eventlog, timestamp, quote
S -> S: check that timestamp is recent;\ndata = Lookup(EKpub, [EKcert], [ID]);\n[Validate(EKcert)];\ncompute PCRs hash from eventlog and PCRs;\nvalidate quote;\nsession_key = genkey();\nAKcert = CA_Certify(AKpub, data.ID, AKtbscert);\nticket = {vno, Encrypt_server_secret_key({timestamp,\n\t\t\tgettimeofday(),\n\t\t\tsession_key})}
S --> ST: TPM2_MakeCredential(EKpub, AKpub, session_key)
ST --> S: credentialBlob, secret
S -> C: credentialBlob, secret, ticket
C --> T: TPM2_ActivateCredential(AKhandle, EKhandle,\n\t\t\t\t\tcredentialBlob, secret)
T --> C: certInfo = session_key
C -> C: PoP = HMAC_session_key(ticket)
S -> C: Encrypt_session_key(stuff =\n\t\t\t\t\t\t{AKcert, data.for_client})
C -> C: {AKcert, secrets} =\n\t\tDecrypt_session_key(stuff)
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -0,0 +1,19 @@
@startuml
participant TPM as T
participant Client as C
participant Server as S
participant ServerSimTPM as ST
title Three-message (1.5 round trips) attestation protocol w/ Proof-of-Possession
C -> C: timestamp = gettimeofday();
C --> T: TPM2_Quote(AK, set-of-all-PCRs, timestamp)
T --> C: quote=Signed_AK({hash-of-PCRs, misc, timestamp})
C -> S: [ID], EKpub, [EKcert], AKpub,\nPCRs, eventlog, timestamp, quote
S -> S: check that timestamp is recent;\ndata = Lookup(EKpub, [EKcert], [ID]);\n[Validate(EKcert)];\ncompute PCRs hash from eventlog and PCRs;\nvalidate quote;\nsession_key = genkey();\nAKcert = CA_Certify(AKpub, data.ID, AKtbscert);\nstuff = Encrypt_session_key({AKcert,\n\t\t\t\t\tdata.for_client})
S --> ST: TPM2_MakeCredential(EKpub, AKpub, session_key)
ST --> S: credentialBlob, secret
S -> C: credentialBlob, secret, stuff
C --> T: TPM2_ActivateCredential(AKhandle, EKhandle,\n\t\t\t\t\tcredentialBlob, secret)
T --> C: certInfo = session_key
C -> C: {AKcert, secrets} =\n\t\tDecrypt_session_key(stuff);
C -> S: {AKcert, PoP = Digest(AKcert)}
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -0,0 +1,18 @@
@startuml
participant TPM as T
participant Client as C
participant Server as S
participant ServerSimTPM as ST
title Two-message (one round trip) attestation protocol
C -> C: timestamp = gettimeofday();
C --> T: TPM2_Quote(AK, set-of-all-PCRs, timestamp)
T --> C: quote=Signed_AK({hash-of-PCRs, misc, timestamp})
C -> S: [ID], EKpub, [EKcert], AKpub,\nPCRs, eventlog, timestamp, quote
S -> S: check that timestamp is recent;\ndata = Lookup(EKpub, [EKcert], [ID]);\n[Validate(EKcert)];\ncompute PCRs hash from eventlog and PCRs;\nvalidate quote;\nsession_key = genkey();\nAKcert = CA_Certify(AKpub, data.ID, AKtbscert);\nstuff = Encrypt_session_key({AKcert,\n\t\t\t\t\tdata.for_client})
S --> ST: TPM2_MakeCredential(EKpub, AKpub, session_key)
ST --> S: credentialBlob, secret
S -> C: credentialBlob, secret, stuff
C --> T: TPM2_ActivateCredential(AKhandle, EKhandle,\n\t\t\t\t\tcredentialBlob, secret)
T --> C: certInfo = session_key
C -> C: {AKcert, secrets} =\n\t\tDecrypt_session_key(stuff);
@enduml

View file

@ -12,29 +12,55 @@ A computer can use a TPM to demonstrate:
Possible outputs of succesful attestation:
- encrypted filesystems getting unlocked with the help of an
attestation server
- authorize client to join its network
- other secrets (e.g., credentials for various authentication systems)
- delivery of configuration metadata to the client
- issuance of X.509 certificate(s) for TPM-resident public keys
- unlocking of storage / filesystems on the client
- delivery of various secrets, such credentials for various authentication systems:
- issuance of X.509 certificate(s) for TPM-resident attestaion
public keys
For servers these certificates would have `dNSName` subject
alternative names (SANs).
For a user device such a certificate might have a subject name and/or
SANs identifying the user.
For a user device such a certificate might have a subject name
and/or SANs identifying the user or device.
- issuance of non-PKIX certificates (e.g., OpenSSH-style certificates)
- issuance of Kerberos host-based service principal long-term keys
("keytabs")
- service account tokens
- etc.
- client state tracking
- etc.
Possible outputs of unsuccessful attestation:
- alerting
- diagnostics (e.g., which PCR extensions in the PCR quote and eventlog
are not recognized)
are not recognized, which then might be used to determine what
firmware / OS updates a client has installed, or that it has been
compromised)
In this tutorial we'll focus on attestion of servers in an enterprise
environment. However, the concepts described here are applicable to
other environments, such as IoTs and personal devices, where the
attestation database could be hosted on a user's personal devices for
use in joining new devices to the user's set of devices, or for joining
new IoTs to the user's SOHO network.
# Attestation Protocols
Attestation is done by a computer with a TPM interacting with an
Attestation is done by a client computer with a TPM interacting with an
attestation service over a network. This requires a network protocol
for attestation.
@ -91,6 +117,27 @@ EKpub and AKpub will happen via
[`TPM2_MakeCredential()`](TPM2_MakeCredential.md) /
[`TPM2_ActivateCredential()`](TPM2_ActivateCredential.md).
Note that the [`TPM2_Quote()`](TPM2_Quote.md) function produces a signed
message -- signed with a TPM-resident AK named by the caller (and to
which they have access), which would be the AK used in the attestation
protocol.
The output of [`TPM2_Quote()`](TPM2_Quote.md) might be the only part of
a client's messages to the attestation service that include a signature
made with the AK, but integrity protection of everything else can be
implied (e.g., the eventlog and PCR values are used to reconstruct the
PCR digest signed in the quote). `TPM2_Quote()` signs more than just a
digest of the selected PCRs. `TPM2_Quote()` signs all of:
- digest of selected PCRs
- caller-provided extra data (e.g., a cookie/nonce/timestamp/...),
- the TPM's firmware version number,
- `clock` (the TPM's time since startup),
- `resetCount` (an indirect indicator of reboots),
- `restartCount` (an indirect indicator of suspend/resume events)
- and `safe` (a boolean indicating whether the `clock` might have ever
gone backwards).
## Binding of Other Keys to EKpub
The semantics of [`TPM2_MakeCredential()`](TPM2_MakeCredential.md) /
@ -143,7 +190,9 @@ Let's start with few observations and security considerations:
- Some replay protection or freshness indication for client requests is
needed. A stateful method of doing this is to use a server-generated
nonce. A stateless method is to use a timestamp.
nonce (as an encrypted state cookie embedding a timestamp). A
stateless method is to use a timestamp and reject requests with old
timestamps.
- Replay protection of server to client responses is mostly either not
needed or implicitly provided by [`TPM2_MakeCredential()`](TMP2_MakeCredential.md)
@ -176,6 +225,33 @@ Let's start with few observations and security considerations:
have to be sent may not fit on a URI local part or URI query
parameters, therefore HTTP `POST` is the better option.
### Error Cases Not Shown
Note that error cases are not shown in the protocols described below.
Naturally, in case of error the attestation server will send a suitable
error message back to the client.
### Databases, Log Sinks, and Dashboarding / Alerting Systems Not Shown
In order to simplify the protocol diagrams below, interactions with
databases, log sinks, and alerting systems are not shown.
A typical attestation service will, however, have interactions with
those components, some or all of which might even be remote:
- attestation database
- log sinks
- dashboarding / alerting
If an attestation service must be on the critical path for booting an
entire datacenter, it may be desirable for the attestation service to be
able to run with no remote dependencies, at least for some time. This
means, for example, that the attestation database should be locally
available and replicated/synchronized only during normal operation. It
also means that there should be a local log sink that can be sent to
upstream collectors during normal operation.
### Single Round Trip Attestation Protocol Patterns
An attestation protocol need not complete proof-of-possession
@ -195,11 +271,13 @@ protocol:
```
<client knows a priori what PCRs to quote, possibly all, saving a round trip>
CS0: Signed_AK({timestamp, [ID], EKpub, [EKcert],
AKpub, PCR_quote, eventlog})
CS0: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, timestamp,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key),
Encrypt_session_key({AKcert, filesystem_keys, etc.})}
<extra_data includes timestamp>
<subsequent client use of AK w/ AKcert, or of credentials made
available by dint of being able to access filesystems unlocked by
SC0, demonstrate that the client has attested successfully>
@ -207,6 +285,11 @@ protocol:
(`ID` might be, e.g., a hostname.)
![Protocol Diagram](Protocol-Two-Messages.png)
(In this diagram we show the use of a TPM simulator on the server side
for implementing [`TPM2_MakeCredential()`](TPM2_MakeCredential.md).)
The server will validate that the `timestamp` is near the current time,
the EKcert (if provided, else the EKpub), the signature using the
asserted (but not yet bound to the EKpub) AKpub, then it will validate
@ -218,26 +301,67 @@ The client obtains those items IFF (if and only if) the AK is resident
in the same TPM as the EK, courtesy of `TPM2_ActivateCredential()`'s
semantics.
NOTE well that in this example it is *essential* that the AKcert not be
logged in any public place since otherwise an attacker can make and send
`CS0` using a non-TPM-resident AK and any TPM's EKpub/EKcert known to
the attacker, and then it may recover the AK certificate from the log in
spite of being unable to recover the AK certificate from `SC1`!
NOTE well that in single round trip attestation protocols using only
decrypt-only EKs it is *essential* that the AKcert not be logged in any
public place since otherwise an attacker can make and send `CS0` using a
non-TPM-resident AK and any TPM's EKpub/EKcert known to the attacker,
and then it may recover the AK certificate from the log in spite of
being unable to recover the AK certificate from `SC1`!
Alternatively, a single round trip attestation protocol can be
implemented as an optimization to a two round trip protocol when the AK
is persisted both, in the client TPM and in the attestation service's
database:
```
<having previously successfully enrolled AKpub and bound it to EKpub...>
CS0: Signed_AK({timestamp, AKpub, PCR_quote, eventlog})
CS0: timestamp, AKpub, PCRs, eventlog,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key),
Encrypt_session_key({AKcert, filesystem_keys, etc.})}
```
### Three-Message Attestation Protocol Patterns
A single round trip protocol using encrypt-only EKpub will not
demonstrate proof of possession immediately, but later on when the
certified AK is used elsewhere. A proof-of-possession (PoP) may be
desirable anyways for monitoring and alerting purposes.
```
CS0: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, timestamp,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key),
Encrypt_session_key({AKcert, filesystem_keys, etc.})}
CS1: AKcert, Signed_AK(AKcert)
```
![Protocol Diagram](Protocol-Three-Messages.png)
(In this diagram we show the use of a TPM simulator on the server side
for implementing [`TPM2_MakeCredential()`](TPM2_MakeCredential.md).)
NOTE well that in this protocol, like single round trip attestation
protocols using only decrypt-only EKs, it is *essential* that the AKcert
not be logged in any public place since otherwise an attacker can make
and send `CS0` using a non-TPM-resident AK and any TPM's EKpub/EKcert
known to the attacker, and then it may recover the AK certificate from
the log in spite of being unable to recover the AK certificate from
`SC1`!
If such a protocol is instantiated over HTTP or TCP, it will really be
more like a two round trip protocol:
```
CS0: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, timestamp,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key),
Encrypt_session_key({AKcert, filesystem_keys, etc.})}
CS1: AKcert, Signed_AK(AKcert)
SC1: <empty>
```
### Two Round Trip Stateless Attestation Protocol Patterns
We can add a round trip to the protocol in the previous section to make
@ -252,11 +376,13 @@ back to the server rather than a secret key possesion of which is proven
with symmetriclly-keyed cryptographic algorithms.
```
CS0: Signed_AK({timestamp, [ID], EKpub, [EKcert],
AKpub, PCR_quote, eventlog})
CS0: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, timestamp,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key), ticket}
CS1: {ticket, MAC_session_key(CS0), CS0}
SC1: Encrypt_session_key({AKcert, filesystem_keys, etc.})
<extra_data includes timestamp>
```
where `session_key` is an ephemeral secret symmetric authenticated
@ -267,6 +393,8 @@ encryption key, and `ticket` is an authenticated encrypted state cookie:
MAC_session_key(CS0)})}
```
![Protocol Diagram](Protocol-Four-Messages.png)
where `server_secret_key` is a key known only to the attestation service
and `vno` identifies that key (in order to support key rotation without
having to try authenticated decryption twice near key rotation events).
@ -309,6 +437,10 @@ An HTTP API binding for this protocol could look like:
Response: SC1
```
Here the attestation happens in the first round trip, but the proof of
possession is completed in the second, and the delivery of secrets and
AKcert also happens in the second round trip.
### Actual Protocols: ibmacs
The [`IBM TPM Attestation Client Server`](https://sourceforge.net/projects/ibmtpm20acs/)
@ -358,7 +490,20 @@ to two round trips.
### Actual Protocols: safeboot.dev
(TBD)
```
CS0: <empty>
SC0: nonce, PCR_list
CS1: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, nonce,
TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data})
SC1: {TPM2_MakeCredential(EKpub, AKpub, session_key),
Encrypt_session_key({filesystem_keys})}
```
Nonce validation is currently not well-developed in Safeboot.
If a timestamp is used instead of a nonce, and if the client assumes all
PCRs are desired, then this becomes a one round trip protocol.
An AKcert will be added to the Safeboot protocol soon.
### Actual Protocols: ...
@ -383,9 +528,8 @@ proof of possession non-interactively, whereas asymmetric encryption
requires interaction to prove possession:
```
CS0: Signed_AK({timestamp, [ID], EKpub, [EKcert],
AKpub, TPM2_Certify(EKpub, AKpub),
PCR_quote, eventlog})
CS0: timestamp, [ID], EKpub, [EKcert], AKpub, PCRs, eventlog,
TPM2_Certify(EKpub, AKpub), TPM2_Quote()
SC0: AKcert
```
@ -393,11 +537,9 @@ If secrets need to be sent back, then a decrypt-only EK also neds to be
used:
```
CS0: Signed_AK({timestamp, [ID],
EKpub_signing, EKpub_encrypt,
[EKcert_signing], [EKcert_encrypt],
AKpub, TPM2_Certify(EKpub, AKpub),
PCR_quote, eventlog})
CS0: timestamp, [ID], EKpub_signing, EKpub_encrypt,
[EKcert_signing], [EKcert_encrypt], AKpub, PCRs, eventlog,
TPM2_Certify(EKpub, AKpub), TPM2_Quote()
SC0: {TPM2_MakeCredential(EKpub_encrypt, AKpub, session_key),
Encrypt_session_key({AKcert, filesystem_keys, etc.})}
```
@ -408,6 +550,7 @@ Attestation servers need to keep some long-term state:
- binding of `EKpub` and `ID`
- PCR validation profile(s) for each identified client
- resetCount (for reboot detection)
Log-like attestation state:
@ -431,16 +574,138 @@ Things to log:
attestation protocols above -- do not log AKcerts in public places
when using single round trip attestation protocols!)
## Long-Term State Created by Attestation Services
## Long-Term State Created or Updated by Attestation Services
An attestation service might support creation of host&lt;-&gt;EKpub
bindings on a first-come-first-served basis.
- An attestation service might support creation of host&lt;-&gt;EKpub
bindings on a first-come-first-served basis. In this mode the
attestation server might validate an EKcert and that the desired
hostname has not been bound to an EK, then create the binding.
An attestation service might support deletion of host PCR validation
profiles that represent past states upon validation of PCR quotes using
newer profiles. This could be used to permit firmware and/or operating
system upgrades and then disallow downgrades after evidence of
successful upgrade.
- An attestation service might support deletion of host PCR validation
profiles that represent past states upon validation of PCR quotes
using newer profiles. This could be used to permit firmware and/or
operating system upgrades and then disallow downgrades after evidence
of successful upgrade.
- An attestation service might keep track of client reboots so as to:
- revoke old AKcerts when the client reboots (but note that this is
really not necessary if we trust the client's TPM, since then the
previous AKs will never be usable again)
- alert if the reboot count ever goes backwards
## Schema for Attestation Server Database
A schema for the attestation server's database entries might look like:
```JSON
{
"EKpub": "<EKpub>",
"hostname": "<hostname>",
"EKcert": "<EKcert in PEM, if available>",
"previous_firmware_profile": "FWProfile0",
"current_firmware_profiles": ["FWProfile1", "FWProfile2", "..."],
"previous_operating_system_profiles": "OSProfile0",
"current_operating_system_profiles": ["OSProfile1", "OSProfile2", "..."],
"previous_PCRs": "<...>",
"proposed_PCRs": "<...>",
"ak_cert_template": "<AKCertTemplate>",
"secrets": "<secrets>",
"resetCount": "<resetCount value from last quote>"
}
```
The attestation server's database should have two lookup keys:
- EKpub
- hostname
The attestation server's database's entry for any client should provide,
de minimis:
- a way to validate the root of trust measurements in the client's
quoted PCRs, for which two methods are possible:
- save the PCRs quoted last as the ones expected next time
- or, name profiles for validating firmware RTM PCRs and profiles
for validating operating system RTM PCRs
A profile for validating PCRs should contain a set of expected extension
values for each of a set of PCRs. The attestation server can then check
that the eventlog submitted by the client lists exactly those extension
values and no others. PCR extension order in the eventlog probably
doesn't matter here. If multiple profiles are named, then one of those
must match -- this allows for upgrades and downgrades.
```JSON
{
"profile_name":"SomeProfile",
"values":[
{
"PCR":0,
"values":["aaaaaaa","bbbbbb","..."]
},
{
"PCR":1,
"values":["ccccccc","dddddd","..."]
}
]
}
```
Using the PCR values from the previous attestation makes upgrades
tricky, probably requiring an authenticated and authorized administrator
to bless new PCR values after an upgrade. A client that presents a PCR
quote that does not match the previous one would cause the
`proposed_PCRs` field to be updated but otherwise could not continue,
then an administrator would confirm that the client just did a
firmware/OS upgrade and if so replace the `previous_PCRs` with the
`proposed_PCRs`, then the client could attempt attestation again.
## Dealing with Secrets
An attestation server might want to return storage/filesystem decryption
key-encryption-keys to a client. But one might not want to store those
keys in the clear on the attestation server. As well, one might want a
break-glass way to recover those secrets.
For break-glass recover, the simplest thing to do is to store
`Encrypt_backupKey({EKpub, hostname, secrets})`, where `backupKey` is an
asymmetric key whose private key is stored offline (e.g., in a safe, or
in an offline HSM). To break the glass and recover the key, just bring
the ciphertext to the offline system where the private backup key is
kept, decrypt it, and then use the secrets manually to recover the
affected system.
Here are some ideas for how to make an attestation client depend on the
attestation server giving it keys needed to continue booting after
successful attestation:
- Store `TPM2_MakeCredential(EKpub, someObjectName, key0), Encrypt_key0(secrets)`.
In this mode the server sends the client the stored data, then client
gets to recreate `someObject` (possibly by loading a saved object) on
its TPM so that the corresponding call to `TPM2_ActivateCredential()`
can succeed, then the client recovers `key0` and decrypts the
encrypted secrets. Here `someObject` can be trivial and need only
exist to make the `{Make,Activate}Credential` machinery work.
TPM replacement and/or migration of a host from one physical system
to another can be implemented by learning the new system's TPM's
EKpub and using the offline `backupKey` to compute
`TPM2_MakeCredential(EKpub_new, someObjectName, key0)` and update the
host's entry.
- Store a secret value that will be extended into an application PCR
that is used as a policy PCR for unsealing a persistent object stored
on the client's TPM.
In this mode the server sends the client the secret PCR extension
value, and the client uses it to extend a PCR such that it can then
unseal the real storage / filesystem decryption keys.
- A hybrid of the previous two options, where the server stores a
secret PCR extension value wrapped with `TPM2_MakeCredential()`.
Other ideas?
# References

45
Attestation/TPM2_Quote.md Normal file
View file

@ -0,0 +1,45 @@
# `TPM2_Quote()`
`TPM2_Quote()` computes a hash of the PCRs selected by the caller, and
signs that hash, some additional metadata, and any extra data provided
by the caller, with a signing key named by the caller. The caller must
have access to that key, naturally.
The PCRs' values are NOT included in the quote produced by
`TPM2_Quote()`. Instead, an attestation service can review an unsigned
eventlog to ensure it leads to the same values as unsigned PCR values
also provided by the attestation client, and then the attestation
service can verify that the hash of the PCR values is indeed signed by
the quote supplied by the client.
## Inputs
- `TPMI_DH_OBJECT sigHandle` (handle for an AK)
- `TPM2B_DATA qualifyingData` (extra data)
- `TPMT_SIG_SCHEME inScheme` ("signing scheme to use if the schemefor signHandleis `TPM_ALG_NULL`")
- `TPML_PCR_SELECTION PCRselect` (set of PCRs to quote)
## Outputs (success case)
- `TPM2B_ATTEST quoted`
- `TPMT_SIGNATURE signature`
Where `TPM2B_ATTEST` is basically a `TPMS_ATTEST`, which contains the
following fields:
- `TPM_GENERATED magic`
- `TPMI_ST_ATTEST type`
- `TPM2B_NAME signer` (name of AK)
- `TPM2B_DATA extraData` ("external information supplied by caller")
- `TPMS_CLOCK_INFO clockInfo` ("Clock, resetCount, restartCount, and Safe")
- `UINT64 firmwareVersion`
- `TPMU_ATTEST attested`, a discriminated union with the
`TPMS_QUOTE_INFO` arm (indicated by the `TPM_ST_ATTEST_QUOTE`
discriminant value), which contains:
- `TPML_PCR_SELECTION pcrSelect` (the set of PCRs digested by `pcrDigest`)
- `TPM2B_DIGEST pcrDigest` (the digest of the PCRs indicated by `pcrSelect`)
## References
- [TCG TPM Library part 3: Commands, section 18.4](https://trustedcomputinggroup.org/wp-content/uploads/TCG_TPM2_r1p59_Part3_Commands_pub.pdf)