Computing and Validating Signatures

While PowerAuth can be used for signing any type of data, the main objective of the protocol is to allow signing of HTTP requests sent to the server in order to prove consistency, authenticity and integrity (CIA) of the data that were sent in the request.

In practical deployment, Intermediate Server Application is responsible for building the normalized data for the purpose of computing the signature and passing it to PowerAuth Server, since it knows details about the networking operation (for example, it knows what endpoint is being signed, what HTTP method it uses, etc.). PowerAuth Server can then just simply accept any data and signature and perform signature validation. In an ideal world, PowerAuth Server should know nothing about the business domain it is used in.

Normalized Data for HTTP Requests

Normalized data to be signed are built using the following procedure:

REQUEST_DATA = ${REQUEST_METHOD}&${REQUEST_URI_IDENTIFIER}&${NONCE}&${REQUEST_BODY}
DATA = ${REQUEST_DATA}&${APPLICATION_SECRET}

Note that the APPLICATION_SECRET is technically outside the request data and is appended after the REQUEST_DATA normalization. This is because Intermediate Server Application does not know the APPLICATION_SECRET but must be able to forward normalized REQUEST_DATA to the PowerAuth Server.

The components of the string above are the following:

  • ${REQUEST_METHOD} - HTTP method written in upper-case, such as GET or POST.
  • ${REQUEST_URI_IDENTIFIER} - identifier of given URI of the resource encoded as Base64 with UTF-8 encoding, for example Base64.encode("/api/payment".getBytes("UTF-8")). The hashed value (in the example before, the /api/payment string) should be uniquely chosen for each URI, but can be of an arbitrary format (if not specified otherwise).
  • ${APPLICATION_SECRET} - An application secret key, used to bind an application identification in the signature explicitly. This value is 16B encoded as Base64 using UTF-8 encoding (see implementation notes).
  • ${NONCE} - Random 16 bytes (suggested length) encoded as Base64 using UTF-8 encoding, serving as a cryptographic nonce.
  • ${REQUEST_BODY} - Request body from the HTTP request
    • In case of request without body (such as GET and DELETE requests), the request data is constructed from the URL query parameters (for example, GET request parameters) in a following way:
      1. Take all URL query parameters as key-value pairs:
        • PARAM[i] = (KEY[i], VALUE[i]), i = 0 .. N
      2. Sort all these key-value pairs according to KEY[i] first, then sort duplicate keys according to the VALUE[i]
      3. Construct data as concatenation of the sorted key-value pairs, key is separated from value using “=” character, individual key-value pairs are separated using “&” character:
        • REQUEST_DATA = BASE64.encode(CONCAT_ALL(CONCAT(KEY[j], VALUE[j], "="), "&", j = 0 .. N)) (let’s assume that j are sorted indexes)
      4. Note: The GET request normalization is inspired by the OAuth 1.0a request normalization.
    • In case of request with body (such as POST and PUT requests), data from the resource body (bytes) are encoded using Base64 with UTF-8 encoding and appended:
      • REQUEST_DATA = BASE64.encode(ByteUtils.getBytes(HTTP['body']))

In case the data for offline signature is being normalized, then the following rules are applied:

  • ${REQUEST_METHOD} is always set to POST.
  • ${APPLICATION_SECRET} is always set to the string constant offline.

Computing the Signature

PowerAuth signature is in principle multi-factor. It uses all keys as defined in the Key Derivation chapter. The signature may include one, two or three factors, therefore achieving 1FA, 2FA or 3FA. In order to determine the type of the signature, following constants are used:

  • 1FA - only a single factor is used
    • possession - Signature uses only possession related key KEY_SIGNATURE_POSSESSION.
    • knowledge - Signature uses only knowledge related key KEY_SIGNATURE_KNOWLEDGE.
    • biometry - Signature uses only biometry related key KEY_SIGNATURE_BIOMETRY.
  • 2FA - possession and one another factor is used
    • possession_knowledge - Signature uses two keys: a possession related key KEY_SIGNATURE_POSSESSION and then knowledge related key KEY_SIGNATURE_KNOWLEDGE.
    • possession_biometry - Signature uses two keys: a possession related key KEY_SIGNATURE_POSSESSION and then biometry related key KEY_SIGNATURE_BIOMETRY.
  • 3FA - all three factors are used
    • possession_knowledge_biometry - Signature uses three keys: a possession related key KEY_SIGNATURE_POSSESSION, then knowledge related key KEY_SIGNATURE_KNOWLEDGE, and finally biometry related key KEY_SIGNATURE_BIOMETRY.

While all values are possible to use, only the values possession, possession_knowledge and possession_biometry are used in any practical setup.

