Verifying PowerAuth Signatures On The Server
Jun 04, 2020
This tutorial uses an old method for signature verification that directly leverages the PowerAuth Server’s internal API. For simple signature verification, use the API provided by the PowerAuth Cloud proxy component.
In this tutorial, we will show you how to verify PowerAuth signatures manually on the server side. While the task is relatively simple, it is very sensitive to any minor inconsistencies. Do not get frustrated if the signature verification does not work for you the first time. If you get stuck, do not hesitate to contact our engineers for help.
Introduction
When implementing mobile banking authentication and authorization, you need to implement at least two core processes:
- Activation - mobile device enrollment
- Transaction signing - for example, login or payment approval
The activation process can be entirely externalized into a standalone Enrollment Server application. The enrollment server can take over the activation process, and it can be deployed fully independently from your existing systems.
The transaction signing process is much more tightly coupled with your protected API resources. As a result, you usually need to integrate the PowerAuth signature verification logic into your existing systems that publish those protected resources. This is a trivial task if you use Spring framework thanks to our magical @PowerAuth
annotation, as we illustrate in our tutorial on mobile authentication and transaction signing.
However, not all systems use Spring or even Java. What if you use .NET, Java, Ruby, Python, or any other server-side technology? Do not worry - adding support for manual ad-hoc signature verification is not difficult.
Mobile App Perspective
To understand the server-side signature verification better, it is helpful to understand how the signatures are computed on the mobile device using our iOS or Android SDK.
When computing a signature, the mobile SDK requires the following input values:
- HTTP method, usually
POST
.- For signed requests, we always recommend using the
POST
method.
- For signed requests, we always recommend using the
- Resource ID is a constant that the client and server need to agree on beforehand.
- The value of this constant can be anything, but it is usually (by convention) set to the relevant part of the URI that gets called. For example, if the URI is
https://api.example.com/app-context/1.0/login
, the resource ID would be set to the/login
value. - Note: We use URI ID instead of the full URL since it could be difficult to deduce the correct path on the server side. Systems may run in different clusters and in different URI contexts (for example, the server could see
https://internal-machine/domain1/app-context/1.0-rel202005/login
instead of the above-mentioned address).
- The value of this constant can be anything, but it is usually (by convention) set to the relevant part of the URI that gets called. For example, if the URI is
- HTTP request data
- for
POST
,PUT
, andDELETE
requests, this is just the HTTP request body - for
GET
requests, we use canonicalized request query parameters
- for
Based on these input values, the mobile app produces an HTTP header with the PowerAuth signature that looks like this one:
X-PowerAuth-Authorization: PowerAuth pa_version="3.1",
pa_activation_id="f8******-****-****-****-**********76",
pa_application_key="Bz******************iQ==",
pa_nonce="GM******************ZQ==",
pa_signature_type="possession_knowledge",
pa_signature="wXay*************************************U="
As you can see, the header contains the:
- Signature version - A version of the used algorithm.
- Activation ID - An identifier of the cryptographic element.
- Application key - An identifier of the application version.
- Nonce - A random value used when computing the cryptographic signature.
- Signature type - A type of signature that represents the authentication factors that were used when computing the signature.
- Signature - An actual value of the signature.
The goal of the server is to:
- Accept the HTTP request sent by the mobile application.
- Extract all important elements from the HTTP request.
- Transform these elements precisely (bit-by-bit) to an appropriate format.
- Call the PowerAuth Server method that verifies the signature and returns additional results of the verification, mainly the user ID.
- Use the signature verification result to follow up with your own business logic.
Parsing the Signature Header
The first task that needs to be carried out is parsing the HTTP header with the PowerAuth signature. Get the value of the X-PowerAuth-Authorization
header from the incoming request.
This is the first thing that you should check: If you expect the signature header on a given resource and it is not there, you should stop the processing and return HTTP 401
.
The PowerAuth signature header has a fixed prefix PowerAuth
, followed by the comma-separated key-value pairs: key1="value1", key2="value2"
. Note that the values are always in quotes.
In our Java code, we use the PowerAuthHttpHeader
class (link) to do this work. The important part of the Java code that we use for the header parsing looks like this:
protected static final String POWERAUTH_PREFIX = "PowerAuth ";
protected Map<String, String> parseHttpHeader(String header) {
// Check if the header is null
if (header == null) {
return new HashMap<>();
}
// Trim the trailing white characters
header = header.trim();
// Check if the header has the right prefix
if (!header.startsWith(POWERAUTH_PREFIX)) {
return new HashMap<>();
}
// Trim the key-value pairs from the header
header = header.substring(POWERAUTH_PREFIX.length()).trim();
// Compile the reqexp and parse the key / value pairs
Map<String, String> result = new HashMap<>();
Pattern p = Pattern.compile("(\\w+)=\"*((?<=\")[^\"]+(?=\")|([^\\s]+)),*\"*");
Matcher m = p.matcher(header);
while (m.find()) {
result.put(m.group(1), m.group(2));
}
// Return the map with key-value pairs that you can fetch later
return result;
}
Then we have a convenience PowerAuthSignatureHttpHeader
subclass (link) for the signature header type that only does the more convenient (and typed) value extraction.
Note that every signature header must contain the following values:
pa_activation_id
- Activation ID.pa_application_key
- Application version ID.pa_nonce
- Cryptographic nonce (Base64 encoded).pa_signature
- The signature value.pa_signature_type
- The signature type.pa_version
- Algorithm version.
You should validate them just to make sure they contain structurally correct values. We use a PowerAuthSignatureHttpHeaderValidator
class (link) for that. The validation logic is longer (not suitable for copy pasting here) but very straightforward.
Again, if parsing of the HTTP header fails, or the header does not contain some of the required values, or the header value validation fails, you should stop the processing and return HTTP 401
.
Obtaining the Request Data
In the case the HTTP request uses any other HTTP method than GET
, this task is easy: Just take the HTTP request body, byte-by-byte (make sure to use UTF-8 encoding for any conversions that you might need).
For GET
requests, this gets a bit more tricky, since GET
requests usually do not have any request body. While this may not be a problem in some cases, we include query parameters in the request data in case they are present. To achieve precisely the same value of request data on the server and client, we apply a canonization algorithm to the query attributes:
- Sort the query attribute by attribute name.
- If there are multiple query attributes with the same name, sort the query attributes also by the value as a secondary criterion.
- Create a new query string with ordered query attributes.
For example, we change key_b=value_b&key_b=value_a&key_a=value_a
to key_a=value_a&key_b=value_a&key_b=value_b
.
We have a special PowerAuthRequestCanonizationUtils
class (link) for this task. The code is a bit too long and boring for a copy-paste to this tutorial. If you need to handle signed GET
requests, have a look at our implementation.
Building the Signature Base String
Now, when we have the request data and parsed HTTP header, we may proceed to build the signature base string. This is the string value that was actually signed on the mobile device.
Building the signature base string is simple once we have all the data we talked about earlier in place. We use our own PowerAuthHttpBody
class (link) to do the most of the logic.
The important Java code is the following:
public static String getSignatureBaseString(String httpMethod, String requestUri, byte[] nonce, byte[] data) {
String requestUriHash = "";
if (requestUri != null) {
byte[] bytes = requestUri.getBytes(StandardCharsets.UTF_8);
requestUriHash = BaseEncoding.base64().encode(bytes);
}
String dataBase64 = "";
if (data != null) {
dataBase64 = BaseEncoding.base64().encode(data);
}
String nonceBase64 = "";
if (nonce != null) {
nonceBase64 = BaseEncoding.base64().encode(nonce);
}
return (httpMethod != null ? httpMethod.toUpperCase() : "GET")
+ "&" + requestUriHash
+ "&" + nonceBase64
+ "&" + dataBase64;
}
To call the method, you need to know:
- HTTP request method - Comes with the HTTP request.
- The “request URI” value - The request URI ID, a constant that we talked about earlier and that the client and server need to share beforehand.
- Note: This is not the absolute HTTP URI path, only a constant that usually resembles it, such as
/login
.
- Note: This is not the absolute HTTP URI path, only a constant that usually resembles it, such as
- The “nonce” - Comes in the HTTP header.
- Note: The nonce value should be Base64 encoded only once. In our code, we receive nonce in the HTTP header as a Base64 encoded string, we decode it to the bytes, and when building a signature base string, we encode it to Base64 again. While this looks like extra work, at least we can validate the correct format of the value as well as the sufficient length of the underlying
byte[]
.
- Note: The nonce value should be Base64 encoded only once. In our code, we receive nonce in the HTTP header as a Base64 encoded string, we decode it to the bytes, and when building a signature base string, we encode it to Base64 again. While this looks like extra work, at least we can validate the correct format of the value as well as the sufficient length of the underlying
- The request data - We obtained those in the section above.
The result should look something like this:
POST&L29wZXJhdGlvbi9hdXRob3JpemU=&j1MADdlwDmN3ZV7cFt74Qg==&eyJyZXF1ZXN0T2JqZWN0I
jp7ImlkIjoiNzBkMDM5MjktNmZkZC00MzE1LTk1NzQtYzk3ZGM2ZDU2YWJhIiwiZGF0YSI6IkEyIn19&
Ec1RlAr6B3Il6wEg9OQLXA==
At this moment, we should have everything to call the PowerAuth Server API.
Verifying the Signatures
This is the moment of truth. After your API server receives a signed HTTP request from the mobile app, you need to call the PowerAuth Server API to check that the signature is correct.
You can use the following RESTful API for that:
POST /rest/v3/signature/verify
{
"requestObject": {
"activationId": "string",
"applicationKey": "string",
"data": "string",
"signature": "string",
"signatureType": "ENUM_SIGNATURE_TYPE",
"signatureVersion": "string"
}
}
Request Body Structure
Parameter | Description | Obtained at |
---|---|---|
activationId |
ID of the activation. | Comes in the signature HTTP header. |
applicationKey |
Key specific for an application version. | Comes in the signature HTTP header. |
data |
Signature base string. | We computed this value during the process. |
signature |
Signature value. | Comes in the signature HTTP header. |
signatureType |
Signature type. | Comes in the signature HTTP header. Note: The PowerAuth Service uses enum values in upper-case, while the HTTP header comes in lower-case format. The values are otherwise the same. |
signatureVersion |
Signature version. | Comes in the signature HTTP header. |
You will receive the following response:
{
"status": "string",
"responseObject": {
"signatureValid": true,
"activationId": "string",
"activationStatus": "ENUM_ACTIVATION_STATUS",
"userId": "string",
"applicationId": 0,
"blockedReason": "string",
"remainingAttempts": 0,
"signatureType": "ENUM_SIGNATURE_TYPE"
}
}
Response Body Structure
Parameter | Description |
---|---|
status |
Status of the request processing, OK if the HTTP request executes correctly, ERROR if an error occurs. |
responseObject.signatureValid |
Boolean indicating if the signature was verified correctly or if the signature was invalid. |
responseObject.activationId |
Activation ID of the activation used to verify the signature. |
responseObject.activationStatus |
Status of the activation during the verification attempt (ACTIVE , BLOCKED , REMOVED , unless you made an implementation error, you should not obtain CREATED or PENDING_COMMIT values.) |
responseObject.userId |
User ID of the user who is associated with this activation. |
responseObject.applicationId |
Application ID of an application that was used to compute the signature. |
responseObject.blockedReason |
In case the activation is blocked, this value provides more information about the cause. It can be any value provided while calling the service that blocks the activation, or a MAX_FAILED_ATTEMPTS value in case the activation was blocked due to too many authentication failed attempts. |
responseObject.remainingAttempts |
Number of the remaining authentication attempts. |
responseObject.signatureType |
Used signature type. |
Note: We also have a SOAP service. If you like this better, here is the documentation, including the link to WSDL definitions.
The first thing you must do is to check the responseObject.signatureValid
value. If the value is false
, the signature verification failed and you should return HTTP 401
in the response to the mobile client.
If the signature verification was successful, you can now work with the remaining response values in any way your business logic requires.
For example, you can:
- Obtain the user ID to create an authenticated session or issue an access token in case of a login endpoint, or submit a payment while checking the user is allowed to do so in the case of a payment endpoint.
- Check if the used signature type can be used for the given operation (for example, to disable biometry for payments above some value).
- Check that the application is allowed to perform the operation (for example, your mobile banking app may be able to call all endpoints, while a mobile brokerage app could be restricted just to some endpoints).
Summary
We successfully guided you through the signature validation process, step by step. You should now be able to implement signature validation in any system you currently have. While the process is not as straightforward in the case of the Spring framework, it is still manageable, and of course, we are always here to help.