#Apple Attest, Transaction Middleware Failing verify the public Key + Signature Buff on TS Middleware
895 messages ยท Page 1 of 1 (latest)
Im building a startup and im sorta pretty technical but im like actually facepalming over some encryption that I simply cannot get to work.
(for reference its to do with a typescript middleware failing to authenticate transactional requests from apple app attest)
not the middleware designated for apple attest registration, that works fine and registers both apple and android devices in my database with their public keys for continued transactional auth.
The issue is a failure with either the publicKeyPem, or the derSignatureBuffer. And its failing when applied to the crypto.verify().
pidLogger(SIGNATURE_ALGORITHM, dataToVerify, publicKeyPem, derSignatureBuffer);
// d) Verify (Authenticity)
verified = crypto.verify(
SIGNATURE_ALGORITHM,
dataToVerify,
publicKeyPem,
derSignatureBuffer
);
is failing for my apple clients, works fine for my android clients
been back and forth with ever LLM in existence to see if they can solve it, and they just go around in circles trying solutions which inevitably are bad or suggest system re-design for no good reason.
The entire file is quite large, and so is the client side apple implementation, so I'm hesitant to post it all at once as its very large. So for now I'll include only the middleware aspect that is strictly relevant. and the error logging.
im not even sure why its failing
ive done so many different attempts with der conversion and not-der conversion
for a more critical analysis, I'll provide 2 logged outputs from the server, the first is an android client going through crypto.verify() and it passes fine, the second is the apple client.
These logs are of the entered data to crypto.verify()
[2025-12-10T08:04:45.236Z] [PID-1] [appRequest.js] LOG sha256 <Buffer 2d da bf 62 29 a0 f4 1b 6d 29 18 e8 f6 2a c9 21 98 bb 15 aa 82 2f 82 77 98 38 e6 1a 90 f9 2a ba> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuvmMezVWhDKzzzmYd3CNbnvadthImoZ4TduMEy6e3fxa6pgd2LoWvYq3BvPRDtrmzYbKvvmLI8smaslVgSkk4g==
-----END PUBLIC KEY----- <Buffer 30 44 02 20 16 5e da 48 77 8d d2 cb 64 a3 28 f2 22 cf 7e 03 82 c2 d7 25 81 fe 01 4d 7c 8e ab d0 76 35 85 aa 02 20 40 da f2 ad b8 d4 6f 4b 54 d0 6c 4b ... 20 more bytes>
so above is android, and that verified fine.
Beneath is apple, and it got rejected.
[2025-12-10T08:05:23.861Z] [PID-1] [appRequest.js] LOG sha256 <Buffer 90 a9 f9 aa 0f 46 0f 6f 92 78 76 40 59 b3 fb 05 7b a1 7d a8 0e 77 a9 09 30 3e 1b b7 c0 71 af bf> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEessoJLJkPaWnWK3svFjV9x3Q06Vi
kgZqsgMZ1WE+sOFTJ5slz1/Kg27iHEzEWfvzgTATIhRxdDpTgzJ/VkP6WQ==
-----END PUBLIC KEY----- <Buffer 30 45 02 21 00 be b9 85 db 24 f7 9b 2c 3b 98 cf 32 79 99 51 5c 04 ee 89 28 c0 81 aa e4 35 86 21 f8 50 b9 87 f4 02 20 0f 79 78 ac be 62 1e 74 d5 eb 78 ... 21 more bytes>
the logging that triggers those is as follows:
pidLogger(SIGNATURE_ALGORITHM, dataToVerify, publicKeyPem, derSignatureBuffer);
// d) Verify (Authenticity)
verified = crypto.verify(
SIGNATURE_ALGORITHM,
dataToVerify,
publicKeyPem,
derSignatureBuffer
);
i can see the derSignatureBuffer for apple has an extra byte. but apart from that, im so confused. (ive never used crypto.verify()) before.
are you using a proper CryptoKey? and isn't the signature verify(algorithm, key, signature, data)
or is this not subtle crypto
oh you're probably using the older nodejs crypto?
the answer to @outer oyster's question would depend on where that crypto variable comes from (i am guessing you're not using the web APIs though and are instead using node's crypto module)
import crypto from 'crypto';
yeah pretty sure this is old node crypto the argument order matches. DER length is a red-herring
yeah, that's the node-specific thing. node also supports most of the web crypto APIs these days
okay so basically the core issue im having
is that i just cant seem to verify
the dersig
and the publickeypem
and sometimes its the publickey thats failing it i think
keyBodyForPem = formatBase64ForPem(rawKeyBody); why do you do this differently for ios?
its unlikely the crypto.verify routines are broken on one os but not another
because the ios instance isnt base64 i think when it comes through
or is
i cant remember ive switched it so many times
they absolutely are
ill re - run right now
and show you the logging basically
of what comes in on an apple vs android instance
sorry im naff at explaining this its just so messy
its running now
android is just slow to talk to the server
on fundamental difference is the PEM key has a newline break in it on iOS?
should be fine and normal, but its different
would it be possible to use the same public key on both for testing purposes? perhaps the signature is being signed by a different key on iOS
[2025-12-10T17:34:34.993Z] [PID-1] [appRequest.js] WARN Device UDID not found or public key missing: 7889a985-2646-45e3-9c88-ff221117ed78. Triggering re-attestation.
[2025-12-10T17:34:34.998Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 401 | 4.913 ms | UA: UnknownBrowser on Android
[2025-12-10T17:34:38.265Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/regcomms/devicekeyregister | 201 | 652.969 ms | UA: UnknownBrowser on Android
[2025-12-10T17:34:38.568Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: Android | Public Key (Body Start): MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJNiVwhhsCmH1AdOeK94JWuA0MsM0VBf5hri6OhHKqY1TENAcPATT8Yak7itXraQ217KvIG7QqchK2+4XfYuG8A== | UDID: e44d54cf-cc42-47cb-8a3b-c0e1fe163f4a
[2025-12-10T17:34:38.568Z] [PID-1] [appRequest.js] LOG [AndroidHandler] Using string concatenation hashing.
[2025-12-10T17:34:38.568Z] [PID-1] [appRequest.js] LOG [AndroidHandler] Processing signature length: 71
[2025-12-10T17:34:38.568Z] [PID-1] [appRequest.js] LOG sha256 <Buffer 45 d4 ba b4 54 a4 d8 dd 0d 21 ca 75 73 37 de a9 95 76 d1 d0 dd 67 24 05 32 d5 0e 48 f0 27 2b 62> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJNiVwhhsCmH1AdOeK94JWuA0MsM0VBf5hri6OhHKqY1TENAcPATT8Yak7itXraQ217KvIG7QqchK2+4XfYuG8A==
-----END PUBLIC KEY----- <Buffer 30 45 02 20 45 19 06 f2 a0 85 e5 2a 4e 1c 34 4b 83 00 7f 46 49 6d 70 ea da 90 5c da d8 ab 93 18 83 37 c9 4c 02 21 00 d7 f8 24 ec 8b f9 f4 8b 60 3c 90 ... 21 more bytes>
[2025-12-10T17:34:38.569Z] [PID-1] [appRequest.js] LOG [VERIFY] Platform: Android | Hash: 45d4bab454a4d8dd0d21ca757337dea99576d1d0dd67240532d50e48f0272b62 | Result: true
[2025-12-10T17:34:38.575Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 200 | 8.006 ms | UA: UnknownBrowser on Android
that above is an ANDROID connection to the server, you can see the whole endpoint flow
the endpoint hits are listed BENEATH the logic thats run because of them
i dont think so
and you can see from that log ultimately the result is TRUE on the verify
and it sends back a 200 and is happy
you reported for apple above
[2025-12-10T08:05:23.861Z] [PID-1] [appRequest.js] LOG sha256 <Buffer 90 a9 f9 aa 0f 46 0f 6f 92 78 76 40 59 b3 fb 05 7b a1 7d a8 0e 77 a9 09 30 3e 1b b7 c0 71 af bf> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEessoJLJkPaWnWK3svFjV9x3Q06Vi
kgZqsgMZ1WE+sOFTJ5slz1/Kg27iHEzEWfvzgTATIhRxdDpTgzJ/VkP6WQ==
-----END PUBLIC KEY----- <Buffer 30 45 02 21 00 be b9 85 db 24 f7 9b 2c 3b 98 cf 32 79 99 51 5c 04 ee 89 28 c0 81 aa e4 35 86 21 f8 50 b9 87 f4 02 20 0f 79 78 ac be 62 1e 74 d5 eb 78 ... 21 more bytes>
sorry yes then
im so fed up of this problem icl im sorry
yes
your right both have the newline break
both
and that is the apple log
when an apple client hits it
and there, the /alive endpoint returns 401 because the verify fails
android doesn't have the newline break, iOS does.
erm
so somethings different about how the PEM files arrive
{
"_id": "6939af2e86137db0b7b08f7c",
"deviceUDID": "e44d54cf-cc42-47cb-8a3b-c0e1fe163f4a",
"__v": 0,
"createdAt": "2025-12-10T17:34:38.259Z",
"deviceIp": "77.103.192.136",
"lastSeenAt": "2025-12-10T17:34:38.569Z",
"manufacturer": "Google",
"model": "Pixel 3 XL",
"osVersion": "31",
"platform": "android",
"publicKeyPem": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJNiVwhhsCmH1AdOeK94JWuA0MsM0VBf5hri6OhHKqY1TENAcPATT8Yak7itXraQ217KvIG7QqchK2+4XfYuG8A==",
"serverIp": "77.103.192.136",
"CACHED": "MATCH"
},
{
"apple": {
"keyId": "lXWIuev+bjTAfWgD9vmwvP1i04A8HlWNI0Rw+rILiog=",
"challengeNonce": "OBlGHy8JNz4NX5Q6ECE44IHnC2BmXhfSyWFWr_dYwb8"
},
"_id": "6939af9b86137db0b7b08fb1",
"deviceUDID": "773fdef9-623d-4ef7-a87b-b38efa7add2f",
"__v": 0,
"createdAt": "2025-12-10T17:36:27.702Z",
"deviceIp": "77.103.192.136",
"lastSeenAt": "2025-12-10T17:36:27.702Z",
"manufacturer": "Apple",
"model": "iPhone",
"osVersion": "18.7.2",
"platform": "apple",
"publicKeyPem": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVy8yuxOqkRIcIHZz8HKRvPxWdSiEDEPDS1TCcn9N3NOX9pJS0Z1u7wjzzW6ozsV6JchfrRmJC1/KifuYYmRwKQ==",
"serverIp": "77.103.192.136",
"CACHED": "MATCH"
},
server storage of the 2 devices
idc about the exposed ip's honestly
that storage is triggered by the /devicekeyregister
I mean look at your logs
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJNiVwhhsCmH1AdOeK94JWuA0MsM0VBf5hri6OhHKqY1TENAcPATT8Yak7itXraQ217KvIG7QqchK2+4XfYuG8A==
-----END PUBLIC KEY-----
versus on iOS
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVy8yuxOqkRIcIHZz8HKRvPxWdSiE
DEPDS1TCcn9N3NOX9pJS0Z1u7wjzzW6ozsV6JchfrRmJC1/KifuYYmRwKQ==
-----END PUBLIC KEY-----
yes sorry yes
hm
im thinking
that its something to do with base 64 URLSAFE vs non urlsafe
or something along those lines
ive been confused by it for ages today
AH YES
pem doesn't use urlsafe it uses mime, iirc
it's probably not the cause, but it could be?
i dont think it is the cause honestly
wait
let me just check and see why thats even occuring
yeah its odd
because the registraion endpoint saves it in the db without a space
as you see in the json above
but then clearly when it comes back for transactional requests it is being processed / sent or whatever with a gap
const rawKeyBody = deviceRecord.publicKeyPem!.trim().replace(/\s/g, '');
let keyBodyForPem = rawKeyBody; // Initialize with the raw body
if (isIos) {
keyBodyForPem = formatBase64ForPem(rawKeyBody);
pidLogger.log(`[VERIFY DEBUG] iOS PEM FORMATTER APPLIED. First 64 chars: \n${keyBodyForPem.substring(0, 64)}...`);
}
const publicKeyPem = [
'-----BEGIN PUBLIC KEY-----',
keyBodyForPem,
'-----END PUBLIC KEY-----',
].join('\n');
yep i need to remove the apple specific formatting
its clearly resulting in the the key having that linebreak
but i dont think
that its the cause
re-running the server and clients
how is iOS delivering its public key differently to you?
can you hard code the signature and PEM to be the working ones from android?
just to check the verification routines
thats a good idea
oh you need to hard code data too
im deving on windows and a mac mini which i have to switch my inputs for so ill try that in a second.
[2025-12-10T17:47:14.131Z] [PID-1] [appRequest.js] LOG sha256 <Buffer b3 f2 35 4b 9b 2e 43 63 f7 79 16 45 7f 2f 38 f2 92 8d 58 c5 e9 59 e4 9a f6 d3 ba 42 57 07 92 04> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdsOFsb56E2VQ2Q9tScmG6Y402Q5zNS8Q+qCsEc26IpcYnPvaMN91pzpkuvK+xpqCwmuA92DfTPeuYHvrqH4oTw==
-----END PUBLIC KEY----- <Buffer 30 45 02 21 00 99 08 32 35 af 95 8d cf ec 2d 24 3a 8b 09 a5 45 7b 45 b0 58 83 db 31 fc 67 ad 92 56 13 cb fc 9b 02 20 75 3c 47 d0 0a c6 d7 d4 8c 96 ff ... 21 more bytes>
thats now the apple printout before we go into the crypto.verify
this is the bit im also confused abuot
i only just comprehend the entire app attest flow
isn't this a http endpoint, don't you get these from request params?
its so confusing to me though as apple is embedding the public key in the attestation object when it sends it
whereas android is sending it as a request object on the body
at least i think
im just so confused its mind blowing
because that log you see above
with the public key looking the same
is the public key from the database
not the public key thats just come from the client
because the client doesnt send the public key
it sends the signature
and the data to verify
to my understanding
same with android
so this appRequestHandler is running on iOS/Android or is it receiving requests from another client?
What does dumping const deviceRecord = await DeviceManager.get(deviceUDID); look like
is that this?
this appRequestHandler is running on my server, and is middleware on the /alive endpoint, and receives requests from the android and apple clients
yes these are the deviceRecords
so dumping deviceRecord would just dump one of those with the given deviceUDID
and the formatting of the public key that we just made the same as when android hits it, is the formatting of the servers public key for that device
ultimately its something to do with the signature that the clients are sending is not right for apple but right for android
so then if the deviceRecord already has a properly formatted publicKeyPem, why are you doing this
const rawKeyBody = deviceRecord.publicKeyPem!.trim().replace(/\s/g, '');
let keyBodyForPem = rawKeyBody; // Initialize with the raw body
when it goes into the crypto.verify function
i cant remember lowk tbh
it was because
i thought i needed to do things differently for apple i think
but it might not be needed now
for context ive been fighting this issue for 4 days
literally
so ive been through so many different iterations
deviceRecord.publicKeyPem
that should be all i need in terms of keyBodyForPem
i think
and just put the tags around it
so ---BEGIN and end
const rawKeyBody = deviceRecord.publicKeyPem!.trim().replace(/\s/g, '');
let keyBodyForPem = rawKeyBody;
if (isIos) {
keyBodyForPem = formatBase64ForPem(rawKeyBody);
}
I think you can rip this out
alright let me give that a shot
because its already formatted correctly according to this dump
yes but i do need to substitute the publicKeyPem from deviceRecord into the --begin end thing now
directly
as opposed to running that formatBase64 on it
yeah you have to wrap it properly
and all that
testing now on android first to ensure its not broken
and then ill run the apple client
i appreciate its kind of messy having 2 systems on the smae middleware
but im trying to simplify it as much as possible
[2025-12-10T17:57:28.858Z] [PID-1] [appRequest.js] WARN Device UDID not found or public key missing: 554a4b53-fecc-4c1a-87a0-48bc14d05484. Triggering re-attestation.
[2025-12-10T17:57:28.863Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 401 | 4.730 ms | UA: UnknownBrowser on Android
[2025-12-10T17:57:30.869Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/regcomms/devicekeyregister | 201 | 397.977 ms | UA: UnknownBrowser on Android
[2025-12-10T17:57:31.129Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: Android | Public Key (Body Start): MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErW/3Bo8tJa6hbYkUoRHGTe+wOhjB4LuJTVQ0gqpLvKRaO+rmtLs6c2AVOhyZPFsKxcUw+GCGh4Qj/RIS07m96Q== | UDID: 26e818d4-7359-4a1e-a47c-030024ddb19e
[2025-12-10T17:57:31.129Z] [PID-1] [appRequest.js] LOG [AndroidHandler] Using string concatenation hashing.
[2025-12-10T17:57:31.129Z] [PID-1] [appRequest.js] LOG [AndroidHandler] Processing signature length: 70
[2025-12-10T17:57:31.129Z] [PID-1] [appRequest.js] LOG sha256 <Buffer b7 b1 d4 bd ff ba 20 06 58 94 98 13 64 bf a7 44 8e 34 6e fd 0d e3 2e ee 53 7b 2f dc f3 70 0c f6> -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErW/3Bo8tJa6hbYkUoRHGTe+wOhjB4LuJTVQ0gqpLvKRaO+rmtLs6c2AVOhyZPFsKxcUw+GCGh4Qj/RIS07m96Q==
-----END PUBLIC KEY----- <Buffer 30 44 02 20 1c cd ee 2c 42 57 d6 29 05 0b 23 fd 7c 15 1c 22 00 fe 9f b1 6c 66 f1 bf bf 89 df b8 92 f2 32 b2 02 20 47 d1 48 39 c5 37 6f b8 e7 f0 5f 2f ... 20 more bytes>
[2025-12-10T17:57:31.130Z] [PID-1] [appRequest.js] LOG [VERIFY] Platform: Android | Hash: b7b1d4bdffba20065894981364bfa7448e346efd0de32eee537b2fdcf3700cf6 | Result: true
[2025-12-10T17:57:31.138Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 200 | 8.705 ms | UA: UnknownBrowser on Android
android is happy
apple is still not
what is this
Using raw buffer concatenation hashing (URL-Safe to Standard Decode).
this is implemented on the client side, not the server side?
ah found it, reading source code on discord is hard
yeah i know im so sorry man
you guys are genuinely good at helping idk how id do it
id have to request the whole project
and put it in my editor
what's androidHandler look like?
for reference this is the structure
and ill send over androidHandler now
const androidHandler: PlatformHandler = {
/**
* Android client hashes the string: Base64(encryptedPayload) + ":" + Base64(nonce)
*/
getPayloadHash: (encryptedPayload: string, nonce: string): Buffer => {
pidLogger.log('[AndroidHandler] Using string concatenation hashing.');
const combinedData = `${encryptedPayload}:${nonce}`;
return crypto.createHash(SIGNATURE_ALGORITHM).update(combinedData, 'utf-8').digest();
},
/**
* Android client sends raw 64-byte ECDSA (R|S) or pre-DER-encoded signature.
*/
processSignature: (rawSignatureBuffer: Buffer): Buffer | null => {
pidLogger.log(`[AndroidHandler] Processing signature length: ${rawSignatureBuffer.length}`);
if (rawSignatureBuffer.length === 64) {
// Raw R|S, convert to DER
return rawEcdsaSigToDer(rawSignatureBuffer);
} else if (rawSignatureBuffer.length > 64 && rawSignatureBuffer.length < 100 && rawSignatureBuffer[0] === 0x30) {
// Already DER format
return rawSignatureBuffer;
}
return null;
},
};
thats the androidHandler
in appRequest.ts
the transactional middleware
what is the client app? is it something you control?
yes
just trying to figure out why these use different schemes
its possible ive messed up my apple client so its sending slightly bad stuff
because apple loads everything into an attestation object
so you're generating the signature yourself? it's not some OS-level process?
its difficult bcuz im working on 1 monitor and replugging in every time to switch to the mac mini xcode
can you show the code for that
ill get the client side stuff for you
I'd tell you that usb switches are cheap and easy, but that doesn't help you right now
i have one it broke tho
my monitor gets pixels from mac mini through it
so annoying
im gonna switch to the mac mini and copy it across from github
okay right
here is the Server.swift
at least the bit that matters
and is used for ongoing transactional requests
I've had about 90 stabs at this with LLM's
ive worked with swift apps but never had to build server - client apps before
im sorry its all so much
its got pretty high security requirements
let signatureB64 = try await deviceIdManager.sign(dataToSign: dataToSign)
// 6. Build Transport Body
let transportBody = AppRequestBody(
encryptedPayload: encryptedPayloadB64,
nonce: nonceB64,
signature: signatureB64,
deviceUDID: deviceUDID
)
okay
ill get the sign() for you
is there a reason you chose to use a different signing technique for iOS than android?
like why not use the same technique for both
i read through a tutorial its done differently
id love them to be the same
but apparently in apple it should be done differently
func sign(dataToSign: Data) async throws -> String {
// Use generateAssertion to get the signature
let keyId = credentials.publicKeyIdentifier
guard !keyId.isEmpty else { throw KeyManagerError.signingFailed }
// The generateAssertion method returns the **Assertion Object**, which is a signed
// PKCS#7 message containing the ECDSA signature. This is what must be sent.
let assertionData = try await generateAssertion(keyId: keyId, clientDataHash: dataToSign)
// The server is expecting this full object, which corresponds to the 141 bytes length,
// and must have verification logic to extract the raw signature from it.
return assertionData.base64EncodedString()
}
private func generateAssertion(keyId: String, clientDataHash: Data) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: clientDataHash) { assertionData, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let assertion = assertionData else {
continuation.resume(throwing: KeyManagerError.signingFailed)
return
}
continuation.resume(returning: assertion)
}
}
}
and theres the asserton generator
I think you have everything you need to match android
id love to match android
relaly ill explain breifly why its all so complex
im building an auth system which authenticates users based on the fact that they HAVE their device
so like
your device is your password
and so when they loose their private key on their device
or their deviceUDID they loose their account
(intentional)
and because of that premise of binding a device to a device record in my db via a public key system
i was advised that i needed to use this complex attestation thing
going forwards
for the transactional middleware for apple
this is my firs time even using the devicecheck service which just makes it all so much more complex
but to my understanding
the reason we use the DC service every transactional request
is to that the user doesnt register on a phone, and then run off with the public key to a different machine (after intercepting their network requests) and use a PC with that public key
and my server believe because the reqs have that public key signature, that the computer is a phone
because we dont bother checking for assertion
at least thats a high level explanation of what im trying to mitigate
its all a bit foggy deeper in
I'm making sure this'll actually work
im trying to build a system where we can trust the device's gps data (within reason) as a security signal
why do you encrypt the body, isn't signature enough?
i just want to be sure rlly.
its like highly sensitive payload data
that will be being sent
yeah but over https, right?
and i dont want either the USER or an attacker to tamper
yes but a user can tamper it in their network
so we encrypt with a company public key on the devices
with the signature, there's no danger of tampering. The signature wont verify if its been tampered with, whether the body is plaintext or encrypted
and its over https so its not like the data is leaked otherwise. But it doesn't really matter, it just makes it more complex, more places to go wrong
nope. If they modify the body data, then the signature wont verify it, because the signature is built to verify only the exact string pre-tampering
so your saying my company public key encryption part isnt needed? or the other AES ecnryption layer?
the AES encryption layer. You need the public key encryption for signature verification
just to clarify theres 2 public key things going on
I'm saying you could skip
let (encryptedPayloadB64, nonceB64) = try CryptoManager.encrypt(
jsonObject: decryptedPayload,
key: credentials.serverSymmetricKey
)
deviceIdManager.sign(dataToSign: dataToSign) this is the one that really matters
the device generates private key to identify itself and send the public key to ths erver and thats how its identified (with the deviceUDID) and then theres also a company public key which comes with the app and signs all of the requests for my server to unencrypt
but thats the bit that uses my companys public key to sign
is that really not necessary??
hm, maybe I don't understand the flow
im just so mind boggled honestly
been round in loops endlessly
instead of changing the apple flow to fit android, why dont i just like idk debug the signature
and fix the server to work with the signature properly
I was imagining
When a user does initial attestation you get a public key from the device credentials, its unique to that phone and can't be exfiltrated. You store that in your database somewhere
When the app on the phone needs to attest is location, all it needs to do is send a message in plaintext with a signature. You lookup the appropriate public key in your database and verify the message from the phone. Now you know that your app on that exact phone provided the info and nothing else
the user nor attackers can tamper with the message once its been signed, that'd cause verification to fail
but I might be missing part of what you're trying to do
yes thats kinda the flow, except also EVERY message between client and server is encrypted with company public key, and we also use attest on the initial registration to pass that clients public key generated up to the server
and then i think ever message onwards from the client to the server is 1, encrypted with company public key, 2, put in an assertion object thing with a signature or something
except also EVERY message between client and server is encrypted with company public key
that's the part I'm not sure is really necessary. You're already using HTTPS transport, which is also public key encryption controlled by your company.
isnt it needed though to prevent network tampering
because the client can tamper the HTTPS
in their own network
sure the client can tamper with https, but if they modify the message then crypto.verify will reject the message as tampered with
and then they can potentially doctor the data like location data
you're signing the location data too, right?
I assume all that info goes in decryptedPayload
let me check
so location and everything else gets signed by the resident credentials
// 2. Build Decrypted Payload
var decryptedPayload: JSONObject = [
"deviceUDID": deviceUDID,
"accountId": SecureStorage.load(key: KeychainKeys.accountIdKey),
"pushNotifKey": SecureStorage.load(key: KeychainKeys.pushNotificationKey),
"data": data,
"deviceFingerprint": [
"manufacturer": "Apple",
"model": UIDevice.current.model,
"osVersion": UIDevice.current.systemVersion
]
]
yeah it just says "data" could be anything
yeah the locatino payload
should be included
NEXT TO the devicefingerpritn
ive accidentally not included it
because i updated it recently
then you're signing the location payload. If anyone tampers with it after signing, your server will reject the message
but yes it goes in the decryptedPayload
regardless of whether or not you encrypt the body in the first place
HTTPS prevents third-parties from reading any sensitive data contained therein
yeah look its generated from the location service but then ive forgotten to put it in
but yes as you say it should be signed
unless you don't trust your server hosts
we send location data on EVERY request irrespective of the purpose of the request
and middleware on the server does stuff like fraud prevention with that
sure good for logging
gps spoofing is a possible attack, its not a gold standard btw, but it is high effort to do that ๐
yeah ik
its a calculated risk
we do biometric authentication + gps + wifi signals etc
but anyway from what you've described, there's no good reason to encrypt the payload. Signing the message is enough, and you need to do that anyway
to provide a relatively strong identity level
the reason im encrypting so much is because its passing biometrics, passport data, gps, etc.
quite routinely
it saves you from the nasty key distribution problem of: what if the company needs to revoke its public key โ ๏ธ
yes ive been thinking about that alot
lmao
so your saying its completely unneeded?
to use that layer
yeah, I think so. I can't promise I fully understand, though I think I do.
what about on the android client where we arent signing with the attest thing but only the public key
encryption is only useful to prevent spying
(by public key i mean the one the device generates)
we use play integrity too
but its different because im using the google API on the server
you're not using the same kind of resident credentials on android?
yes we are, but we dont like bundle it into some weird app assertion
its just encrypted by the device's private key, and then its unencrypted by the servers public key (from registration of that device)
and it is also encrypted by the company public key
which your saying i should drop
and all that also has the integrity token in it
and then from that point onwards we BLINDLY trust the private key
signing
on the client
we dont require integrity again
yes, you have to do that anyway too?
anyone on android could extract the company public key and use it to encrypt messages no problem
okay so basicaly your saying drop the public key from the company
its unneeded and a security problem
when we have to re-issue
yeah its not necessary and it makes it more complex to both process messages and deal with key distribution. Maybe feed this all into chatgpt or something to validate
ive spoken to chatgpt more than my family the past few days
and it contradicts itself
EVERY TIME
with different advice
so i honestly dont trust it
thats why i find myself here
okay but aside from dropping the public key for the company, what about the fact we still have this issue with the signature being broken or something
on the apple side
I am not a swift expert, I am having changes verified to make sure I am not insane, but I think it can work with the android handler
tysm man
your lowk doing a better job than any AI ive spoken to
of like summarising
so when we receive a req in appRequest.ts middleware... the nonce isnt needed anymore?
const { encryptedPayload, nonce, signature, deviceUDID } = req.body as AppRequestBody;
as the nonce is only for the decryption of the encryptedPayload
no you need the nonce for the signature
getPayloadHash: (encryptedPayload: string, nonce: string): Buffer => {
pidLogger.log('[AndroidHandler] Using string concatenation hashing.');
const combinedData = `${encryptedPayload}:${nonce}`;
return crypto.createHash(SIGNATURE_ALGORITHM).update(combinedData, 'utf-8').digest();
},
if you don't have the nonce you can't properly hash the payload for signature verification
the nonce exists to prevent replay attacks
so im kinda struggling, which bit do i need to remove to remove the company public encryption
because thats handled here:
import { Response, NextFunction } from 'express';
import { AppRequest, AppRequestBody } from '../types/requests';
import { pidLogger } from '../utils/logger';
import crypto from 'crypto';
import { ENV } from '../config/env';
import { IncomingHttpHeaders } from 'http';
import { derToRawSignature, extractEcdsaSignatureFromAssertion, formatBase64ForPem, rawEcdsaSigToDer } from '../utils/crypto';
import { DeviceManager } from '../database/client';
// 1. IMPORT REQUIRED DEVICE MODEL TYPES
import { DeviceBase } from '../types/device';
import { DeviceLocationsModel } from '../database/models/devices/locations';
// Security Constant
const ENCRYPTION_KEY = Buffer.from(ENV.APP_ENCRYPTION_KEY, 'hex');
const SIGNATURE_ALGORITHM = 'sha256';
// --- SHARED CRYPTOGRAPHIC UTILITIES ---
function decryptPayload(encrypted: string, nonce: string): AppRequestBody | null {
try {
const raw = Buffer.from(encrypted, 'base64');
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
if (raw.length < 29) {
pidLogger.error('[DecryptPayload] Payload too short for GCM structure.');
return null;
}
const iv = raw.slice(0, IV_LENGTH);
const tag = raw.slice(raw.length - TAG_LENGTH);
const ciphertext = raw.slice(IV_LENGTH, raw.length - TAG_LENGTH);
const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
decipher.setAuthTag(tag);
// NOTE: Client (Swift) does NOT use AAD, so we should NOT set it here.
// However, your provided code sets AAD to the nonce string. I will maintain this,
// assuming it is required for your specific configuration.
decipher.setAAD(Buffer.from(nonce, 'utf-8'));
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return JSON.parse(decrypted.toString('utf-8')) as AppRequestBody;
} catch (error) {
pidLogger.error('[DecryptPayload] Decryption or Tag verification failed', error);
return null;
}
}```
I wouldn't touch android at this point
and you can see the ENCRYPTION_KEY is the company key, and its used in the decryptPayload function
okay
lets get ios working first
okay so swift is impenetrable
most of my issue is I dunno what kind of base64 swift uses internally
let stringToHash = "\(encryptedPayloadB64):\(nonceB64)"
let hashValue = SHA256.hash(data: Data(stringToHash.utf8))
let dataToSign = hashValue.rawData
this is my crypto manager
on the apple client
struct CryptoManager {
// Encrypts payload data using AES-256-GCM.
static func encrypt(jsonObject: JSONObject, key: Data) throws -> (encryptedPayloadB64: String, nonceB64: String) {
let plaintextData = try JSONSerialization.data(withJSONObject: jsonObject, options: [])
guard key.count == 32 else { throw Server.ServerError.networkError(statusCode: 500, body: "Invalid Encryption Key Size") }
let symmetricKey = SymmetricKey(data: key)
let nonce = AES.GCM.Nonce() // 12 bytes
// Encrypt and authenticate the payload (no AAD used in the client here,
// as the backend uses the nonce/IV Base64 string as AAD)
let sealedBox = try AES.GCM.seal(plaintextData, using: symmetricKey, nonce: nonce)
// Full payload = IV (Nonce) || Ciphertext || AuthTag
var fullPayload = Data()
fullPayload.append(sealedBox.nonce.withUnsafeBytes { Data($0) }) // 12 bytes (IV/Nonce)
fullPayload.append(sealedBox.ciphertext)
fullPayload.append(sealedBox.tag) // 16 bytes (Auth Tag)
let nonceData = nonce.withUnsafeBytes { Data($0) }
// Generate STANDARD Base64 strings first
let standardPayloadB64 = fullPayload.base64EncodedString(options: [])
let standardNonceB64 = nonceData.base64EncodedString(options: [])
// === CRITICAL FIX: Convert to URL-SAFE format for transmission ===
return (
encryptedPayloadB64: toBase64URL(standardBase64: standardPayloadB64),
nonceB64: toBase64URL(standardBase64: standardNonceB64)
)
}
}
and there you can see at the end
ive been tampering
between base64 and URL safe
why url safe aren't you passing it via post in the body, not url params?
yes its going in the body but gemini got mad at me and said it was a good idea.... lmao
and so then i tried iwth and without
and ive just been perma confused
either way it doesnt seem to work
stupid gemini. Body is safe for any data even binary raw
chatgpt looses context from previous messages, and gemini just hallucinates
its so annoying lmao
and articles online dont have context
and i cant glue them together very well
so basically
the client does this:
anyway what do you think of this?
downlaoding it
this should match android formatting... though maybe its got the wrong flavor of base64
which version does your android send as? if its url-safe base64 then it seems like this should work directly with the android handler
since it formats the message the same way for hashing
this is your android handler., anyway. it hashes the payload by concatenating the payload with the nonce, in base64 format, separated by a colon
I tried to do the same thing in swift so that you don't need device specific hashing algorithms
we need to mtch that in swift
yeah and then you can just use that one handler... if I understood correctly
so ill install that thing you just wrote in mac
okay i think so i agree yes
okay
but aspects are still slightly different because of attest no?
where is the attest? isn't that what this signature is?
let signatureB64 = try await deviceIdManager.sign(dataToSign: dataToSign) this is the thing you validate on the server side right?
lemme answer your question here
yes
i think
okay so basically looking at the server
we want to like
simplify the apple to match the android
kind of
yeah. So I looked at your android code its not generating url-safe base64
so iOS needs to not use url-safe
its because gemini told me to "try " url safe on apple
or the server wont be able to validate it properly
and your swift u wrote doesnt do URLSAFE
i think
lemme read rq
okay so
we've got non url safe
coming from the clients now
lemme test the server to see what sort of issues its showign
okay android is passing
happily
without issues still
apple still complaining
but thats fine
i think
because the server is anticipating URLSAFE
base64
for apple
make sure the server isn't transforming the base64 from apple
doesn't your encrypt return url-safe base64
maybe lemme vet that good point
on the server i need to change this right
so that the apple doesnt expect base64
you should just use the android handler, unless I missed something?
use it for both?
[2025-12-10T19:39:59.916Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/attest/challenge | 200 | 0.694 ms | UA: Verefa-App-IOS
[2025-12-10T19:40:02.024Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/regcomms/devicekeyregister | 201 | 31.228 ms | UA: Verefa-App-IOS
[2025-12-10T19:40:02.287Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: iOS | Public Key (Body Start): MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1IkGdjcaavEJMIBmcga1yBQvaclOUYj8A6H07GgRLn9djSpJzW9y9o48vYrpfYPs3EVUYQ84SMAl5+ULMBAj3Q== | UDID: 75867278-d53c-4efe-8e96-2692580783b8
[2025-12-10T19:40:02.287Z] [PID-1] [appRequest.js] LOG Using string concatenation hashing.
[2025-12-10T19:40:02.287Z] [PID-1] [appRequest.js] LOG Processing signature length: 141
[2025-12-10T19:40:02.287Z] [PID-1] [appRequest.js] ERROR Signature processing failed for UDID: 75867278-d53c-4efe-8e96-2692580783b8. Platform: iOS
[2025-12-10T19:40:02.288Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 400 | 1.140 ms | UA: Verefa-App-IOS
still not right im so confused,
ive converted it to NON URL SAFE
on the client
const rawSignatureBuffer = Buffer.from(signature, 'base64');
const derSignatureBuffer = handler.processSignature(rawSignatureBuffer);
if (!derSignatureBuffer) {
pidLogger.error(`Signature processing failed for UDID: ${deviceUDID}. Platform: ${isAndroid ? 'Android' : 'iOS'}`);
return res.status(400).json({
error: 'Signature format invalid or processing failed.',
errorCode: 'SIGNATURE_FORMAT_INVALID'
});
}
thats the server block thats tripping
im thinking something else is wrong or smth
can you hard code the ios client to send the public key and encrypted message from android to check and make sure there's nothing else?
how would i do that because the client doesnt send a public key, it sends a signature
like just log what android sends, then copy that into the ios client just for testing
const { encryptedPayload, nonce, signature, deviceUDID } = req.body as AppRequestBody;
oh I see hmm
thats what comes
ill log the req.body
for both
and see what we get
[2025-12-10T19:48:52.944Z] [PID-1] [appRequest.js] LOG {
deviceUDID: '136a9319-4519-47d0-bb39-25eb39a60387',
encryptedPayload: 'WiCDhGtVX5LI5t+hR+3HpQrkNL4hNXH7Qz5hJ6Zc/5e+URMRG6SkGwXBE0qsUYU6M4z7Z64X30X3KfvQvPFI/Y4+YGVix3/bTfXo1HnqmgAenkyEJucxO2kLr1D7gkXtRwi9/rwpD0SRe37J7CRKoZEmiK92vCYk8LWIZaP66BDJyZXJm7FQgMJ/b//YDd7+e1S9uo+wSq/HBRfSPk6emaJY01yOLGrKaC/tXL3NNsk8cKlClND6NwTfCl3ShvQcROgL/TeWhnsqAoPmkNmvofOmnuripzVnuOxR0LLeF1IZLNSf+WecekT3BqeJJEreisSdpw+rXsb5CKhuG5QYuQ==',
signature: 'MEUCIQD1NFvJOo/hi6qNZrWDHObJgwnYFK9zA7EXpqN+JUgkYQIgIGcY+JQOVLe3NvT/o1ClX4LtJPV4kVyq+v33ED+AK7s=',
nonce: 'OgI1HovLr1taoe482DJyRw=='
}
[2025-12-10T19:48:52.945Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: Android | UDID: 136a9319-4519-47d0-bb39-25eb39a60387
[2025-12-10T19:48:52.946Z] [PID-1] [appRequest.js] LOG [VERIFY] Platform: Android | Hash: 89f5b1de6a688e237bffdc062b4f4819ddf301b3d010105982f389ce74a4c4e6 | Result: true
[2025-12-10T19:48:52.953Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 200 | 8.666 ms | UA: UnknownBrowser on Android
okay so thats an android hit
(ive simplified the logging quite a bit)
and thats the req.body
and here's an apple hit:
[2025-12-10T19:50:43.630Z] [PID-1] [appRequest.js] LOG {
deviceUDID: '164ca5e6-78bf-48bc-a65a-214332b68a98',
encryptedPayload: 'isroEkgdjMkk9Xhg5oaFYErJPwMS4eKcLYJfLa9Ya9KNDvMkaa3bpIO71NQ8OeKAsofSug3aZ2qNTp5og5RPVUijcovfR4wyYaHvoJw9TpZRSvGFUR6cRASH5386Pt9LDVGyqZ/HJVGZhoJU4S6RVEBwC2TCD4u3+LO/VNW7ACIm0U3r2htbvTaS7pPGaaui2T3qTO8dTW0anr9/nzgcqyzoVNfQ7EO+qxil8fSoU9Bl0uIAZMsGar6Frbs0vSPEUge1CoO31Jpn8UqaYu4/jMBOfBs+DwD7JBgI02VvbFu2QrM1ZxQCxpwArsGgBIz0Op9zDpVbBjj5icgwv0/xy2SbTOJ2vC6rnn0bueZyd60o0aA8tc+gtBo1j4rjPZicdllkIQDtj1evQC92k7Jz+Lwdj+Qh3sUKPPy+0AlXo+VTNfqwjUwu2AJplTZJZF/lCrEa/FlJAJuIu73JJjib/TAY/W1SCVW0tYT1GzPkaC/jbGOhJA==',
nonce: 'isroEkgdjMkk9Xhg',
signature: 'omlzaWduYXR1cmVYRjBEAiA6LpK55LIfvTbSZ+rSbckOUdOA/owXvtQZSFwFZSIM+gIgE1ndsq62t2RWLAEnSEbloGw5r9/T2eN5Cz2NftWloMtxYXV0aGVudGljYXRvckRhdGFYJWG1rH4xtzJCrO145yn7wLII/w2c7PSOoexs7EhmSoiCQAAAAAE='
}
[2025-12-10T19:50:43.631Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: iOS | UDID: 164ca5e6-78bf-48bc-a65a-214332b68a98
[2025-12-10T19:50:43.631Z] [PID-1] [appRequest.js] ERROR Signature processing failed for UDID: 164ca5e6-78bf-48bc-a65a-214332b68a98. Platform: iOS
[2025-12-10T19:50:43.632Z] [PID-1] [server.js] REQ 77.103.192.136 | POST | /api/v1/device/alive | 400 | 1.107 ms | UA: Verefa-App-IOS
is it because the nonce and signature are in a different order?
you mean in the log message or are they in the wrong order for hashing?
in the log message
in the log message the order is likely random
it doesn't matter
mk
cuz its getting them via keys of the req.body anyway
so it shouldnt matter
as u say
the nonce is significantly shorter in the apple one
is that something to worry about?
I noticed that, it shouldn't affect anything, but I'm not fully sure
i dont even know what to do anymore
i cnat understand
why its not working
its so annoying
gemini doesnt know either
"Your Android hit verifies because its signature is valid. Your iOS hit fails because the base64-encoded signature blob is not an ASN.1 ECDSA signature at all. It is structurally different, much longer, and almost certainly coming from a different signing mechanism."
"A. The signature starts with โomlzaWduYXR1cmVYโฆโ
Decoding the first bytes:
omlzaWduYXR1cmVY โ Base64 decodes to:
.signatureXF0
This is not the header of an ECDSA DER signature.
It looks like Apple Secure Enclave envelope metadata, not a raw ECDSA signature."
i dont understand how that can be the case
because ive made the flow the same as android...
oh thats odd, whats the code look like for sign?
deviceIdManager.sign(dataToSign: dataToSign)
this function I mean
what does it call internally, or is that system-level code?
ah you sent me
Iโm comparing the apple and android sign functions and other bade64 bits now
yeah now I understand a lot better
its probably why your original code didn't work either
its a whole attestation object, which is what you said originally, not just a signature
Yeah itโs like cuz I was told I needed to send that for apple
But now we donโt want to right?
ย ย ย func sign(dataToSign: Data) async throws -> String {
ย ย ย ย ย
ย ย ย ย // Use generateAssertion to get the signature
ย ย ย ย let keyId = credentials.publicKeyIdentifier
ย ย ย ย guard !keyId.isEmpty else { throw KeyManagerError.signingFailed }
ย ย ย ย ย
ย ย ย ย // The generateAssertion method returns the **Assertion Object**, which is a signed
ย ย ย ย // PKCS#7 message containing the ECDSA signature. This is what must be sent.
ย ย ย ย let assertionData = try await generateAssertion(keyId: keyId, clientDataHash: dataToSign)
ย ย ย ย ย
ย ย ย ย // The server is expecting this full object, which corresponds to the 141 bytes length,
ย ย ย ย // and must have verification logic to extract the raw signature from it.
ย ย ย ย return assertionData.base64EncodedString()
ย ย }
ย ย ย
ย ย private func generateAssertion(keyId: String, clientDataHash: Data) async throws -> Data {
ย ย ย ย try await withCheckedThrowingContinuation { continuation in
ย ย ย ย ย ย DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: clientDataHash) { assertionData, error in
ย ย ย ย ย ย ย ย if let error = error {
ย ย ย ย ย ย ย ย ย ย continuation.resume(throwing: error)
ย ย ย ย ย ย ย ย ย ย return
ย ย ย ย ย ย ย ย }
ย ย ย ย ย ย ย ย guard let assertion = assertionData else {
ย ย ย ย ย ย ย ย ย ย continuation.resume(throwing: KeyManagerError.signingFailed)
ย ย ย ย ย ย ย ย ย ย return
ย ย ย ย ย ย ย ย }
ย ย ย ย ย ย ย ย continuation.resume(returning: assertion)
ย ย ย ย ย ย }
ย ย ย ย }
ย ย }
thats how apple signs
/**
* Performs a cryptographic signing operation.
*/
fun signData(context: Context, data: ByteArray): ByteArray {
val alias = SecureStorage.load(context, DEVICE_PUBLIC_KEY_ALIAS_KEY)
?: throw IllegalStateException("Key alias not found. Cannot sign data.")
val entry = keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry
?: throw IllegalStateException("Private Key entry not available for signing.")
val signature = Signature.getInstance(SIGNING_ALGORITHM)
signature.initSign(entry.privateKey)
signature.update(data)
val result = signature.sign()
Log.i(TAG, "Signing successful. Output size: ${result.size} bytes.")
return result
}
and thats how android signs
so hence the frustrating difference between clients
im not sure what to do
should i change the apple signing
to fit with android
or should i change the server
to support the apple signing
in a seperate handler
I'm not exactly sure what the "attestation object" is providing
but it doesn't look like you're using it currently on the server side?
just ongoing attestation that the client is an apple client and blah blah
you just extract the signature
yeah im not
so switch toandroid
extractEcdsaSignatureFromAssertion maybe this is broken, but ugh
issue android company phones /s
what's the implementation of extractEcdsaSignatureFromAssertion look like?
thats a good idea
this is all complex logic that gemini was like splurging out
a few days ago
because it didnt know what else to suggest
let verified: boolean = false;
// --- 2. Extract and Validate Required Fields ---
var { encryptedPayload, nonce, signature, deviceUDID } = req.body as AppRequestBody;
pidLogger(req.body);
if (!encryptedPayload || !nonce || !signature || !deviceUDID) {
pidLogger.warn('Missing required fields in secure request body. Likely non-app client attempt.');
return res.status(400).json({ error: 'Missing required request fields' });
}
if (isIos){
signature = extractEcdsaSignatureFromAssertion(signature);
}
doing this in an attempt
to see if i can get the sig specifically for the ios one
but theres issues with the function
src/middleware/appRequest.ts(119,5): error TS2322: Type 'Buffer<ArrayBufferLike> | null' is not assignable to type 'string | undefined'.
Type 'null' is not assignable to type 'string | undefined'.
src/middleware/appRequest.ts(119,52): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Buffer<ArrayBufferLike>'.
src/middleware/appRequest.ts(154,44): error TS2769: No overload matches this call.
Overload 1 of 4, '(arrayBuffer: WithImplicitCoercion<ArrayBufferLike>, byteOffset?: number | undefined, length?: number | undefined): Buffer<ArrayBufferLike>', gave the following error.
Argument of type 'string | undefined' is not assignable to parameter of type 'WithImplicitCoercion<ArrayBufferLike>'.
Type 'undefined' is not assignable to type 'WithImplicitCoercion<ArrayBufferLike>'.
Overload 2 of 4, '(string: WithImplicitCoercion<string>, encoding?: BufferEncoding | undefined): Buffer<ArrayBuffer>', gave the following error.
Argument of type 'string | undefined' is not assignable to parameter of type 'WithImplicitCoercion<string>'.
Type 'undefined' is not assignable to type 'WithImplicitCoercion<string>'.
which is probably why gemini told me to stop using it
im generating a new function with gemini currently
and trying to build it to work with the attestation object
this is from my own LLM, it is beyond me otherwise
but this doesn't try to built its own cbor parser
ty man
ill give it a try
it needs to return a string i think, not a buffer. because the sig isnt a buffer its just a base64 string
src/middleware/appRequest.ts(119,5): error TS2322: Type 'Buffer<ArrayBufferLike> | null' is not assignable to type 'string | undefined'.
Type 'null' is not assignable to type 'string | undefined'.
src/middleware/appRequest.ts(119,52): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Buffer<ArrayBufferLike>'.
src/middleware/appRequest.ts(154,44): error TS2769: No overload matches this call.
Overload 1 of 4, '(arrayBuffer: WithImplicitCoercion<ArrayBufferLike>, byteOffset?: number | undefined, length?: number | undefined): Buffer<ArrayBufferLike>', gave the following error.
Argument of type 'string | undefined' is not assignable to parameter of type 'WithImplicitCoercion<ArrayBufferLike>'.
Type 'undefined' is not assignable to type 'WithImplicitCoercion<ArrayBufferLike>'.
Overload 2 of 4, '(string: WithImplicitCoercion<string>, encoding?: BufferEncoding | undefined): Buffer<ArrayBuffer>', gave the following error.
Argument of type 'string | undefined' is not assignable to parameter of type 'WithImplicitCoercion<string>'.
Type 'undefined' is not assignable to type 'WithImplicitCoercion<string>'.
the same issue i was having on mine
i cant seem to work out HOW on earth we are supposed to get the signature out
of the assertion object
and typically the "apple says" link in that stackoverflow, is gone
its this i think
i just dont understand why my system isnt able to decode the assertion
https://github.com/srinivas1729/appattest-checker-node
im wondering how risky it is to use this guys library in a prod environment
ive found how this guy gets the sig
const assertionObj = await cbor.decodeFirst(assertion);
const { signature, authenticatorData } = assertionObj;
with this
so if i do the same with that simple cbor function
i should be good
not working
the signature is coming back as undefined...
i hate my life
oh my god
maybe i change apple
so that it fits with android
and we send the signature like android
instead of an attestation object
hm
yes bcuz if we are trusting a sig from android
we might aswell trust apple
yes
thats a better approach
unified middleware
none of these weird attestation objects from apple
that i hate
can you dump the whole cbor object?
cbor is like json its just a serialization format
from what I could find, there's no other way to do the onboarding without doing the weird attestation object
after that you can use normal signing, but initial onboarding takes attest
how would i do that?
just like cbor(assertion)?
i meant none of these weird assertion objects
not attestation
because the attestation signup part works fine
its just the assertions which dont
Oh allegedly if you already have onboarding set up you can use that key for simple signing operations. I will have to get back to you on how exactly
You only need the assertion object for first onboarding
wait whaaa
no stress
but i dont understand that icl
what ive tried since talking tho is basically i tried switching the client to just sending the sig
turns out thats not possible
because you cant sign with the private key on the client
apple doenst let you
so the signature cant be generated on the client
instead only an assertion object
so now im trying to decrypt the assertion object on the server
its still not working
im actually starting to wanna die
in a few hours this will be my fith day of trying
[2025-12-10T23:59:55.929Z] [PID-1] [crypto.js] INFO --- iOS Assertion Object Tracing START ---
[2025-12-10T23:59:55.929Z] [PID-1] [crypto.js] INFO [TRACE-1] Input Buffer Length (Base64 Decoded): 141
[2025-12-10T23:59:55.929Z] [PID-1] [crypto.js] INFO [TRACE-3] CBOR Decoding Successful.
[2025-12-10T23:59:55.929Z] [PID-1] [crypto.js] INFO [TRACE-7] Signature (DER=71) and AuthData (37) extracted successfully.
[2025-12-10T23:59:55.929Z] [PID-1] [crypto.js] INFO --- iOS Assertion Object Tracing END (Success) ---
[2025-12-10T23:59:55.930Z] [PID-1] [appRequest.js] LOG [VERIFY DEBUG] Platform: iOS | ClientDataHash: ef58119ab4c72c988d5a0830de2eb90b9a3c4359bfb8e0bdb2a91e59f5b284fe | Final Hash Used: b7793f0b1fcb4c50689de3a6f92754f3c658df5233002672bea837650154d388
[2025-12-10T23:59:55.930Z] [PID-1] [appRequest.js] LOG [VERIFY] Platform: iOS | Hash: b7793f0b1fcb4c50689de3a6f92754f3c658df5233002672bea837650154d388 | Result: true
[2025-12-10T23:59:55.930Z] [PID-1] [appRequest.js] ERROR [DecryptPayload] Decryption or Tag verification failed Error: Unsupported state or unable to authenticate data
at Decipheriv.final (node:internal/crypto/cipher:193:29)
at decryptPayload (/app/dist/middleware/appRequest.js:33:80)
at appRequestHandler (/app/dist/middleware/appRequest.js:180:34)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
finally
its working
kind of
lmao
its just started working
yeah I was just reading about what attest does
finallly
yay
now the error is beyond the verification
congrats
i literalyl just marched aorund my room im so happy dude
lmao
actually goated
taken so long
now thers like anothe error
yeah nasty problems like this where you can't properly debug are so frustrating
thats why i hate the vagueness of the apple docs
and the sort of enclosed system
whereby you cant see the client stuff
when it sends it
well I suppose it could just be misverifying and passing everything
lmao
we wont talk about that
ill change some stuff server side to vet against that somehow
maybe
hmm
thats a good point actually
yeah just swap a character in the payload and see if it verifies
the sig has structure iirc
its made out of two numbers or something
oh nvm you're right
I was confusing with key
yeah its finally just the 1 sig thing
ill share the final code here
so u can at least see how the crypto work
incase u need it one day
lmao
thank you so much man
now ive got to like fix the next issue its causing
but at least its verifying
thats the crypto that takes the attestation object.
I only helped with rubber ducking, you did the real work
nah u actually helped me understand my own architechture
lmao
i didnt even know it very well prior
or comprehend it that well
its mostly tutorials and llm's
but now i actually comprehend the flow better
cuz like im tryna build an industry standard or better security system, and im not an industry standard guy
im like 19
lmao
i hate vibe coding too icl
but working with apple has forced me to
cuz i had no experience with DCAppAttest or much complex swift
only kotlin
yeah apples docs seem suspiciously lacking
its like super vague
like theres no real practical demos
showing how to do it
its just instructions for clever dudes
and somehow i cant find many typescript instances of this being done before
maybe its treated as an industry secret as to the most efficient comms path
i guess its also quite sensitive data
for a company to be disclosing how they use attest
it would be nice kinda if apple hosted an attest endpoint
like play integrity
to save all this effort
except i do fear my apps deployment with play integrity will get bottlenecked by the integrity rate limits if i get loads of users
but there is a google form u fill in to request more usage
for free i think
so thats promising
at least apple attest is technically unrestricted usage as u do the verification urself
play integrity still offers that tho too
so apple has no excuses really
evidently not even nearly out of the woods
the decryption isnt passing
on apple
lmao
and wont
well at least you probably don't need that part?
yeah thats one thing ive got in the back of my mind
its something that i dont think im gonna need
i think the nonce is in UTF8 on android and BASE64 on apple
which is causing issues
when it comes to decrypting the 2 with the same func
the rule of thumb, as I understand it is: signing is for anti-tampering, and encryption is for anti-eavesdropping
surely tho i need to encrypt then
because the app sends data like passport numbers
and if that was breached via an admin on a school network where someone was using the app
then that would uh
cause me to get put in jail
oh but you already have https transport underneath that
it doesn't hurt anything to have it. But the extra layer of encryption doesn't help that much https is already industry state of the art
and either way your server has the capability to decrypt it
but cant a local network https be violated by the network admins
no, not unless you have the user install a private certificate authority. You can always pin the certificate on the client side, too, to prevent mitm attacks
but mitm always starts with the user intentionally installing a certificate authority
and if the user can be convinced to do that, they can be convinced into doing any number of other privilege escalation tricks to introspect your app
but like I said: it doesn't hurt to have another layer of encryption, if you need peace of mind. You need to figure out how you're going to revoke the keys though, if worst case happens.
with https there's already key revocation infrastructure
okay
i see
well i can just revoke the keys server side, and then encryption will reject
and the apps will all go dead in the water basically
and they will all have to update their clients
not quite, if the attacker has violated the https tunnel, then clients will still be sending messages to you encrypted with the corporate public key
so all that data will leak to an attacker, because they can decrypt it
yeah that's the worst case scenario
what if like the clients ask the server "yo has the key leaked" lmao
it has the same failure mode as https cert private key leaking
first
then you're relying on https anyway
'cause the cert private key is also on the server
so its basically an added security risk if my servers get breached
no no extra risk
but its a nice end 2 end convenience until that happens
it does provide a little extra defense, though I feel its more "security through obscurity" kind of bonus
until my company gets breached
this won't impact performance mesurably
and small overall request objects for low data environments
you're not sending that many messages, whether you encrypt or not
we send a message when a device stops moving
so for like hundreds of users in transit they would technically be all firing off quite alot
but yes i agree your right its not measurable
its sort of like
extra complexity until we get hacked
ah well I guess you might be right in aggregate it probably is measurable
hmmm
not sure anymore
because its a complete like product collapse if the company key leaks
like complete collapse
I am a little surprised, I always have location on my device turned off 'cause it drains the battery
all clients immediately are breached
yeah avoid that: defense in depth
You are one of the more like location concious users lmao
we are trying to get users to keep it on
and not just approx, but precise
and always enabled
so that we can do location based, and biometric based, auth
in real world environments
gotta supply them with company phones
so glance at a camera at a venue and we let you in based on your face, and the pool of devices that are reporting to be near the gate
but it is a cool idea
this is for public market
lmao
but yea
its gonna be hard no doubt
to get continuous valid data from the clients
the product is rlly close
but we are just working on this auth flow aspect
whilst switching from a very much so dev environment to an encrypted secure flow
well good luck on launching. Cross platform mobile is not a pleasant development experience
tell me about it
lmao
i was gonna do React native
but it was taking me too long to get comfortable
so i bought a mac mini
and now im like juggling swift, kotlin, the nodejs servers, and the frickin marketing sites and the server that runs that
so its a fun experience
hence 1:35am
oh no that's an implement of torture. Don't be tricked into thinking Google's Flutter is any better
(tho flutter does have some neat ideas in it, the cross-platform dev experience is bad)
i went from flutter, to react native, and then i waslike what the hell man these are all so stupid
yeah roll your own, its the only thing sane
and for like longevity
and actual control
becuase im reading passports
i need like proper language level stuff
not some high level wrapper of the language
reading the passport data took so long to get right
yeah. Some of the "native" libs for flutter are like "lets serialize every os message through a json protocol, its super slow, this'll be great"
it seems nobody has ever done it in kotlin successfully
i cant even understand how there is so little docs or stack overflow stuff about things like nfc scanning
i can imagine
lmao
the trust levels too is a problem
like do i trust the BAC authentication of a passport that gets sent from the client, or do we copy the passport data to the server and then run BAC authentication on it
theres like the security argument that storing passport data server side is a huge risk if we get hacked
on android? aside from google play integrity which is just a hair away from "non-existent", no you do not ๐
lowk
yeah i feel that way more and more
swift is fast for implementing new features and stuff
and kotlin feels like this janky java environment with some new stuff
and its not nearly as safe
because its an android
lmao
for that kinda stuff, there's no way I'd do it without a physical hsm attached to the server
but storing passport data locally on the clients and just claiming auth is safer for the companys media image when we get our servers breached
yeah i mean thats lovely idea tbh
but i use cloud services that im pretty sure dont offer that
I think aws offers something? but I haven't really looked into it much, in some ways cloud is a downgrade
its probably expensive
and we do biometrics on the passport face vs a scan in real time and i pass that off to a third party GPU provider for speed
but that again is a concern with security really
that's a neat idea, so basically you're storing the critical data on the phone, signed to that you know you verified it?
I'm not sure what happens when you get audited though
hence this level of like data transmission trust im trying to build
to ensure we cant get users tampering the passport data and stuff
yeah me neither
lmao
were also trying to enforce 1 account per human
ah good luck
using intelligent registration signals so like likelihood of someone called "james" registering twice in the same ip area and whatnot
and that is a minefield of security too because we dont want people getting into other peoples accounts by claiming they lost their old device
when our primary trust data is biometrics, location, and sometimes passports
the goal is to throw away passwords
and that brings with it a ton of complication
I am fond of security where you get mailed a blank steel plate and a set of letter blanks you hammer into the metal to preserve your account recovery key
(steel not paper so if your house burns down you don't get locked out for life)
i feel like im building a product that will have an "acceptable rate of account violations"
whereby a percentage of users get violated
which is a crazy admission really
yeah
yeah its like what do we do if they uninstall the app
and their keychain is wiped
or get a new phone
our server has to consider it a new phone when and if they reinstall
actually apple might be able to handle migration, but ideally the keys shouldn't be migratable
and deduce using signals like location and their face if they have an account already and then assign it to that phone
yeah im gonna try and enforce they never move
instead your account gets repositioned
that's scary. Spoofing location is super easy, and AI is making "live faces" easier and easier every day to spoof
to a new device public key
indeed
lmao
thats why we use nfc passports as final truth
and whether the device public key is registered with us to an account or not
these are like US govt passports or do you mean something different?
yeah the gov ones
they have a little NFC chip in them
which phones read
we get you to scan it
oh neat I never realized they had nfc in them. I guess I need to get a little wire mesh bag
nah its okay
the hacker needs the details
(name, dob, expiry)
to read the nfc chip
oh cool
its encrypted by those
to prevent slideby attacks like that
so we get you to take a picture of it first
to read those values
thats some forward thinking security from the govt. unexpected
not really a picture tho, weve put a camera text detection model on the app
which is so janky because its different for ios vs android
ikr
its in like most passports now
all around the world
and somehow all the agencies use the same protocols
within reason
so most passports can be read like that
and authenticated against the ICAO root certificates
I wonder if they got china on board, since iirc china doesn't like any of the common western encryption algorithms
lmao i dont think so
theres an extensive list
but yeah i mean im pretty sure afghanistan isnt on there for sure
i cant remember entirely but theres a good handful who arent
unfortunately theres 2 versions of the protocol too
PACE and BAC
and some passports only have the older one
and vice versa
so its a difficult game
ultimately the entire product is a consumer grade surveillance network that you can opt into, and leverage that network to authenticate yourself among the network without needing passwords, usernames and stuff like that
and its built for consumer and business customers for IAM software for a business to manage customers + staff
its all completely working now
ive fixed it entirely
the encryption works
everything
its because on android i was supplying the nonce as the AAD for encryption
and on apple i wasnt supplying anything
so the encryption was completely different too
fixed that now
and introduced TONS of ratelimiting on the reverse proxy
because im being inundated with a botnet
lmao
i kept the company public key system
so yes we are in a precarious situation if the keys get leaked
but its just an added layer of security until that happens
and this is arguably an MVP
which will have like early users primarily
before i hire professional developers
lmao
dude i swear i never sleep these days its crazy