When using more than one factor key, the keys are added additively in the signature algorithm, so that the factors can be validated individually. The resulting PowerAuth signature can be then represented in two different formats:

  1. For online validation, the PowerAuth signature is one Base64 string. The length depends on the number of factors involved in the calculation (16, 32 or 48 bytes encoded in Base64).
  2. For offline validation purposes, the PowerAuth signature is a sequence of one to three numeric strings with 8 digits, each sequence is separated by “-“ character.

Both formats share the same core algorithm to calculate the signature components:

/**
 * Compute the signature components for given data using provided keys
 * and current counter.
 * @param data - data to be signed
 * @param signatureKey - array of symmetric keys used for signature
 * @param CTR_DATA - hash based counter
 */
List<byte[]> computeSignatureComponents(byte[] data, List<SecretKey> signatureKeys, byte[] CTR_DATA) {
    // ... compute signature components
    List<byte[]> signatureComponents = new ArrayList<byte[]>();
    for (int i = 0; i < signatureKeys.size(); i++) {
        byte[] KEY_SIGNATURE = KeyConversion.secretKeyFromBytes(signatureKey.get(0));
        byte[] KEY_DERIVED = Mac.hmacSha256(KEY_SIGNATURE, CTR_DATA);

        // ... compute signature key using more than one keys,
        //     at most 2 extra keys
        // ... this skips the key with index 0 when i == 0
        for (int j = 0; j < i; j++) {
            KEY_SIGNATURE = KeyConversion.secretKeyFromBytes(signatureKey.get(j + 1));
            KEY_DERIVED_CURRENT = Mac.hmacSha256(KEY_SIGNATURE, CTR_DATA);
            KEY_DERIVED = Mac.hmacSha256(KEY_DERIVED_CURRENT, KEY_DERIVED);
        }
        // ... sign the data
        byte[] SIGNATURE_COMPONENT = Mac.hmacSha256(KEY_DERIVED, DATA);
        // ... keep it in the list
        signatureComponents.add(SIGNATURE_COMPONENT);
    }
    return signatureComponents;
}

Signing HTTP Requests

PowerAuth signature for online purposes can be obtained in the following manner:

/**
 * Compute the signature for HTTP request purposes for given data using provided keys and current counter.
 * @param data - data to be signed
 * @param signatureKey - array of symmetric keys used for signature
 * @param CTR_DATA - hash based counter
 */
String computeOnlineSignature(byte[] data, List<SecretKey> signatureKeys, byte[] CTR_DATA) {
    // ... at first, calculate signature components
    List<byte[]> signatureBinaryComponents = computeSignatureComponents(data, signatureKeys, CTR_DATA);  

    // ... now convert components into one Base64 string
    byte[] signatureBytes = new byte[signatureKeys.size() * 16];
    for (int i = 0; i < signatureComponents.size(); i++) {
        byte[] SIGNATURE_COMPONENT = signatureBinaryComponents.get(i);
        // ... append last 16 bytes from SIGNATURE_COMPONENT to signature bytes        
        ByteUtils.copy(SIGNATURE_COMPONENT, 16, signatureBytes, i * 16, 16);
    }
    // ... final conversion to Base64
    return Base64.encode(signatureBytes);
}

PowerAuth Client sends the signature value in the HTTP X-PowerAuth-Authorization header:

X-PowerAuth-Authorization: PowerAuth
	pa_activation_id="7a24c6e9-48e9-43c2-ab4a-aed6270e924d",
	pa_application_key="Z19gyYaW5kb521fYWN0aXZ==",
	pa_nonce="kYjzVBB8Y0ZFabxSWbWovY==",
	pa_signature_type="possession_knowledge"
	pa_signature="MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
	pa_version="3.2"

Offline Signature

Offline signatures are used in case when the mobile device is not connected to the internet. The computation of such signature is similar to signing HTTP requests, but the final string is more human readable and can be easily manually retyped. Also, some of the attributes that would otherwise be present in the HTTP header need to have static value:

Parameter Value
REQUEST_METHOD POST (recommended)
REQUEST_URI_IDENTIFIER /operation/authorize/offline (recommended)
APPLICATION_KEY not used
APPLICATION_SECRET offline

The following algorithm produces a short decimal signature value:

/**
 * Compute the offline signature for given data using provided keys
 * and current counter.
 * @param data - data to be signed
 * @param signatureKey - array of symmetric keys used for signature
 * @param CTR_DATA - hash based counter
 */
