-
Notifications
You must be signed in to change notification settings - Fork 116
/
androidSafetyNet.js
108 lines (78 loc) · 3.33 KB
/
androidSafetyNet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { ab2str, appendBuffer, coerceToBase64, tools } from "../utils.js";
import { Certificate } from "../certUtils.js";
function androidSafetyNetParseFn(attStmt) {
const ret = new Map();
// console.log("android-safetynet", attStmt);
ret.set("ver", attStmt.ver);
const response = ab2str(attStmt.response);
ret.set("response", response);
// console.log("returning", ret);
return ret;
}
// Validation:
// https://www.w3.org/TR/webauthn/#android-safetynet-attestation (verification procedure)
async function androidSafetyNetValidateFn() {
const response = this.authnrData.get("response");
// parse JWS
const protectedHeader = await tools.decodeProtectedHeader(response);
const publicKey = await tools.getEmbeddedJwk(protectedHeader);
const parsedJws = await tools.jwtVerify(
response,
await tools.importJWK(publicKey),
);
// Append now verified header to jws
parsedJws.header = protectedHeader;
this.authnrData.set("payload", parsedJws.payload);
// Required: verify that ctsProfileMatch attribute in the parsedJws.payload is true
if (!parsedJws.payload.ctsProfileMatch){
throw new Error("android-safetynet attestation: ctsProfileMatch: the device is not compatible");
}
// Required: verify nonce
// response.nonce === base64( sha256( authenticatorData concatenated with clientDataHash ))
const rawClientData = this.clientData.get("rawClientDataJson");
const rawAuthnrData = this.authnrData.get("rawAuthnrData");
// create clientData SHA-256 hash
const clientDataHash = await tools.hashDigest(rawClientData);
// concatenate buffers
const rawAuthnrDataBuf = new Uint8Array(rawAuthnrData);
const clientDataHashBuf = new Uint8Array(clientDataHash);
const concatenated = appendBuffer(rawAuthnrDataBuf, clientDataHashBuf);
// create hash of the concatenation
const hash = await tools.hashDigest(concatenated);
const nonce = tools.base64.fromArrayBuffer(hash);
// check result
if(nonce!==parsedJws.payload.nonce){
throw new Error("android-safetynet attestation: nonce check hash failed");
}
// check for any safetynet errors
if(parsedJws.payload.error){
throw new Error("android-safetynet: " + parsedJws.payload.error + "advice: " + parsedJws.payload.advice);
}
this.audit.journal.add("payload");
this.audit.journal.add("ver");
this.audit.journal.add("response");
// get certs
this.authnrData.set("attCert", parsedJws.header.x5c.shift());
this.authnrData.set("x5c", parsedJws.header.x5c);
this.audit.journal.add("attCert");
this.audit.journal.add("x5c");
// TODO: verify attCert is issued to the hostname "attest.android.com"
const attCert = new Certificate(coerceToBase64(parsedJws.header.x5c.shift(), "parsedAttCert"));
this.audit.info.set("organization-name", attCert.getSubject().get("organization-name"));
// attCert.getExtensions()
// TODO: verify cert chain
// var rootCerts;
// if (Array.isArray(rootCert)) rootCerts = rootCert;
// else rootCerts = [rootCert];
// var ret = await CertManager.verifyCertChain(parsedJws.header.x5c, rootCerts, crls);
// If successful, return attestation type Basic and attestation trust path attCert.
this.audit.info.set("attestation-type", "basic");
this.audit.journal.add("fmt");
return true;
}
const androidSafetyNetAttestation = {
name: "android-safetynet",
parseFn: androidSafetyNetParseFn,
validateFn: androidSafetyNetValidateFn,
};
export { androidSafetyNetAttestation };