Pull to refresh

Подписывание Java апплета и некоторые тонкости java security

Reading time 10 min
Views 26K
Постановка задачи:

В качестве WYSIWYG XML редактора в нашем приложении используется java applet Oxygen Author Component. При загрузке этого апплета на клиенте Java не должна выкидывать никаких пугающих варнингов о небезопасном коде, а спокойно и тихо загружать себе апплет, не напрягая пользователя и не заставляя брать на себя тяжелую ответственность. У нас ведь солидное приложение как никак.


Пролог

Статья не претендует на научную выверенность и точность, допускаю какие-то некорректности в определениях. Википедию не открывал, пишу, так сказать, от сердца, но замечания принимаются.

Как работает апплет на пальцах

Браузер при обнаружении тэга в HTML странице передает управление загрузке апплета соответствующему java-плагину, который, в свою очередь передает управление JRE установленному на клиентской машине. Есть два способа загрузки апплета (под апплетом понимается некое java приложение, представляющее из себя набор jar-ок, где есть main jar с Main class реализующим класс Applet):

  • в тэге Applet перечесляются jar файлы апплета, которые нужно загрузить и откуда их грузить (codebase)
  • с помощью jnlp файла, в котором указывается эта информация, а также много других опций и аргументов

В нашем случае мы используем jnlp. Преимуществом jnlp подхода помимо прочих является то, что при изменении каких-то параметров загрузки апплета (например, codebase) не нужно менять HTML или JavaScript код, ответственный за загрузку апплета. Достаточно просто поменять jnlp. Замена codebase (codebase — это url адрес указывающий на то где лежат jar-ки апплета) является даволно неприятной проблемой, так как нужно указывать абсолютный URL, а это значит, что в зависимости от того на каком сервере запущен апплет, такой и должен быть codebase. На локальной машине это один адрес, на QA стенде другой, на продакшене третий, поэтому при сборке приложения нужно явно указывать Context Path, то есть абсолютный адрес web-приложения, на котором оно будет работать.
Использование jnlp файла решает эту проблему тем, что есть специальный сервлет, суть работы которого — динамически, на лету, менять codebase на текущий при загрузке jnlp файла клиентской явой.

Поехали

Итак, загрузка апплета начинается с загрузки jnlp файла, в котором кроме списка нужных jar файлов и их codebase указываются также аргументы java (это очень важный момент, ниже узнаете почему), с которыми эта java должна стартовать на клиентской машине. Java начинает загружать jar-ки и проверять их на безопасность. Тут вступает в силу java security механизм, который начинает проверку загружаемой жарки, прежде чем из нее будут загружены классы. Этот механизм давольно сложный и многогранный: настройка общего уровня безопасности при загрузке апплетов и java security policy на клиенте (permission grants), параметры безопасности в манифестах самих jar, проверка верификация цифровой подписи jar и тд. Не хочу углублятся и буду затрагивать только те аспекты, которые важны в описываемой задаче.

Для справки: подписанние апплета подразумевает подписание jar-фалов. Подписание jar — это выполнение java-команды signjar, в результате которой в jar-ке появляется информация о ключе, которым подписывали в зашифрованном виде, а также каждому ресурсу упакованному jar ставится в соответствие нейкий зашифрованный этим ключом код, содержащи в себе информацию о контенте данного ресурса. Таким, образом, если вы попытаетесь изменить подписанную жарку, подкинув например туда какой-нибудь класс, или изменив старый, то такая жарка станет невалидной и она не пройдет проверку на security и не будет загружена.


Итак, апплет может быть не подписанным, самоподписанным и подписанным доверенным сертификатом. В зависимости от версии клиентской явы, на которой апплет загружается, уровень подписанности апплета влияет на то будет ли апплет вообще загружен или заблокирован, загружен но с кучей варнингов и сообщений типа «Если вы согласитесь запустить этот апплет, то наступит глобальный катаклизм и вообще все пропадет, так что запускайте на свой страх и риск»,