String computeOfflineSignature(byte[] data, List<SecretKey> signatureKeys, byte[] CTR_DATA) {
    // ... at first, calculate signature components
    List<byte[]> signatureBinaryComponents = computeSignatureComponents(data, signatureKeys, CTR_DATA);

	// ... compute signature components
	String[] signatureComponents = new String[signatureKeys.size()];
    for (int i = 0; i < signatureComponents.size(); i++) {
        byte[] SIGNATURE_COMPONENT = signatureBinaryComponents.get(i);
        // ... decimalize the signature component
        int decimalized = (ByteUtils.getInt(ByteUtils.truncate(SIGNATURE_COMPONENT, 4)) & 0x7FFFFFFF) % Math.pow(10,8);
        signatureStrings[i] = String.valueOf(decimalized);
	}
    // ... join the signature component using "-" character.
    return String.join("-", signatureComponents);
}

PowerAuth Client displays the signature on the screen and then the user has to manually retype that string into another PowerAuth powered application (e.g. typically to web application, connected to PowerAuth Server). You can read more about the offline signatures in PowerAuth Server’s Offline Signatures documentation.

Validating the Signature

Constants and variables

The following constants and variables are involved in the signature validation:

  • CTR, signature counter
    • Is a representation of logical time. Each parts in the scheme (client and server) increments the counter independently.
    • Note: In protocols version 3 and above, the counter has only informational value and is no longer involved in the signature calculation. In protocol version 2, the counter was directly involved in the signature calculation.
  • CTR_DATA, hash based signature counter
    • Introduced in the protocol version 3, now is used in the signature calculation.
    • It’s randomly initialized and exchanged during the activation, or in the protocol upgrade process.
    • In protocol version 3.1+, the client can synchronize its counter with the server.
  • CTR_LOOK_AHEAD, tolerance set on server to overcome ahead clients
    • Server is trying to calculate and validate the signature ahead in time, in half-closed interval defined by this tolerance: [CTR, CTR + CTR_LOOK_AHEAD).
    • Default value is 20
  • FAILED_ATTEMPTS, how many attempts failed before in row
    • Initial value is 0.
    • If value reaches value defined in MAX_FAILED_ATTEMPTS, then activation is set to BLOCKED state.
    • Value is increased in case that signature validation fails (see description below)
  • MAX_FAILED_ATTEMPTS, how many maximum failed attempts in a row result in blocked activation.
    • If FAILED_ATTEMPTS reaches this value, then activation is set to BLOCKED state.

Algorithm

PowerAuth Server validates the signature using the following mechanism:

  1. Find the activation record using activation ID.
  2. Check the record state. If it is other than ACTIVE, or if a declared application version is unsupported, terminate the validation and report error.
  3. Obtain KEY_SERVER_PRIV and KEY_DEVICE_PUB from the record.
  4. Compute KEY_MASTER_SECRET.
    • KEY_MASTER_SECRET = ByteUtils.convert32Bto16B(ECDH.phase(KEY_SERVER_PRIV, KEY_DEVICE_PUB))
  5. Compute required signature keys (KEY_SIGNATURE_POSSESSION, KEY_SIGNATURE_KNOWLEDGE or KEY_SIGNATURE_BIOMETRY).
  6. Compute the expected signature for obtained data and check if the expected signature matches the one sent with the client. Since the PowerAuth Client may be ahead with counter from PowerAuth Server, server should try couple extra indexes ahead:
boolean verifySignature(byte[] data, List<SecretKey> signatureKeys, int CTR, byte[] CTR_DATA, int CTR_LOOK_AHEAD) {
    boolean verified = false;
    byte[] CTR_DATA_ITER = CTR_DATA;
    for (int CTR_ITER = CTR; CTR_ITER++; CTR_ITER < CRT + CTR_LOOK_AHEAD) {
        //... compute signature for given CTR_DATA_ITER, data and
        //    signature keys (see the algorithm above)
        String SIGNATURE = computeSignature(data, signatureKeys, CTR_DATA_ITER);
        if (SIGNATURE.equals(SIGNATURE_PROVIDED) && !VERIFIED) {
            verified = true;
            CTR_DATA = CTR_DATA_ITER; // ... also, persist CTR_DATA
            break;
        }
        // Move to the next hash-based counter's value
        CTR_DATA_ITER = ByteUtils.convert32Bto16B(Hash.sha256(CTR_DATA_ITER));
    }
    return verified;
}

Success

In case that signature is successfully verified, then:

  • If signature is of a type possession, do NOT reset FAILED_ATTEMPTS.
  • For other signature types than possession, reset the FAILED_ATTEMPTS to 0.
  • Move signature counter in database forward by setting CTR to CTR_ITER and CTR_DATA to CTR_DATA_ITER.

Failure

In case of failure:

  • Increase the FAILED_ATTEMPTS value by 1.
  • If FAILED_ATTEMPTS is equal or greater than MAX_FAILED_ATTEMPTS, then set activation state to BLOCKED.
Last updated on Aug 23, 2023 (09:12) Edit on Github Send Feedback
Search

1.9.x

PowerAuth Java Crypto