Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions README_device_attestation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
Device attestation
===
The Device Attestation SDK creates an _attestation_—a message signed with the Clover device private key. This is used to assert a set of truths about the device or the system state, such that the integrity and authenticity of the message are cryptographically guaranteed by the Clover hardware.

# Create a signed message
```
val userTruths = mapOf(
"nonce" to theNonce,
"accountId" to theAccountId,
...,
)

// Generate with certificate chain in JWS header
val response = DeviceAttestationClient(context).sign(userTruths, CertificateReference.CERTIFICATE)
// OR
// Generate with SHA-256 certificate thumbprint reference
val response = DeviceAttestationClient(context).sign(userTruths, CertificateReference.THUMBPRINT)
```
`message` is a [Java Web Signature](https://datatracker.ietf.org/doc/html/rfc7515) (JWS) [compact serialization](https://datatracker.ietf.org/doc/html/rfc7515#page-7). JWS is fairly simple, but if you are not comfortable with the specification, there are multiple robust client libraries:
- **Node.js / TypeScript**: jose (https://www.npmjs.com/package/jose) or jsonwebtoken (https://www.npmjs.com/package/jsonwebtoken)
- **Python**: PyJWT (https://pyjwt.readthedocs.io/) or authlib (https://authlib.org/)
- **Java / Kotlin**: nimbus-jose-jwt (https://connect2id.com/products/nimbus-jose-jwt) or okta-jwt-verifier (https://github.com/okta/okta-jwt-verifier-java)
- **Go**: go-jose (https://github.com/go-jose/go-jose)
- **C# / .NET**: System.IdentityModel.Tokens.Jwt (https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/)

The following sections describe what to expect in a Clover device attestation JWS compact serialization. Refer to the JWS specification for details.

## Header
The JWS header contains _either_:
- `x5c`: The Clover device intermediate certificate chain (default, or when `CertificateReference.CERTIFICATE` is used in signing).
- `x5t#S256`: The base64, url-encoded SHA-256 certificate thumbprint reference (when `CertificateReference.THUMBPRINT` is used in signing).

When invoking `DeviceAttestationClient.sign`, an optional `CertificateReference` can be supplied. This is either:
- `CertificateReference.CERTIFICATE`: The entire intermediate certificate chain is encoded in the JWS header, in the `x5c` field.
- `CertificateReference.THUMBPRINT`: A SHA-256 hash of the leaf certificate is encoded in the JWS header, in the `x5t#256` field.

`CERTIFICATE` generates a larger message (roughly ~5k bytes, plus encoded payload length), but it is completely self-contained, and can be verified completely offline.

`THUMBPRINT` generates a smaller message (roughly 800 bytes, plus encoded payload length). However, the verifier must obtain, or otherwise have access to, the intermediate certificate chain. The verifier must verify that the thumbprint (hash) in the JWS header matches the hash of the actual SHA-256 leaf certificate (in addition to verifying the signature, and validating the certificate chain).

## Payload
JWS does not define a payload format. Clover uses a well-defined JSON object that defines *device* and *user truths*.
```
{
"deviceTruths": {
"timestamp": "2026-06-16T21:12:36Z",
"clover_id": "GARENZRPEFZ6E",
"serial": "C051UQ03660028",
"mid": "12345678901",
"is_prod": "false"
},
"userTruths": {
"nonce": "c2519b9f-a8bd-459d-bcda-4fba2a014d65",
"accountId": "16433789684660082549"
}
}
```

### Device truths
Device truths are attestations made about the signing device, by the signing device.

- `timestamp`: The time the attestation was signed.
- `clover_id`: The Clover merchant UUID.
- `serial`: The Clover device serial number.
- `mid`: The merchant identifier (must not be confused with the merchant UUID).
- `is_prod`: A flag indicating if the message was signed on a production, or otherwise development, Clover device. This must be used to select the correct device root, to complete message verification.

The set of device truths is fixed; these same device truth keys exist in all signed messages.

### User truths
User truths are attestations made by the caller about the device's relationship with their software. While not required, it is recommended that this at least contain a server-generated [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) that can be authenticated at verification time. It might also contain things like user or account IDs.

## Signature
Per the JWS specification, the signature signs the header and payload.

# Verify a signed message
To verify a message:
1. Decode the JWS compact serialization.
2. Verify the cryptographic signature.
3. Verify that the certificate chain chains to either the development or production device Clover root certificate.

If `THUMBPRINT` mode is used, the verifier must first retrieve and match the intermediate certificate chain before performing signature and path validation.

A Kotlin and Python samples demonstrating verification is provided. See "Samples" below.

The device production and development root certificate, in PEM format, can be found at:
- `certs/device_root_prod.pem` (use with production Clover devices)
- `certs/device_root_dev.pem` (use with development Clover devices)
- `certs/device_root_dev_legacy.pem` (use with older development Clover devices)

Because there are two development root certificates, you may have to write your code to attempt verification with each.

respectively.

> [!IMPORTANT]
> Some older, primarily development devices, may have expired intermediate certificates. This does not affect the operation of the device, or the authenticity of the signature.

# Rate limiting
Callers are limited to 24 signing requests per day (24 invocations of `DeviceAttestationClient.sign`).

# Samples
To demonstrate message signing and verification, run the Clover Android SDK Examples application, and select "Device attestation test". Find the functions `DeviceAttestationViewModel::sign` and `::verify`. Of course the signer and verifier will never be the same entity, outside sample code.

Two python samples can be found in `clover-android-sdk-examples/scripts/`:
- `verify_device_attestation.py`: Manual parse and decode the JWS compact serialization, and manual cryptographic verification.
- `jwt_verify_device_attestation.py`: Uses the [PyJWT](https://pyjwt.readthedocs.io/en/stable/) Python module to do the same.

To use them, first generate a signed message using the "Device attestation test" screen in the Clover Android SDK Examples app on the device. In the device's log, look for messages like:
```
06-16 15:41:14.030 9492 9492 I device_attestation: Wrote attestation: /storage/emulated/0/Android/data/com.clover.android.sdk.examples/cache/attestation.jws
06-16 15:41:14.035 9492 9492 I device_attestation: Wrote device certificates: /storage/emulated/0/Android/data/com.clover.android.sdk.examples/cache/device_certs.pem
```
Pull these files from the device to your host PC:
```
adb pull /storage/emulated/0/Android/data/com.clover.android.sdk.examples/cache/attestation.jws
adb pull /storage/emulated/0/Android/data/com.clover.android.sdk.examples/cache/device_certs.pem
```
and run either Python script to verify it.

For messages generated with a `CERTIFICATE` certificate reference:
```
clover-android-sdk-examples/scripts/verify_device_attestation.py --jws attestation.jws --root device_root.pem
# OR
clover-android-sdk-examples/scripts/jwt_verify_device_attestation.py --jws attestation.jws --root device_root.pem
```

For messages generated with a `THUMBPRINT` certificate reference:
```
clover-android-sdk-examples/scripts/verify_device_attestation.py --jws attestation.jws --root device_root.pem --certs device_certs.pem
# OR
clover-android-sdk-examples/scripts/jwt_verify_device_attestation.py --jws attestation.jws --root device_root.pem --certs device_certs.pem
```
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ subprojects {
remoteLineSuffix.set("#L")
}
externalDocumentationLink {
url.set(new URL("https://square.github.io/retrofit/2.x/retrofit/"))
url.set(new URL("https://javadoc.io/doc/com.squareup.retrofit2/retrofit/2.12.0/"))
}
externalDocumentationLink {
url.set(new URL("https://square.github.io/okhttp/3.x/okhttp/"))
url.set(new URL("https://javadoc.io/doc/com.squareup.okhttp3/okhttp/3.12.13/"))
}
if(project.name == "clover-android-sdk") includes.from("overview.md")

perPackageOption {
matchingRegex.set("com.clover.sdk.internal.*")
matchingRegex.set('com\\.clover\\.sdk(\\..*)?\\.internal(\\..*)?')
suppress.set(true)
}
}
Expand Down
21 changes: 21 additions & 0 deletions certs/device_root_dev.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDgzCCAmugAwIBAgIQFG4Y+mStDpqR+MKFT1A89zANBgkqhkiG9w0BAQsFADBa
MQswCQYDVQQGEwJVUzERMA8GA1UECBMITmVicmFza2ExEzARBgNVBAoTCkZpcnN0
IERhdGExIzAhBgNVBAMTGlRFU1QgQ2xvdmVyIERldmljZSBSb290IENBMB4XDTI2
MDEyNzAzNDYzOFoXDTM2MDEyNTAzNDYzOFowWjELMAkGA1UEBhMCVVMxETAPBgNV
BAgTCE5lYnJhc2thMRMwEQYDVQQKEwpGaXJzdCBEYXRhMSMwIQYDVQQDExpURVNU
IENsb3ZlciBEZXZpY2UgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAONeQHDe10665RPPHCw2r6QOZ0a3/hXShL4ghORIc5KvDyZRNHQHw2Mn
sPFa1QDUgrwkKqY9+bC/Nnv5Or5XSIfg9oTv3r7xteG16zB53DCtVd6ZDoMq3SIZ
Hk+ExebJpcMtujPjKDZU1917AIlsXXqdJ4ggYDRsMC8tKPVDSFQ62BnVbIr+NSdL
j0dsLbvOsqKPNbrw1BGTF3ySUZjwBSEKwV2TdRfcMSEHJCvHDRhTVHqs941DDfSz
/kNT3fEhK22oBabZglEfsA5bY3GZT0kZKXtX8SQd1TcWYm+Npauqk9p1BGaLN58Y
JRNXfy28DO/ddtWGuo9qbUPvEUyy+MkCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEG
MBIGA1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFOC+VmSVJQmJu54y0kMmzEhH
CA5KMA0GCSqGSIb3DQEBCwUAA4IBAQCJvYhisklkVFM3Kpa49h6ZfwypbO+UIYjS
JFlre8reH1fklsVEdTa4eXh86lYUpDEABwThH/WcPtr6ExZfJr0V19IKkXkW2VhV
aazviQIjrIa3T8KD7pDx4voJGOeb2MwxtM6vHm4WEsFWaWa+2EMMYhfCNITBi89Z
6qlqTLFrtvjEWgNlJ1H/X/3tFvdIsXREKaCQ44YGCZbsMT1QoBWARMrYbPj26cTc
jYk5mQ/zVckgtJWZVASQGZN3U2J7wWXupn3yfhJk3CzQg7WEr+rCXzIRDWBvfdv4
IEs40frtmvZevceJFBOIrvlkEelLjf9BRtCznascQC30epcoRLsY
-----END CERTIFICATE-----
20 changes: 20 additions & 0 deletions certs/device_root_dev_legacy.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSDCCAjCgAwIBAgIRAJqJRWb9mB9jhGX62xXHf3wwDQYJKoZIhvcNAQELBQAw
VTEeMBwGA1UEAxMVQ2xvdmVyIERldmljZSBSb290IENBMQswCQYDVQQGEwJVUzER
MA8GA1UECBMITmVicmFza2ExEzARBgNVBAoTCkZpcnN0IERhdGEwHhcNMTQwOTIz
MjI1NTA3WhcNMjQwOTIwMjI1NTA3WjBVMR4wHAYDVQQDExVDbG92ZXIgRGV2aWNl
IFJvb3QgQ0ExCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhOZWJyYXNrYTETMBEGA1UE
ChMKRmlyc3QgRGF0YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJJQ
I+gaIp7ibneZOhKYlPkHWl+4KkK1NL0TULP11I/Irj2vy+UBS6gSKWN3joRc8WNf
+TvE6Atje/NcW3vzLhBbpdWK1hE/cxgckYThiK4ezepwo7p3LF4SK4nsBPb0Rw3I
vb6CbTGvPH7VdHfK1rsxxI3teX1m/F6NFYJkZv5+l8Tk5vYV0Vlq5rinOiZD/FvP
be1Z+VCd+6IKa0yrTfZmCo7Rz4kmtf9nJc5k+9njfNw9A0qCdBskD6HwivHGARCW
0zSBOTzRAkbMoony/FEMawdFJGkH5fKMP83Ugg4UD6+ue2H/06G+fTW4EPM0reuo
y/Ixhe0smtxbY91mjs0CAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
9w0BAQsFAAOCAQEAAW7asNL8+j1MbDJc5YhZK5znkrXE25/UxyYSO+xJEoaBbLba
9nz4VwsA1zQkSHKJF96MFssf7UajgWz2Mo/JbPFl6gCGgoW2fydOxEyffYKi38Fr
quCp7jqbUoREssZUp07uALKswbhhCtAwm7MfQ/Y/Se6wSYpwtRvun5y2kBruoeku
lWflekfsUHtmzLufWwlysCxJ7hsxaTLDmnpfn7h9PYYWT2kHxmnFl+Rwi4GQocVN
aPWmHIrV8Xh/OrF43EZ6Vm9uyF0nDM8eQSTYINqyrTqnJ2nWfjcCe0Bclgn8+0Db
vyWOxMJ0Zqv7S6Zd5oxfDMykzrStPYKsU2v/bA==
-----END CERTIFICATE-----
24 changes: 24 additions & 0 deletions certs/device_root_prod.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEATCCAumgAwIBAgIHTgP8AAAAaTANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UE
BhMCVVMxETAPBgNVBAgMCE5lYnJhc2thMQ4wDAYDVQQHDAVPbWFoYTEfMB0GA1UE
CgwWRmlyc3QgRGF0YSBDb3Jwb3JhdGlvbjEdMBsGA1UECwwUSW5mb3JtYXRpb24g
U2VjdXJpdHkxGjAYBgNVBAMMEUZEIERldmljZSBSb290IENBMSkwJwYJKoZIhvcN
AQkBFhpwa2ktc2VydmljZXNAZmlyc3RkYXRhLmNvbTAeFw0xNDEwMTQwMDAwMDBa
Fw0zNDEwMTQwMDAwMDBaMIG3MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmVicmFz
a2ExDjAMBgNVBAcMBU9tYWhhMR8wHQYDVQQKDBZGaXJzdCBEYXRhIENvcnBvcmF0
aW9uMR0wGwYDVQQLDBRJbmZvcm1hdGlvbiBTZWN1cml0eTEaMBgGA1UEAwwRRkQg
RGV2aWNlIFJvb3QgQ0ExKTAnBgkqhkiG9w0BCQEWGnBraS1zZXJ2aWNlc0BmaXJz
dGRhdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmp7eyQ1
Buvu01Q9Rw7IoAQBymerTV5S9Il+6xU5S0CEZrZhX9RqWMBOj9RaCketH2OjXxfW
w+cyjwDynY04aUSaIAgJARsiQH4Jdg6ctzi/kVXiJzD1MojWQs0qKlgyFL9vfWdQ
os8B4h7UCstF2bh46N76zQY1NCkAs+2Sz6ds87LvqUSXo8EdBAoErPJmEOFoKaqc
IK3v1bOsimoZ/hFW7Z2oDXRkq7Gii3qRIenMMhk1LFPhTQd56kFeQhJIg8Vc7fLU
isARd0jiOSdHZJGX8pLRhyveluQAzNIJWyIRZYWRa51A45l0OYm93h6ybmR7rxQ2
nEIqEJQgBz/b8wIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4IBAQCk3w/JnubLwHxhIqJuhhefzkGXciI4w33Xl/ZFyGCmhDjIik5FJJ+mQO6E
7Cjuatq7928MeaZv+v6IhPQbzkf9tfQyIBgUSVqsPx2Vz1M6HmbHLAGn2XV9rOU3
5aEM0Hliep1XpQpo6v+0sB6a4Nu8dSZL/gGFnpwOp6qasT5G584UsWaFuCWxVQ1P
GG8SzLHFpPjhpIOyZAQaKQpFFMW+WXBV6xUz/s1t5kpiPkVeuBS3iVBaeWimW4ej
wj2w40RNHJXk4BYQZ6ZKiMGrrc/xUbgmYMTlmhQu/w5CzceqRzQWQAFcM0zyB26H
hloCVJz1SdPsAssBTlKnp0iStBww
-----END CERTIFICATE-----
2 changes: 1 addition & 1 deletion clover-android-connector-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
* limitations under the License.
*/
group = 'com.clover.sdk'
version = '334'
version = '340'


apply from: file("${project.rootDir}/lib.gradle")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Manuelle Transaktion</string>
<string name="authorization_line_item_name">Autorisierung</string>
</resources>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Manual Transaction</string>
<string name="authorization_line_item_name">Authorization</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Manual Transaction</string>
<string name="authorization_line_item_name">Authorisation</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Manual Transaction</string>
<string name="authorization_line_item_name">Authorisation</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="default_manual_line_item_name">Transaction manuelle</string>
<string name="authorization_line_item_name">Autorisation</string>
</resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Transaction manuelle</string>
<string name="authorization_line_item_name">Autorisation</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">直接入力決済</string>
<string name="authorization_line_item_name">オーソリ</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Handmatige transactie</string>
<string name="authorization_line_item_name">Autorisatie</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Transação manual</string>
<string name="authorization_line_item_name">Autorização</string>
</resources>
</resources>
5 changes: 3 additions & 2 deletions clover-android-connector-sdk/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
~ limitations under the License.
-->

<!-- smartling.instruction_attributes = comment -->
<resources>
<string name="sdkName">com.clover.connector.sdk.v3.PaymentConnector</string>
<string name="sdkVersion">3.0.0</string>
<string name="default_manual_line_item_name">Manual Transaction</string>
<string name="authorization_line_item_name">Authorization</string></resources>
<string name="default_manual_line_item_name" comment="Location: CreateOrUpdateOrderTask.java. Purpose: Default name for a line item in a manually created order.">Manual Transaction</string>
<string name="authorization_line_item_name" comment="Location: CreateOrUpdateOrderTask.java. Purpose: The line item name used when the transaction type is an authorization, for web receipt generation.">Authorization</string></resources>
2 changes: 1 addition & 1 deletion clover-android-loyalty-kit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
* limitations under the License.
*/
group = 'com.clover.sdk'
version = '334'
version = '340'


apply from: file("${project.rootDir}/lib.gradle")
Expand Down
3 changes: 2 additions & 1 deletion clover-android-sdk-examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
* limitations under the License.
*/
group = 'com.clover.sdk'
version = '334'
version = '340'

apply from: file("${project.rootDir}/app.gradle")
apply plugin: 'kotlin-android'
Expand Down Expand Up @@ -92,6 +92,7 @@ dependencies {
implementation "androidx.activity:activity-ktx:$ANDROIDX_ACTIVITY_VERSION"
implementation "com.google.code.gson:gson:$GSON_VERSION"
implementation "androidx.constraintlayout:constraintlayout:$ANDROIDX_CONSTRAINTLAYOUT_VERSION"
implementation "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"

def room_version = "2.6.1"
implementation("androidx.room:room-common:$room_version")
Expand Down
Loading