# Verifying that requests originate from Office Online by using proof keys¶

When processing WOPI requests from Office Online, you might want to verify that these requests are coming from Office Online. To do this, you use proof keys.

Office Online signs every WOPI request with a private key. The corresponding public key is available in the
**proof-key** element in the WOPI discovery XML. The signature is sent with every request in the **X-WOPI-Proof** and
**X-WOPI-ProofOld** HTTP headers.

The signature is assembled from information that is available to the WOPI host when it processes the incoming WOPI request. To verify that a request came from Office Online, you must:

- Create the expected value of the proof headers.
- Use the public key provided in WOPI discovery to decrypt the proof provided in the
**X-WOPI-Proof**header. - Compare the expected proof to the decrypted proof. If they match, the request originated from Office Online.
- Ensure that the
**X-WOPI-TimeStamp**header is no more than 20 minutes old.

Note

Requests to the FileUrl will not be signed. The FileUrl is used exactly as provided by the host, so it does not necessarily include the access token, which is required to construct the expected proof.

Tip

The Office Online GitHub repository contains a set of unit tests that hosts can adapt to verify proof key validation implementations. See Proof key unit tests for more information.

## Constructing the expected proof¶

To construct the expected proof, you must assemble a byte array consisting of the access token, the URL of the
request (in uppercase), and the value of the **X-WOPI-TimeStamp** HTTP header from the request. Each of these values
must be converted to a byte array. In addition, you must include the length, in bytes, of each of these values.

To convert the access token and request URL values, which are strings, to byte arrays, you must ensure the original
strings are in UTF-8 first, then convert the UTF-8 strings to byte arrays. Convert the **X-WOPI-TimeStamp** header
to a *long* and then into a byte array. Do not treat it as a string.

Then, assemble the data as follows:

- 4 bytes that represent the length, in bytes, of the
`access_token`

on the request. - The
`access_token`

. - 4 bytes that represent the length, in bytes, of the full URL of the WOPI request, including any query string parameters.
- The WOPI request URL in all uppercase. All query string parameters on the request URL should be included.
- 4 bytes that represent the length, in bytes, of the
**X-WOPI-TimeStamp**value. - The
**X-WOPI-TimeStamp**value.

The following C# code snippet illustrates the construction of an expected proof.

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | ```
public bool Validate(ProofKeyValidationInput testCase)
{
// Encode values from headers into byte[]
var accessTokenBytes = Encoding.UTF8.GetBytes(testCase.AccessToken);
var hostUrlBytes = Encoding.UTF8.GetBytes(testCase.Url.ToUpperInvariant());
var timeStampBytes = EncodeNumber(testCase.Timestamp);
// prepare a list that will be used to combine all those arrays together
List<byte> expectedProof = new List<byte>(
4 + accessTokenBytes.Length +
4 + hostUrlBytes.Length +
4 + timeStampBytes.Length);
expectedProof.AddRange(EncodeNumber(accessTokenBytes.Length));
expectedProof.AddRange(accessTokenBytes);
expectedProof.AddRange(EncodeNumber(hostUrlBytes.Length));
expectedProof.AddRange(hostUrlBytes);
expectedProof.AddRange(EncodeNumber(timeStampBytes.Length));
expectedProof.AddRange(timeStampBytes);
// create another byte[] from that list
byte[] expectedProofArray = expectedProof.ToArray();
// validate it against current and old keys in proper combinations
bool validationResult =
TryVerification(expectedProofArray, testCase.Proof, _currentKey.CspBlob) ||
TryVerification(expectedProofArray, testCase.OldProof, _currentKey.CspBlob) ||
TryVerification(expectedProofArray, testCase.Proof, _oldKey.CspBlob);
// TODO:
// in real code you should also check that TimeStamp header is no more than 20 minutes old
// but because we're using predefined test cases to validate that the method works
// we can't do it here.
return validationResult;
}
``` |

## Retrieving the public key¶

Office Online provides two different public keys as part of the WOPI discovery XML: the current key and the old key. Two keys are necessary because the discovery data is meant to be cached by the host, and Office Online periodically rotates the keys it uses to sign requests. When the keys are rotated, the current key becomes the old key, and a new current key is generated. This helps to minimize the risk that a host does not have updated key information from WOPI discovery when Office Online rotates keys.

Both keys are represented in the discovery XML in two different formats. One format is for WOPI hosts that use the .NET framework. The other format can be imported in a variety of different programming languages and platforms.