image

или загружен с одним красивым и непугающим сообщением, которое можно не повторять в дальнейшем нажав соответствующую галочку. Вот тут мы подобрались к, собственно, задаче. На момент решения описываемой задачи наш Oxygen апплет был самоподписанным. То есть подпись была, но фэйковая — ключи были сгенерены нами же при помочи утилиты keytool.
Естественно, при загрузке такого апплета, java вбрасывала фекалии в вентелятор и ругалась матом, хоть и загружала апплет в конечном счете. Но нам нужно было от этих сообщения избавится, поэтому нам нужен был сертификат от доверенного центра сертификации (Trusted Certifictae Authority).

Итак нам нужен сертификат от доверенного центра. Как его получить? Легко, но удовольствие не дешевое. Сертификат сроком на год будет стоить порядка 500 американских. Мы воспользовались услугами центра www.verisign.com. Нужно создать keystore со своим альясом и другой информацией о паблишере (при помощи утилиты keytool) и сформировать специальный request (и конечно, заплатить). В ответ CA присылает ключи. Наш прислал аж 3 штуки: Code Signing certificate, intermediate CA certificate и certificate in pkcs7 format. Для JKS типа сертификата нам понадобятся intermediate и Code Signing certificate. Сначала в созданый ранее keystore добавляется intermediate certificate, а затем основной Code Signing certificate. Полученный keystore и будет использован для подписания jar-ок.

Если ваши жарки ранее были подписаны (а в моем случае были подписаны, точнее самоподписаны), то прежде чем подписывать их заново, нужно сначала удалить старые подписи. Если вы это не сделаете, то при загрузке апплета java захлебнется на первой же жарке и пошлет вас куда подальше. Удалить подпись можно, например, руками — удалить файлы .RSA (или .DSA) и .SF из папки META-INF, а также поудалять все Digest подписи ресурсов из файла манифеста.
Чуть не забыл: перед подписывании jar в манифест нужно добавить security attributes:

Permissions: all-permissions
Codebase: *
Caller-Allowable-Codebase: *
Application-Library-Allowable-Codebase: *


Начиная с 51 апдейта 7 явы все jar-ки не содержащие security attributes автоматически будут блокироваться.
А вот и ант скриптик для этого:

  <target name="addSecurityProperty">
	        <jar file="${jarFile}" update="true">
	            <manifest>
	                <attribute name="Permissions" value="all-permissions"/>
	                <attribute name="Codebase" value="*"/>
	              <attribute name="Application-Library-Allowable-Codebase" value="*"/>
	              <attribute name="Caller-Allowable-Codebase" value="*"/>
	            </manifest>
	        </jar>
	    </target>
	  <target name="addSecurityProperties" if="hasForEach">
	    <foreach target="addSecurityProperty" param="jarFile">
	      <path>
	        <fileset dir="lib" includes="**/*.jar, **/*.zip"/>
	      </path>
	    </foreach>
	  </target>


Важно: вам понадобится antcontrib для возможности использование foreach в данном скрипте.

Итак, чистим жарки, добавляем в манифесты нужные атрибуты, подписываем, запускаем и… Опять видим варнинг!

image

Что опять? Оказывается, что подписывать нужно не только жарки, но и jnlp файл. А как его подписать? А так. Кому лень читать: jnlp файл кидается в вашу main jar в дирикторию JNLP-INF и название файла должно быть именно такое: APPLICATION.JNLP. Не вопрос, добавляем в наш ant скрипт, билдящий main jar и подписывающий jar-ки, незамысловатый код который копирует наш исходный jnlp в подписанный jnlp.

	<target name="compile">
		<mkdir dir="classes"/>
		<javac srcdir="src" destdir="classes" includeantruntime="false" debug="on">
			<classpath>
				<fileset dir="lib">
					<include name="*.jar"/>
				</fileset>
			</classpath>			
		</javac>

		<mkdir dir="classes/JNLP-INF"/>
		<copy file="author-component-dita.jnlp" tofile="classes/JNLP-INF/APPLICATION.JNLP" overwrite="true"/>
	</target>


