1
+ package com .yubico .webauthn ;
2
+
3
+ import com .fasterxml .jackson .databind .JsonNode ;
4
+ import com .yubico .internal .util .ExceptionUtil ;
5
+ import com .yubico .webauthn .data .AttestationObject ;
6
+ import com .yubico .webauthn .data .AttestationType ;
7
+ import com .yubico .webauthn .data .ByteArray ;
8
+ import java .nio .charset .Charset ;
9
+ import java .io .IOException ;
10
+ import javax .net .ssl .SSLException ;
11
+ import java .security .GeneralSecurityException ;
12
+ import java .security .cert .X509Certificate ;
13
+ import java .util .Arrays ;
14
+ import java .util .Optional ;
15
+ import java .util .ArrayList ;
16
+ import java .util .List ;
17
+ import lombok .extern .slf4j .Slf4j ;
18
+ import com .google .api .client .json .webtoken .JsonWebSignature ;
19
+ import com .google .api .client .json .jackson2 .JacksonFactory ;
20
+ import org .apache .http .conn .ssl .DefaultHostnameVerifier ;
21
+
22
+ @ Slf4j
23
+ class AndroidSafetynetAttestationStatementVerifier implements AttestationStatementVerifier {
24
+
25
+ private final BouncyCastleCrypto crypto = new BouncyCastleCrypto ();
26
+
27
+ private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier ();
28
+
29
+ private X509Certificate mX5cCert = null ;
30
+
31
+ public Optional <List <X509Certificate >> getAttestationTrustPath () {
32
+ if (mX5cCert != null ) {
33
+ List <X509Certificate > certs = new ArrayList <>(1 );
34
+ certs .add (mX5cCert );
35
+ return Optional .of (certs );
36
+ }
37
+ return Optional .empty ();
38
+ }
39
+
40
+ @ Override
41
+ public AttestationType getAttestationType (AttestationObject attestation ) {
42
+ return AttestationType .BASIC ;
43
+ }
44
+
45
+ @ Override
46
+ public boolean verifyAttestationSignature (AttestationObject attestationObject , ByteArray clientDataJsonHash ) {
47
+ final JsonNode ver = attestationObject .getAttestationStatement ().get ("ver" );
48
+ final JsonNode response = attestationObject .getAttestationStatement ().get ("response" );
49
+
50
+ if (ver == null || !ver .isTextual () ) {
51
+ throw new IllegalArgumentException ("attStmt.ver must be set as text! " + ver .toString ());
52
+ }
53
+
54
+ if (response == null || !response .isBinary () ) {
55
+ throw new IllegalArgumentException ("attStmt.response must be set to a binary value." );
56
+ }
57
+
58
+ String attStmtString ;
59
+ try {
60
+ attStmtString = new String (response .binaryValue (), Charset .forName ("UTF-8" ));
61
+ } catch (IOException ioe ) {
62
+ throw ExceptionUtil .wrapAndLog (log , "reponseNode.isBinary() was true but reponseNode.binaryValue() failed" , ioe );
63
+ }
64
+
65
+ if (attStmtString != null && !attStmtString .isEmpty ()) {
66
+ final AndroidSafetynetAttestationStatement attStmtObj = parseAndVerify (attStmtString );
67
+ if (attStmtObj != null ) {
68
+ final byte [] nonce = attStmtObj .getNonce ();
69
+ final boolean isCtsProfileMatch = attStmtObj .isCtsProfileMatch ();
70
+
71
+ if (isCtsProfileMatch ) {
72
+ // Verify that the nonce in the response is identical to the SHA-256 hash of
73
+ // the concatenation of authenticatorData and clientDataHash.
74
+ ByteArray signedData = attestationObject .getAuthenticatorData ().getBytes ().concat (clientDataJsonHash );
75
+ ByteArray hashSignedData = crypto .hash (signedData );
76
+ ByteArray nonceByteArray = new ByteArray (nonce );
77
+
78
+ final int compareResult = hashSignedData .compareTo (nonceByteArray );
79
+ return (compareResult == 0 );
80
+ }
81
+ }
82
+ }
83
+
84
+ return false ;
85
+ }
86
+
87
+ /**
88
+ * This code is copied from android-play-saftynet attestion sample.
89
+ * @param signedAttestationStatment
90
+ * @return
91
+ */
92
+ private AndroidSafetynetAttestationStatement parseAndVerify (String signedAttestationStatment ) {
93
+ // Parse JSON Web Signature format.
94
+ JsonWebSignature jws ;
95
+ try {
96
+ jws = JsonWebSignature .parser (JacksonFactory .getDefaultInstance ())
97
+ .setPayloadClass (AndroidSafetynetAttestationStatement .class ).parse (signedAttestationStatment );
98
+ } catch (IOException e ) {
99
+ System .err .println ("Failure: " + signedAttestationStatment + " is not valid JWS " +
100
+ "format." );
101
+ return null ;
102
+ }
103
+
104
+ // Verify the signature of the JWS and retrieve the signature certificate.
105
+ X509Certificate cert ;
106
+ try {
107
+ cert = jws .verifySignature ();
108
+ if (cert == null ) {
109
+ System .err .println ("Failure: Signature verification failed." );
110
+ return null ;
111
+ }
112
+ } catch (GeneralSecurityException e ) {
113
+ System .err .println (
114
+ "Failure: Error during cryptographic verification of the JWS signature." );
115
+ return null ;
116
+ }
117
+
118
+ // Verify the hostname of the certificate.
119
+ if (!verifyHostname ("attest.android.com" , cert )) {
120
+ System .err .println ("Failure: Certificate isn't issued for the hostname attest.android" +
121
+ ".com." );
122
+ return null ;
123
+ }
124
+
125
+ // Save the cefrtificate
126
+ mX5cCert = cert ;
127
+
128
+ // Extract and use the payload data.
129
+ AndroidSafetynetAttestationStatement stmt = (AndroidSafetynetAttestationStatement ) jws .getPayload ();
130
+ return stmt ;
131
+ }
132
+
133
+ /**
134
+ * Verifies that the certificate matches the specified hostname.
135
+ * Uses the {@link DefaultHostnameVerifier} from the Apache HttpClient library
136
+ * to confirm that the hostname matches the certificate.
137
+ *
138
+ * @param hostname
139
+ * @param leafCert
140
+ * @return
141
+ */
142
+ private static boolean verifyHostname (String hostname , X509Certificate leafCert ) {
143
+ try {
144
+ // Check that the hostname matches the certificate. This method throws an exception if
145
+ // the cert could not be verified.
146
+ HOSTNAME_VERIFIER .verify (hostname , leafCert );
147
+ return true ;
148
+ } catch (SSLException e ) {
149
+ e .printStackTrace ();
150
+ }
151
+
152
+ return false ;
153
+ }
154
+ }
0 commit comments