### Using .NET to retrieve the public key¶

If your application is built on the .NET framework, you should use the contents of the **value** and **oldvalue**
attributes of the *proof-key* element in the WOPI discovery XML. These two attributes contain the Base64-encoded public
keys that are exported by using the RSACryptoServiceProvider.ExportCspBlob method of the .NET Framework.

To import this key in your application, you must decode it from Base64 then import it by using the RSACryptoServiceProvider.ImportCspBlob method.

### Using the RSA modulus and exponent to retrieve the public key¶

For hosts that don’t use the .NET framework, Office Online provides the RSA modulus and exponent directly. The
modulus and exponent of the current key are found in the **modulus** and **exponent** attributes of the *proof-key*
element in the WOPI discovery XML. The modulus and exponent of the old key are found in the **oldmodulus** and
**oldexponent** attributes. All four of these values are Base64-encoded.

The steps to import these values differ based on the language, platform, and cryptography API that you are using.

The following example shows how to import the public key by using the modulus and exponent in a Python program using the PyCrypto library.

38 39 40 41 42 43 44 45 46 47 48 49 50 51 | ```
def generate_key(modulus_b64, exp_b64):
"""
Generates an RSA public key given a base64-encoded modulus and exponent
:param modulus_b64: base64-encoded modulus
:param exp_b64: base64-encoded exponent
:return: an RSA public key
"""
mod = int(b64decode(modulus_b64).encode('hex'), 16)
exp = int(b64decode(exp_b64).encode('hex'), 16)
seq = asn1.DerSequence()
seq.append(mod)
seq.append(exp)
der = seq.encode()
return RSA.importKey(der)
``` |

## Verifying the proof keys¶

After you import the key, you can use a verification method provided by your cryptography library to verify incoming requests were signed by Office Online. Because Office Online rotates the current and old proof keys periodically, you have to check three combinations of proof key values:

- The
**X-WOPI-Proof**value using the current public key - The
**X-WOPI-ProofOld**value using the current public key - The
**X-WOPI-Proof**value using the old public key

If any one of the values is valid, the request was signed by Office Online.

The following example shows how to verify one of these combinations in .NET using C#.

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | ```
private static bool TryVerification(byte[] expectedProof,
string signedProof,
string publicKeyCspBlob)
{
using(RSACryptoServiceProvider rsaAlg = new RSACryptoServiceProvider())
{
byte[] publicKey = Convert.FromBase64String(publicKeyCspBlob);
byte[] signedProofBytes = Convert.FromBase64String(signedProof);
try
{
rsaAlg.ImportCspBlob(publicKey);
return rsaAlg.VerifyData(expectedProof, "SHA256", signedProofBytes);
}
catch(FormatException)
{
return false;
}
catch(CryptographicException)
{
return false;
}
}
}
``` |

The following example shows how to verify one of these combinations in Python using the PyCrypto library.

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | ```
def try_verification(expected_proof, signed_proof_b64, public_key):
"""
Verifies the signature of a signed WOPI request using a public key provided in
WOPI discovery.
:param expected_proof: a bytearray of the expected proof data
:param signed_proof_b64: the signed proof key provided in the X-WOPI-Proof or
X-WOPI-ProofOld headers. Note that the header values are base64-encoded, but
will be decoded in this method
:param public_key: the public key provided in WOPI discovery
:return: True if the request was signed with the private key corresponding to
the public key; otherwise, False
"""
signed_proof = b64decode(signed_proof_b64)
verifier = PKCS1_v1_5.new(public_key)
h = SHA256.new(expected_proof)
v = verifier.verify(h, signed_proof)
return v
``` |

## Troubleshooting proof key implementations¶

If you are having difficulty with your proof key verification implementation, here are some common issues that you should investigate:

- Verify you’re converting the URL to uppercase.
- Verify you’re including any query string parameters on the URL when transforming it for the purposes of building the expected proof value.
- Verify you’re using the same encoding for any special characters that may be in the URL.
- Verify you’re using an HTTPS URL if your WOPI endpoints are HTTPS. This is especially important if you have SSL termination in your network prior to your WOPI request handlers. In this case, the URL Office Online will use to sign the requests will be HTTPS, but the URL your WOPI handlers ultimately receive will be HTTP. If you simply use the incoming request URL your expected proof will not match the signature provided by Office Online.

In addition, use the Proof key unit tests to verify your implementation with sample data.