Отцы сразу разберутся, но я все же поясню, на всякий случай, что происходит в этом антовском таргете. В первой части все ясно — создаем дирикторию classes и компилируем туда код нашего апплета. Обратите внимание, что при компиляции в класспас добавляется папочка lib, в которой лежит куча jar-ок, нужных апплету и их всех нужно подписывать. Далее там же создается папка JNLP-INF и туда копируется наш исходный
author-component-dita.jnlp.
Далее мы упаковываем все это в jar (это и есть наш main jar) и кидаем его в ту же папку lib рядом с остальными jar-ками.
Получается что сейчас у нас есть 2 jnlp файла: исходный author-component-dita.jnlp и APPLICATION.JNLP упакованный в jar. Это меня немного смущает, ну ладно. Запускаю апплет — error!

image

Что на этот раз? Оказывается, что эти jnlp файлы не совпадают, а должны. Исходный jnlp используется для загрузки апплета, а упакованный — для проверки подписи и они не должны отличатся. Но почему они отличаются, они же копии? И тут вспоминаем наш чудный сервлет (JnlpDownloadServlet ), который используется для облегчения деплоя вашего веб приложения — я уже упоминал об этом выше. С помощью него можно не писать в jnlp конкретный codebase (например, localhost:8888/oxygen-editor/), а использовать переменную $$CODEBASE, а сервлет на рантайме сам изменяет jnlp подставляя в нее нужные значения переменных. Вот почему загружаемый jnlp не совпадает с подписанным. Что же делать? Деплоить для разных адресов сервера разные варки? Это не наш путь. Все просто: нужно использовать APPLICATION-TEMPLATE.JNLP вместо APPLICATION.JNLP. Использование шаблона APPLICATION-TEMPLATE.JNLP отличается тем, что он может отличастся от исходного jnlp, если вместо конкретных значений параметров указывать "*", например, codebase="*". Видоизменим наш антовский build.xml:


	<target name="compile">
		<mkdir dir="classes"/>
		<javac srcdir="src" destdir="classes" includeantruntime="false" debug="on">
			<classpath>
				<fileset dir="lib">
					<include name="*.jar"/>
				</fileset>
			</classpath>			
		</javac>

		<mkdir dir="classes/JNLP-INF"/>
		<copy file="author-component-dita.jnlp" tofile="classes/JNLP-INF/APPLICATION-TEMPLATE.JNLP" overwrite="true"/>
		<replace file="classes/JNLP-INF/APPLICATION-TEMPLATE.JNLP" token="@@CODEBASE@@" value="*"/>
		<replace file="classes/JNLP-INF/APPLICATION-TEMPLATE.JNLP" token="@@HREF@@" value="*"/>

	</target>



Итак, неужели это все и я наконец увижу при загрузке апплета долгожданное user-friendly сообщение с синим щитом о том, что апплет доверенный, надежный и не вызывает подозрений? Не веря в свое счастье, дрожащими руками запускаю приложание, загружаю апплет и…

image

Ура! Свершилось! Я увидел наконец это сообщение с синим щитом! И разбрызгивая слюни радости я нажимаю галочку «Всегда доверять этому паблишеру», оно закрывается, начинает грузится апплет и… тут появляется это:

image

Что за ...!? Стремительно седея, начинаю лихорадочно втыкать в код — что же там такого несекьюрного? В общем, я убил еще 2 дня, чтобы найти вот эту строчку в jnlp файле:

<j2se java-vm-args="-Xmx512m -XX:MaxPermSize=80m -Xss4m -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5021" version="1.6+" />


