1 /*
2  * Copyright (c) 2008, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 import com.sun.security.auth.module.Krb5LoginModule;
25 
26 import java.io.IOException;
27 import java.lang.reflect.InvocationTargetException;
28 import java.lang.reflect.Method;
29 import java.security.PrivilegedActionException;
30 import java.security.PrivilegedExceptionAction;
31 import java.util.Arrays;
32 import java.util.HashMap;
33 import java.util.Map;
34 import javax.security.auth.Subject;
35 import javax.security.auth.callback.Callback;
36 import javax.security.auth.callback.CallbackHandler;
37 import javax.security.auth.callback.NameCallback;
38 import javax.security.auth.callback.PasswordCallback;
39 import javax.security.auth.callback.UnsupportedCallbackException;
40 import javax.security.auth.kerberos.KerberosKey;
41 import javax.security.auth.kerberos.KerberosTicket;
42 import javax.security.auth.login.LoginContext;
43 import org.ietf.jgss.GSSContext;
44 import org.ietf.jgss.GSSCredential;
45 import org.ietf.jgss.GSSException;
46 import org.ietf.jgss.GSSManager;
47 import org.ietf.jgss.GSSName;
48 import org.ietf.jgss.MessageProp;
49 import org.ietf.jgss.Oid;
50 import sun.security.jgss.krb5.Krb5Util;
51 import sun.security.krb5.Credentials;
52 import sun.security.krb5.internal.ccache.CredentialsCache;
53 
54 import java.io.ByteArrayInputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.security.Principal;
57 import java.util.Set;
58 
59 /**
60  * Context of a JGSS subject, encapsulating Subject and GSSContext.
61  *
62  * Three "constructors", which acquire the (private) credentials and fill
63  * it into the Subject:
64  *
65  * 1. static fromJAAS(): Creates a Context using a JAAS login config entry
66  * 2. static fromUserPass(): Creates a Context using a username and a password
67  * 3. delegated(): A new context which uses the delegated credentials from a
68  *    previously established acceptor Context
69  *
70  * Two context initiators, which create the GSSContext object inside:
71  *
72  * 1. startAsClient()
73  * 2. startAsServer()
74  *
75  * Privileged action:
76  *    doAs(): Performs an action in the name of the Subject
77  *
78  * Handshake process:
79  *    static handShake(initiator, acceptor)
80  *
81  * A four-phase typical data communication which includes all four GSS
82  * actions (wrap, unwrap, getMic and veryfyMiC):
83  *    static transmit(message, from, to)
84  */
85 public class Context {
86 
87     private Subject s;
88     private GSSContext x;
89     private String name;
90     private GSSCredential cred;     // see static method delegated().
91 
92     static boolean usingStream = false;
93 
Context()94     private Context() {}
95 
96     /**
97      * Using the delegated credentials from a previous acceptor
98      */
delegated()99     public Context delegated() throws Exception {
100         Context out = new Context();
101         out.s = s;
102         try {
103             out.cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() {
104                 @Override
105                 public GSSCredential run() throws Exception {
106                     GSSCredential cred = x.getDelegCred();
107                     if (cred == null && x.getCredDelegState() ||
108                             cred != null && !x.getCredDelegState()) {
109                         throw new Exception("getCredDelegState not match");
110                     }
111                     return cred;
112                 }
113             });
114         } catch (PrivilegedActionException pae) {
115             throw pae.getException();
116         }
117         out.name = name + " as " + out.cred.getName().toString();
118         return out;
119     }
120 
121     /**
122      * No JAAS login at all, can be used to test JGSS without JAAS
123      */
fromThinAir()124     public static Context fromThinAir() throws Exception {
125         Context out = new Context();
126         out.s = new Subject();
127         return out;
128     }
129 
130     /**
131      * Logins with a JAAS login config entry name
132      */
fromJAAS(final String name)133     public static Context fromJAAS(final String name) throws Exception {
134         Context out = new Context();
135         out.name = name;
136         LoginContext lc = new LoginContext(name);
137         lc.login();
138         out.s = lc.getSubject();
139         return out;
140     }
141 
142     /**
143      * Logins with username/password as a new Subject
144      */
fromUserPass( String user, char[] pass, boolean storeKey)145     public static Context fromUserPass(
146             String user, char[] pass, boolean storeKey) throws Exception {
147         return fromUserPass(new Subject(), user, pass, storeKey);
148     }
149 
150     /**
151      * Logins with username/password as an existing Subject. The
152      * same subject can be used multiple times to simulate multiple logins.
153      */
fromUserPass(Subject s, String user, char[] pass, boolean storeKey)154     public static Context fromUserPass(Subject s,
155             String user, char[] pass, boolean storeKey) throws Exception {
156         Context out = new Context();
157         out.name = user;
158         out.s = s;
159         Krb5LoginModule krb5 = new Krb5LoginModule();
160         Map<String, String> map = new HashMap<>();
161         Map<String, Object> shared = new HashMap<>();
162 
163         if (storeKey) {
164             map.put("storeKey", "true");
165         }
166 
167         if (pass != null) {
168             krb5.initialize(out.s, new CallbackHandler() {
169                 @Override
170                 public void handle(Callback[] callbacks)
171                         throws IOException, UnsupportedCallbackException {
172                     for (Callback cb: callbacks) {
173                         if (cb instanceof NameCallback) {
174                             ((NameCallback)cb).setName(user);
175                         } else if (cb instanceof PasswordCallback) {
176                             ((PasswordCallback)cb).setPassword(pass);
177                         }
178                     }
179                 }
180             }, shared, map);
181         } else {
182             map.put("doNotPrompt", "true");
183             map.put("useTicketCache", "true");
184             if (user != null) {
185                 map.put("principal", user);
186             }
187             krb5.initialize(out.s, null, shared, map);
188         }
189 
190         krb5.login();
191         krb5.commit();
192 
193         return out;
194     }
195 
196     /**
197      * Logins with username/keytab as an existing Subject. The
198      * same subject can be used multiple times to simulate multiple logins.
199      */
fromUserKtab( String user, String ktab, boolean storeKey)200     public static Context fromUserKtab(
201             String user, String ktab, boolean storeKey) throws Exception {
202         return fromUserKtab(new Subject(), user, ktab, storeKey);
203     }
204 
205     /**
206      * Logins with username/keytab as a new subject,
207      */
fromUserKtab(Subject s, String user, String ktab, boolean storeKey)208     public static Context fromUserKtab(Subject s,
209             String user, String ktab, boolean storeKey) throws Exception {
210         Context out = new Context();
211         out.name = user;
212         out.s = s;
213         Krb5LoginModule krb5 = new Krb5LoginModule();
214         Map<String, String> map = new HashMap<>();
215 
216         map.put("isInitiator", "false");
217         map.put("doNotPrompt", "true");
218         map.put("useTicketCache", "false");
219         map.put("useKeyTab", "true");
220         map.put("keyTab", ktab);
221         map.put("principal", user);
222         if (storeKey) {
223             map.put("storeKey", "true");
224         }
225 
226         krb5.initialize(out.s, null, null, map);
227         krb5.login();
228         krb5.commit();
229         return out;
230     }
231 
232     /**
233      * Starts as a client
234      * @param target communication peer
235      * @param mech GSS mech
236      * @throws java.lang.Exception
237      */
startAsClient(final String target, final Oid mech)238     public void startAsClient(final String target, final Oid mech) throws Exception {
239         doAs(new Action() {
240             @Override
241             public byte[] run(Context me, byte[] dummy) throws Exception {
242                 GSSManager m = GSSManager.getInstance();
243                 me.x = m.createContext(
244                           target.indexOf('@') < 0 ?
245                             m.createName(target, null) :
246                             m.createName(target, GSSName.NT_HOSTBASED_SERVICE),
247                         mech,
248                         cred,
249                         GSSContext.DEFAULT_LIFETIME);
250                 return null;
251             }
252         }, null);
253     }
254 
255     /**
256      * Starts as a server
257      * @param mech GSS mech
258      * @throws java.lang.Exception
259      */
startAsServer(final Oid mech)260     public void startAsServer(final Oid mech) throws Exception {
261         startAsServer(null, mech, false);
262     }
263 
startAsServer(final String name, final Oid mech)264     public void startAsServer(final String name, final Oid mech) throws Exception {
265         startAsServer(name, mech, false);
266     }
267     /**
268      * Starts as a server with the specified service name
269      * @param name the service name
270      * @param mech GSS mech
271      * @throws java.lang.Exception
272      */
startAsServer(final String name, final Oid mech, final boolean asInitiator)273     public void startAsServer(final String name, final Oid mech, final boolean asInitiator) throws Exception {
274         doAs(new Action() {
275             @Override
276             public byte[] run(Context me, byte[] dummy) throws Exception {
277                 GSSManager m = GSSManager.getInstance();
278                 me.cred = m.createCredential(
279                         name == null ? null :
280                           (name.indexOf('@') < 0 ?
281                             m.createName(name, null) :
282                             m.createName(name, GSSName.NT_HOSTBASED_SERVICE)),
283                         GSSCredential.INDEFINITE_LIFETIME,
284                         mech,
285                         asInitiator?
286                                 GSSCredential.INITIATE_AND_ACCEPT:
287                                 GSSCredential.ACCEPT_ONLY);
288                 me.x = m.createContext(me.cred);
289                 return null;
290             }
291         }, null);
292     }
293 
294     /**
295      * Accesses the internal GSSContext object. Currently it's used for --
296      *
297      * 1. calling requestXXX() before handshake
298      * 2. accessing source name
299      *
300      * Note: If the application needs to do any privileged call on this
301      * object, please use doAs(). Otherwise, it can be done directly. The
302      * methods listed above are all non-privileged calls.
303      *
304      * @return the GSSContext object
305      */
x()306     public GSSContext x() {
307         return x;
308     }
309 
310     /**
311      * Accesses the internal subject.
312      * @return the subject
313      */
s()314     public Subject s() {
315         return s;
316     }
317 
318     /**
319      * Returns the cred inside, if there is one
320      */
cred()321     public GSSCredential cred() {
322         return cred;
323     }
324 
325     /**
326      * Disposes the GSSContext within
327      * @throws org.ietf.jgss.GSSException
328      */
dispose()329     public void dispose() throws GSSException {
330         x.dispose();
331     }
332 
333     /**
334      * Does something using the Subject inside
335      * @param action the action
336      * @param in the input byte
337      * @return the output byte
338      * @throws java.lang.Exception
339      */
doAs(final Action action, final byte[] in)340     public byte[] doAs(final Action action, final byte[] in) throws Exception {
341         try {
342             return Subject.doAs(s, new PrivilegedExceptionAction<byte[]>() {
343 
344                 @Override
345                 public byte[] run() throws Exception {
346                     return action.run(Context.this, in);
347                 }
348             });
349         } catch (PrivilegedActionException pae) {
350             throw pae.getException();
351         }
352     }
353 
354     /**
355      * Prints status of GSSContext and Subject
356      * @throws java.lang.Exception
357      */
358     public void status() throws Exception {
359         System.out.println("STATUS OF " + name.toUpperCase());
360         if (x != null) {
361             StringBuffer sb = new StringBuffer();
362             if (x.getAnonymityState()) {
363                 sb.append("anon, ");
364             }
365             if (x.getConfState()) {
366                 sb.append("conf, ");
367             }
368             if (x.getCredDelegState()) {
369                 sb.append("deleg, ");
370             }
371             if (x.getIntegState()) {
372                 sb.append("integ, ");
373             }
374             if (x.getMutualAuthState()) {
375                 sb.append("mutual, ");
376             }
377             if (x.getReplayDetState()) {
378                 sb.append("rep det, ");
379             }
380             if (x.getSequenceDetState()) {
381                 sb.append("seq det, ");
382             }
383             System.out.println("   Context status of " + name + ": " + sb.toString());
384             if (x.isProtReady() || x.isEstablished()) {
385                 System.out.println("   " + x.getSrcName() + " -> " + x.getTargName());
386             }
387         }
388         xstatus();
389         if (s != null) {
390             System.out.println("====== START SUBJECT CONTENT =====");
391             for (Principal p : s.getPrincipals()) {
392                 System.out.println("    Principal: " + p);
393             }
394             for (Object o : s.getPublicCredentials()) {
395                 System.out.println("    " + o.getClass());
396                 System.out.println("        " + o);
397             }
398             System.out.println("====== Private Credentials Set ======");
399             for (Object o : s.getPrivateCredentials()) {
400                 System.out.println("    " + o.getClass());
401                 if (o instanceof KerberosTicket) {
402                     KerberosTicket kt = (KerberosTicket) o;
403                     System.out.println("        " + kt.getServer() + " for " + kt.getClient());
404                 } else if (o instanceof KerberosKey) {
405                     KerberosKey kk = (KerberosKey) o;
406                     System.out.print("        " + kk.getKeyType() + " " + kk.getVersionNumber() + " " + kk.getAlgorithm() + " ");
407                     for (byte b : kk.getEncoded()) {
408                         System.out.printf("%02X", b & 0xff);
409                     }
410                     System.out.println();
411                 } else if (o instanceof Map) {
412                     Map map = (Map) o;
413                     for (Object k : map.keySet()) {
414                         System.out.println("        " + k + ": " + map.get(k));
415                     }
416                 } else {
417                     System.out.println("        " + o);
418                 }
419             }
420             System.out.println("====== END SUBJECT CONTENT =====");
421         }
422     }
423 
424     public void xstatus() throws Exception {
425         System.out.println("   Extended context status:");
426         if (x != null) {
427             try {
428                 Class<?> clazz = Class.forName("com.sun.security.jgss.ExtendedGSSContext");
429                 if (clazz.isAssignableFrom(x.getClass())) {
430                     if (clazz.getMethod("getDelegPolicyState").invoke(x) == Boolean.TRUE) {
431                         System.out.println("   deleg policy");
432                     }
433                     if (x.isEstablished()) {
434                         Class<?> inqType = Class.forName("com.sun.security.jgss.InquireType");
435                         Method inqMethod = clazz.getMethod("inquireSecContext", inqType);
436                         for (Object o : inqType.getEnumConstants()) {
437                             System.out.println("   " + o + ":");
438                             try {
439                                 System.out.println("      " + inqMethod.invoke(x, o));
440                             } catch (Exception e) {
441                                 System.out.println(e.getCause());
442                             }
443                         }
444                     }
445                 }
446             } catch (ClassNotFoundException cnfe) {
447                 System.out.println("   -- ExtendedGSSContext not available");
448             }
449         }
450         if (cred != null) {
451             try {
452                 Class<?> clazz2 = Class.forName("com.sun.security.jgss.ExtendedGSSCredential");
453                 if (!clazz2.isAssignableFrom(cred.getClass())) {
454                     throw new Exception("cred is not extended");
455                 }
456             } catch (ClassNotFoundException cnfe) {
457                 System.out.println("   -- ExtendedGSSCredential not available");
458             }
459         }
460     }
461 
462     public byte[] wrap(byte[] t, final boolean privacy)
463             throws Exception {
464         return doAs(new Action() {
465             @Override
466             public byte[] run(Context me, byte[] input) throws Exception {
467                 System.out.printf("wrap %s privacy from %s: ", privacy?"with":"without", me.name);
468                 MessageProp p1 = new MessageProp(0, privacy);
469                 byte[] out;
470                 if (usingStream) {
471                     ByteArrayOutputStream os = new ByteArrayOutputStream();
472                     me.x.wrap(new ByteArrayInputStream(input), os, p1);
473                     out = os.toByteArray();
474                 } else {
475                     out = me.x.wrap(input, 0, input.length, p1);
476                 }
477                 System.out.println(printProp(p1));
478                 if ((x.getConfState() && privacy) != p1.getPrivacy()) {
479                     throw new Exception("unexpected privacy status");
480                 }
481                 return out;
482             }
483         }, t);
484     }
485 
486     public byte[] unwrap(byte[] t, final boolean privacyExpected)
487             throws Exception {
488         return doAs(new Action() {
489             @Override
490             public byte[] run(Context me, byte[] input) throws Exception {
491                 System.out.printf("unwrap from %s", me.name);
492                 MessageProp p1 = new MessageProp(0, true);
493                 byte[] bytes;
494                 if (usingStream) {
495                     ByteArrayOutputStream os = new ByteArrayOutputStream();
496                     me.x.unwrap(new ByteArrayInputStream(input), os, p1);
497                     bytes = os.toByteArray();
498                 } else {
499                     bytes = me.x.unwrap(input, 0, input.length, p1);
500                 }
501                 System.out.println(printProp(p1));
502                 if (p1.getPrivacy() != privacyExpected) {
503                     throw new Exception("Unexpected privacy: " + p1.getPrivacy());
504                 }
505                 return bytes;
506             }
507         }, t);
508     }
509 
510     public byte[] getMic(byte[] t) throws Exception {
511         return doAs(new Action() {
512             @Override
513             public byte[] run(Context me, byte[] input) throws Exception {
514                 MessageProp p1 = new MessageProp(0, true);
515                 byte[] bytes;
516                 p1 = new MessageProp(0, true);
517                 System.out.printf("getMic from %s: ", me.name);
518                 if (usingStream) {
519                     ByteArrayOutputStream os = new ByteArrayOutputStream();
520                     me.x.getMIC(new ByteArrayInputStream(input), os, p1);
521                     bytes = os.toByteArray();
522                 } else {
523                     bytes = me.x.getMIC(input, 0, input.length, p1);
524                 }
525                 System.out.println(printProp(p1));
526                 return bytes;
527             }
528         }, t);
529     }
530 
531     public void verifyMic(byte[] t, final byte[] msg) throws Exception {
532         doAs(new Action() {
533             @Override
534             public byte[] run(Context me, byte[] input) throws Exception {
535                 MessageProp p1 = new MessageProp(0, true);
536                 System.out.printf("verifyMic from %s: ", me.name);
537                 if (usingStream) {
538                     me.x.verifyMIC(new ByteArrayInputStream(input),
539                             new ByteArrayInputStream(msg), p1);
540                 } else {
541                     me.x.verifyMIC(input, 0, input.length,
542                             msg, 0, msg.length,
543                             p1);
544                 }
545                 System.out.println(printProp(p1));
546                 if (p1.isUnseqToken() || p1.isOldToken()
547                         || p1.isDuplicateToken() || p1.isGapToken()) {
548                     throw new Exception("Wrong sequence number detected");
549                 }
550                 return null;
551             }
552         }, t);
553     }
554 
555     /**
556      * Transmits a message from one Context to another. The sender wraps the
557      * message and sends it to the receiver. The receiver unwraps it, creates
558      * a MIC of the clear text and sends it back to the sender. The sender
559      * verifies the MIC against the message sent earlier.
560      * @param message the message
561      * @param s1 the sender
562      * @param s2 the receiver
563      * @throws java.lang.Exception If anything goes wrong
564      */
565     static public void transmit(String message, final Context s1,
566                                 final Context s2) throws Exception {
567         transmit(message.getBytes(), s1, s2);
568     }
569 
570     /**
571      * Transmits a message from one Context to another. The sender wraps the
572      * message and sends it to the receiver. The receiver unwraps it, creates
573      * a MIC of the clear text and sends it back to the sender. The sender
574      * verifies the MIC against the message sent earlier.
575      * @param messageBytes the message
576      * @param s1 the sender
577      * @param s2 the receiver
578      * @throws java.lang.Exception If anything goes wrong
579      */
580     static public void transmit(byte[] messageBytes, final Context s1,
581             final Context s2) throws Exception {
582         System.out.printf("-------------------- TRANSMIT from %s to %s------------------------\n",
583                 s1.name, s2.name);
584         byte[] wrapped = s1.wrap(messageBytes, true);
585         byte[] unwrapped = s2.unwrap(wrapped, s2.x.getConfState());
586         if (!Arrays.equals(messageBytes, unwrapped)) {
587             throw new Exception("wrap/unwrap mismatch");
588         }
589         byte[] mic = s2.getMic(unwrapped);
590         s1.verifyMic(mic, messageBytes);
591     }
592 
593     /**
594      * Returns a string description of a MessageProp object
595      * @param prop the object
596      * @return the description
597      */
598     static public String printProp(MessageProp prop) {
599         StringBuffer sb = new StringBuffer();
600         sb.append("MessagePop: ");
601         sb.append("QOP="+ prop.getQOP() + ", ");
602         sb.append(prop.getPrivacy()?"privacy, ":"");
603         sb.append(prop.isDuplicateToken()?"dup, ":"");
604         sb.append(prop.isGapToken()?"gap, ":"");
605         sb.append(prop.isOldToken()?"old, ":"");
606         sb.append(prop.isUnseqToken()?"unseq, ":"");
607         if (prop.getMinorStatus() != 0) {
608             sb.append(prop.getMinorString()+ "(" + prop.getMinorStatus()+")");
609         }
610         return sb.toString();
611     }
612 
613     public Context impersonate(final String someone) throws Exception {
614         try {
615             GSSCredential creds = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() {
616                 @Override
617                 public GSSCredential run() throws Exception {
618                     GSSManager m = GSSManager.getInstance();
619                     GSSName other = m.createName(someone, GSSName.NT_USER_NAME);
620                     if (Context.this.cred == null) {
621                         Context.this.cred = m.createCredential(GSSCredential.INITIATE_ONLY);
622                     }
623                     return (GSSCredential)
624                             Class.forName("com.sun.security.jgss.ExtendedGSSCredential")
625                             .getMethod("impersonate", GSSName.class)
626                             .invoke(Context.this.cred, other);
627                 }
628             });
629             Context out = new Context();
630             out.s = s;
631             out.cred = creds;
632             out.name = name + " as " + out.cred.getName().toString();
633             return out;
634         } catch (PrivilegedActionException pae) {
635             Exception e = pae.getException();
636             if (e instanceof InvocationTargetException) {
637                 throw (Exception)((InvocationTargetException) e).getTargetException();
638             } else {
639                 throw e;
640             }
641         }
642     }
643 
644     public byte[] take(final byte[] in) throws Exception {
645         return doAs(new Action() {
646             @Override
647             public byte[] run(Context me, byte[] input) throws Exception {
648                 if (me.x.isEstablished()) {
649                     System.out.println(name + " side established");
650                     if (input != null) {
651                         throw new Exception("Context established but " +
652                                 "still receive token at " + name);
653                     }
654                     return null;
655                 } else {
656                     if (me.x.isInitiator()) {
657                         System.out.println(name + " call initSecContext");
658                         return me.x.initSecContext(input, 0, input.length);
659                     } else {
660                         System.out.println(name + " call acceptSecContext");
661                         return me.x.acceptSecContext(input, 0, input.length);
662                     }
663                 }
664             }
665         }, in);
666     }
667 
668     /**
669      * Saves the tickets to a ccache file.
670      *
671      * @param file pathname of the ccache file
672      * @return true if created, false otherwise.
673      */
674     public boolean ccache(String file) throws Exception {
675         Set<KerberosTicket> tickets
676                 = s.getPrivateCredentials(KerberosTicket.class);
677         if (tickets != null && !tickets.isEmpty()) {
678             CredentialsCache cc = null;
679             for (KerberosTicket t : tickets) {
680                 Credentials cred = Krb5Util.ticketToCreds(t);
681                 if (cc == null) {
682                     cc = CredentialsCache.create(cred.getClient(), file);
683                 }
684                 cc.update(cred.toCCacheCreds());
685             }
686             if (cc != null) {
687                 cc.save();
688                 return true;
689             }
690         }
691         return false;
692     }
693 
694     /**
695      * Handshake (security context establishment process) between two Contexts
696      * @param c the initiator
697      * @param s the acceptor
698      * @throws java.lang.Exception
699      */
700     static public void handshake(final Context c, final Context s) throws Exception {
701         byte[] t = new byte[0];
702         while (true) {
703             if (t != null || !c.x.isEstablished()) t = c.take(t);
704             if (t != null || !s.x.isEstablished()) t = s.take(t);
705             if (c.x.isEstablished() && s.x.isEstablished()) break;
706         }
707     }
708 }
709