Это java аргументы, с которыми запускается апплет. Так вот, аргументы дебага являются несекьюрными. И если ваш апплет подписан, причем доверенным сертификатом, эти аргументы запрещены. Что интересно, если ваш апплет самоподписан, то запускайте апплет хоть с чертом лысым, клиент ведь предупрежден, что исполняемое приложение неизвестно, а значит изначально опасно.

Вот полный список запрещенных аргументов:

// note: this list MUST correspond to native secure.c file
private static String[] secureVmArgs = {
    "-d32",                         /* use 32-bit data model if available */
    "-client",                      /* to select the "client" VM */
    "-server",                      /* to select the "server" VM */
    "-verbose",                     /* enable verbose output */
    "-version",                     /* print product version and exit */
    "-showversion",                 /* print product version and continue */
    "-help",                        /* print this help message */
    "-X",                           /* print help on non-standard options */
    "-ea",                          /* enable assertions */
    "-enableassertions",            /* enable assertions */
    "-da",                          /* disable assertions */
    "-disableassertions",           /* disable assertions */
    "-esa",                         /* enable system assertions */
    "-enablesystemassertions",      /* enable system assertions */
    "-dsa",                         /* disable system assertione */
    "-disablesystemassertions",     /* disable system assertione */
    "-Xmixed",                      /* mixed mode execution (default) */
    "-Xint",                        /* interpreted mode execution only */
    "-Xnoclassgc",                  /* disable class garbage collection */
    "-Xincgc",                      /* enable incremental gc. */
    "-Xbatch",                      /* disable background compilation */
    "-Xprof",                       /* output cpu profiling data */
    "-Xdebug",                      /* enable remote debugging */
    "-Xfuture",                     /* enable strictest checks */
    "-Xrs",                         /* reduce use of OS signals */
    "-XX:+ForceTimeHighResolution", /* use high resolution timer */
    "-XX:-ForceTimeHighResolution", /* use low resolution (default) */
    "-XX:+PrintGCDetails",          /* Gives some details about the GCs */
    "-XX:+PrintGCTimeStamps",       /* Prints GCs times happen to the start of the application */
    "-XX:+PrintHeapAtGC",           /* Prints detailed GC info including heap occupancy */
    "-XX:PrintCMSStatistics",       /* If > 0, Print statistics about the concurrent collections */
    "-XX:+PrintTenuringDistribution",  /* Gives the aging distribution of the allocated objects */
    "-XX:+TraceClassUnloading",     /* Display classes as they are unloaded */
    "-XX:SurvivorRatio",            /* Sets the ratio of the survivor spaces */
    "-XX:MaxTenuringThreshol",      /* Determines how much the objects may age */
    "-XX:CMSMarkStackSize",
    "-XX:CMSMarkStackSizeMax",
    "-XX:+CMSClassUnloadingEnabled",/* It needs to be combined with -XX:+CMSPermGenSweepingEnabled */
    "-XX:+CMSIncrementalMode",      /* Enables the incremental mode */
    "-XX:CMSIncrementalDutyCycleMin",  /* The percentage which is the lower bound on the duty cycle */
    "-XX:+CMSIncrementalPacing",    /* Automatic adjustment of the incremental mode duty cycle */
    "-XX:CMSInitiatingOccupancyFraction",  /* Sets the threshold percentage of the used heap */
    "-XX:+UseConcMarkSweepGC",      /* Turns on concurrent garbage collection */
    "-XX:-ParallelRefProcEnabled",
    "-XX:ParallelGCThreads",        /* Sets the number of parallel GC threads */
    "-XX:ParallelCMSThreads",
    "-XX:+DisableExplicitGC",       /* Disable calls to System.gc() */
    "-XX:+UseCompressedOops",       /* Enables compressed references in 64-bit JVMs */
    "-XX:+UseG1GC",
    "-XX:GCPauseIntervalMillis",
    "-XX:MaxGCPauseMillis"          /* A hint to the virtual machine to pause times */
};


Спасибо за внимание, надеюсь эта статья будет полезной.
Tags:
Hubs:
+14
Comments 9
Comments Comments 9

Articles