Обновление сервисов, запущенных на Go

Я люблю программировать на Go, но больше всего сейчас мне нравится программировать в gobot для Raspberry Pi. Каждое изменение в коде требует определенное время на нудные операции, связанные с обновлением кода. Сначала я должен остановить процесс, так как Filezilla отказывается писать в исполняемый файл, когда процесс запущен, загрузить новый исполняемый файл по SFTP и запустить его (это не только нудно, но еще 10-20 секунд простоя, когда процесс остановлен).

Аналогичная ситуация меня преследует и при разработке для обычного веба на Go. Именно в gobot я вынужден очень часто обновлять код, что связанно со стилем разработки, который приносит мне удовольствие в свободное время. С разработкой нового пакета обновлять код, написанный на Go стало проще и быстрее.

Сейчас расскажу, как пользоваться моим пакетом и как он устроен.

go get github.com/CossackPyra/updater  

Этой командой вы установили мой updater в ваш Go, также вы можете установить тулзу в командную строку следующей командой:

go get github.com/CossackPyra/updater/pyra-poster

Вот пример веб-сервиса с возможностью обновления:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "github.com/CossackPyra/updater"
)

func main() {

    http.HandleFunc("/", handle_def)

    os.Mkdir("tmp", 0700)
    u1 := updater.UpdaterServer("tmp", []byte("1234567890123456"), os.Args[0])
    http.Handle("/updater-me", u1)
    http.ListenAndServe(":9999", nil)
}

func handle_def(w http.ResponseWriter, r *http.Request) {
    fmt.Println("def ", r.Method)
    io.WriteString(w, "v 24\n")
    io.WriteString(w, r.URL.Path)
    println(r.URL.Path)
}

Именно следующие 3 строчки добавляют возможность обновлять программу:

    "github.com/CossackPyra/updater"
...
    u1 := updater.UpdaterServer("tmp", []byte("1234567890123456"), os.Args[0])
    http.Handle("/updater-me", u1)

В данном случает все шифруется 16-байтовым ключом []byte(«1234567890123456»), а 31323334353637383930313233343536 это его шестнадцатеричное представление. new-service новый исполняемый файл, и вот команда, чтобы обновить старый процесс.

pyra-poster 31323334353637383930313233343536 new-service http://127.0.0.1:9999/updater-me

Как это работает?


Весь код уместился в 280 строках. У меня больше доверия в коду, в котором можно быстро разобраться. Я не до конца уверен в криптографической стойкости моего алгоритма шифрования и как вообще можно быть уверенным в чем-то подобном, поэтому мне интересно его обсудить с вами.

При инициализации хендлера в updater.UpdaterServer, генерится случайная последовательность 16 байт (rand1). Для того, чтобы обновить код, надо выполнить два запроса к хендлеру. Первый GET запрос получает эти случайные 16 байт (rand1). Случайные 16 байт (rand1) и секретный пароль (key1) используются для того, чтобы зашифровать отправляемый исполняемый файл алгоритмом AES и отправить шифрограмму методом POST.

Сначала буфер наполняется данными согласно следующему рецепту:
— Случайные 20 байт (это не rand1, а rand0 — в настоящий момент они никак не используются);
— 20 байт — хеш исполняемого файла sha1;
— «pyra-poster» — последовательность байт, название протокола;
— 6 байт (1 — int16 Little Endian, 0 — int32 Little Endian) — версия протокола;
— сам исполняемый файл.

На этот буфер налагается гамма-функция методом xor, полученная блочным шифром AES(key1, rand1). В результате получается, шифротекст, который отправляется в хендлер методом POST.

На сервере для дешифрования на последовательность накладывается гамма AES(key1, rand1), проверяется заголовок «pyra-poster»(1,0), исполняемый файл сохраняется во временную папку и вычисляется хеш по алгоритму sha1 и сравнивается со sha1 из заголовка. Если хеши равны, то можно полагать с большой вероятностью, что это аутентичный файл созданный хозяином сервиса.

После обновления сервиса rand1 меняется и поэтому, перехватив шифротекст, повторно загрузить его нельзя. Рассчитать key1, имея rand1, и зная особенность заголовка «pyra-poster»(1,0) или, даже имея исполняемый файл, также достаточно сложно.

16-байтовый (128 бит) ключ key1 можно заменить на 32-байтовый (256 бит) без каких-либо дополнительных изменений в коде. Большинство веб броузеров как раз используют шифрование AES 256 бит.

Пользуйтесь на здоровье.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 43
    0
    А почему бы не использовать обычные scp (rsync) для копирования и ssh для перезапуска сервиса? А для автоматизации написать трехстрочный скриптик на баше.
      +1
      так все работает быстрей, все реализовано в рамках кода написанного на Go, все что надо знать, что бы обновить это урл и ключ.

      трехстрочный скриптик — будет как средний init.d скрипт + обновление, я не против, но мне захотелось так, наверняка скрипт на баше имеет право на жизнь, я бы на него посмотрел бы с не меньшим удовольствием
        0
        (go build; cat service_binary) | ssh user@server 'cd SERVICE_DIR/; killall ./service; cat > ./service; setsid ./service'
      0
      Если я правильно понял, у вас «клиент», запихивает новую версию бинарника сервиса в сам сервис. И соответственно основная сложность — это проверка аутентичности нового бинарника. Отсюда вопрос — почему не сделать обратную схему — запрашивать обновления со стороны сервиса?
        0
        1. Если сервис будет запрашивать сам, то все равно, хотелось бы шифровать, что бы ни кто не подслушал и не подсунул на запрос не то.
        2. я разрабатываю код и обновляю двумя командами
        go build
        pyra-poster 31323334353637383930313233343536 new-service http://my-robot:9999/updater-me
        

        т.е. я все равно как то должен сказать роботу, что бы он само обновится и вероятно, я должен сделать откомпелированный файл доступным например через веб.
          0
          1. От подслушиваня проще защитися tls
          2. так и в этом случае будет одна комманда —
          pyra-getter my-desktop.local:9999/proj-name

          а там запущен сервер который делает cd proj-name && go build и отдает бинарник
            0
            если упростить, то TLS обменивается ключами по RSA, что бы потом шифровать поток тем же AES.

            в маштабах интернета, это правильное решение, а если я лично хочу обновлять прошивку робота на RaPI по вайфай, то мне достаточно просто AES без RSA. (что я такое говорю, можно и не шифровать)

            RSA понадобится если я сделаю армию роботов, и захочу захватить мир, но при этом поместить в них один и тот же ключ!, тогда симметричных ключ глупость и следует использовать криптографию с открытым ключом.

            Я полагаю AES сам по себе надежней чем TLS
            надежность(TLS) = надежность(AES) * надежность(RSA)
            а 100% надежности не бывает
              +1
              «Вы опасно некомпетентны в криптографии»
              habrahabr.ru/post/181372/

              вы самостоятельно реализовали шифрование в виде AES в CFB режиме плюс SHA1 как MAC,
              наверняка это все подвержено timing-атакам, «padding oracle» и прочему
              проверка валидности заголовка тоже не внушает доверия (строка 133 и вокруг нее)

              не лучше ли просто использовать с обоих сторон стандартный человеческий TLS?
                0
                я же написал
                Я не до конца уверен в криптографической стойкости моего алгоритма шифрования


                Но все же я надеялся, увидеть в чем он слаб, а не ссылку на статью, которая своим названием уже ставит точку

                В свое оправдание хочу привести пример из документации Go
                golang.org/pkg/crypto/cipher/#NewCFBEncrypter (нажмите Example)

                Точно этим коментом вы можете критиковать и этот пример (хотя я проверяют исполняемый файл с помощью sha1)
                Внетерпении от конструктивной критики
                  0
                  Пример атаки:
                  1. Атакер снифает одну сессию. Из неё узнает кусок гаммы (pyra-poster + версия + хедер бинаря + паттерны в бинаре и т.д. и т.д.). Затем выбирает, например, 5й блок шифротекста в качестве «злого» IV.
                  2. Потом MITMом атакер вклинивается и подсовывает клиенту свой IV (он же rand1). Из структуры AES-CFB следует, что гамма просто «сдвинулась» назад на 5*16 байт. Поэтому атакер может расшифровать ранее неизвестные ему куски бинаря, используя известные ему части гаммы. Более того, он узнаёт новые части гаммы из того же запроса по предыдущей методике и расшифровывает куски из первого запроса.

                  Затем атакер может перехватывать каждую последующую сессию и подсовывать клиенту тот же IV и вытаскивать данные новых бинарей.

                  3. Атакеру всё мало, он запрашивает у сервера новый rand1. Отдаёт клиенту свой старый IV для которого он умудрился вычислить большую часть гаммы. Клиент шифрует свой бинарь. Бинарь вышел небольшим, и атакер восстанавливает его весь, а значит он знает его sha1 хэш. Клиент получает «ошибка, рандом протух. перезапустите скрипт». Клиент перезапускает скрипт а атакер подсовывает ему новый, реальный rand1 от сервера. Клиент шифрует тот же бинарь. Но атакер знает теперь всё что после rand0, поэтому узнаёт всю нужную гамму. И допивая рутбир зафигачивает реверс-шелл на сервачок.

                  PS: В идеале нужно использовать библиотечное Authenticated Encryption. Иначе, самое простое и надежное что можно придумать — encrypt-then-mac (что и является по сути AE, главное именно в таком порядке). Шифруем только бинарь, затем навешиваем (H)MAC. На сервере первым делом проверяем MAC. Для ограничения реплеев можно использовать тот самый «rand1» но ни в коем случае не в качестве IV/параметра шифра, а просто закинуть под HMAC (можно даже не шифровать). Можно обойтись без первого GETa, используя в качестве rand1 таймстамп. Хотя девелоперы могут расстроиться из-за расстроеного времени на сервере.

                  RSA тут не нужно, т.к. ключ клиент заливает на сервер по защищенному каналу (будем надеяться..). Хотя в качестве подписи это надежнее в том плане, что если кто то читанет исходники на сервере — узнает только публичный ключ.
                    +1
                    Спасибо, вы меня очень озадачили, спасибо. Первый достойный коммент.
                    Даже если подменой IV можно было сдвигать AES, то я бы мог реализовать гамму которую нельзя сдвинуть зная IV просто добавляя позицию в файле.

                    я посмотрел реализацию XORKeyStream, ее нельзя сдвинуть

                    golang.org/src/crypto/cipher/cfb.go#L34 — строка 34

                    dst (результат xor) копируется в next

                    на строка 21, next используется для каждого следующего шифроблока out
                      +1
                      Да, я накосячил :( Гамма зависит от шифротекста и можно так узнать только rand0 от клиента, после него гамма изменится.
                      0
                      Знания впечатляющие! Посоветуйте книжек чтоль?
                    0
                    В CFB не нужен padding, поэтому «padding oracle» конечно же не в тему. SHA1 по моему тут используется тупо как хэш («хеш исполняемого файла»). Автор пусть почитает про MAC/HMAC.
                      0
                      спасибо, что вы отбросили padding oracle, а зачем тут HMAC?

                      Я передал шифротекст, сервер декодировал шифротекст, используя AES(key1, rand1), дешифрованный текст содержит, sha1 и сам файл, если ключ не верен, то врядли sha1 соответствует файлу. Т.е. что бы зашифровать
                      текст надо знать key1

                      атака 1. перебрать все возможные key1, декодировать и сравнить sha1 и файл. (наверное это сложно)

                      атака 2. слабость алгоритма может быть в том, что зная текст и шифротекст (или частично зная текст), я могу получить key1, но ведь TSL после обмена ключами, данные шифруются как раз AES, а по TSL часто передаются данные где мы знаем, и текст и шифротекст, например часто пользователь первым запросом открывает домашнюю страницу сайта.

                        0
                        Я так понимаю вы про TLS. Там тоже используется MAC.

                        С sha1 у вас получилось hash-then-encrypt. Правильно делать encrypt-then-mac. Сил столько же, зато секурно. Одну из возможных атак я описал выше.
                          0
                          Мне идея атаки понравилась, но она не будет работать не только потому, что разработчик должен сделать n не сработавших попыток, при подмене IV(rand1)
                          или потому, что реализация XORKeyStream не позволяет сделать сдвиг при подмене IV,
                          но и потому, что при каждой отсылке файла, первыми 20 байтами являются rand0 которые будут разные каждый раз

                          шифр я называл AES(key1, rand1), но правильней было писать AES(key1, rand1, rand0), и клиент расcчитывает rand0, перед каждойпопыткой отправить файл.
            +1
            Забавно, что для самого обновления вы делаете exec() с аргументами. Это не создает проблем с утеканием файловых дескрипторов только лишь потому, что go runtime помечает все файловые дескрипторы флагом O_CLOEXEC, чтобы они закрывались при exec-е. Это достаточно уникально для Go, и в других языках так просто обновление сделать не получится.
              0
              Верно! ведь это мой третий полученный инвайт, а вот второй инвайт я получил за Изящный вебсервер на Go (Graceful Restart), где перед exec я делал
                      fd2, err := syscall.Dup(fd1)
              
              и передавал открытый порт
              0
              Как себя показывает gobot на Raspberry Pi, arduino. Влазит по ресурсу, не тормозит? Полная спецификация языка поддерживается, весь stdlib? Пожалуйста поделитесь ощущениями.
                0
                В RaPI 512MB и там точно такой Go как везде, там можно компелировать Go и даже Docker, но я компелирую на ноуте под ARM и вот таким образом обновляю.

                gobot с arduino общается по протоколу firmata (похож на MIDI но вместо звуков мигает диодом и двигает серво)
                0
                Ваш сервис должен иметь право на запись в каталог, где лежит его бинарник? Кажется, это довольно суровое требование…
                  0
                  Давид, а как иначе?
                    0
                    Ну, в рамках одного процесса, конечно, никак.

                    Но обычно всё-таки бинарник подменяют другим процессом, с более высоким приоритетом, а потом дают работающему сервису USR2.

                    Как было бы весело, если бы апач умел писать в /usr/bin/…
                      0
                      Но обычно всё-таки бинарник подменяют другим процессом, с более высоким приоритетом, а потом дают работающему сервису USR2
                      Это вот типа такого решения?
                      Как было бы весело, если бы апач умел писать в /usr/bin/
                      Ну, go-приложения не принято в /usr/bin вроде как помещать…
                        +2
                        Там как-то слишком сложно написано, есть и для Go библиотечки. Например, github.com/facebookgo/grace/gracehttp

                        А почему не принято, чем они хуже любых других экзешников? GOPATH/bin — он для разработки, а для распространения есть стандартные пути.
                  0
                  Если пользователь alex загрузил файл app1 по scp, и сделал chmod a+x app1 & ./app1, то app1 имеет все те права, что alex, app1 может создать рядом файл app2, chmod a+x app2 & ./app2

                    0
                    Только это очень плохая и опасная практика, и алексу в боевом окружении лучше бы её не применять.
                      –1
                      Ваш сервис должен иметь право на запись в каталог, где лежит его бинарник

                      Я не про то, что принимать или нет, а про то, что по умолчанию, у процесса есть такие права, как писать в файл и давать этому файлу права на запуск. Процесс может даже удалить свой исполняемый файл (на линуксе) по сути моя программа удаляет старый когда двигает новый из временной папки. Из за того, что это новый файл я делаю chmod a+x
                        +2
                        Тут нет никакого «по умолчанию». Вы описали один конкретный способ деплоя («руками залить файлик по FTP»), который годится максимум для собственной разработки в режиме отладки. Но Вы же из него сделали пакет, чтобы и другие люди пользовались, в боевых условиях. А в боевых условиях это либо не будет работать из-за прав доступа, либо, если такие права дать, обернётся огромной дырой в безопасности.
                          0
                          Возможно, кто то будет использовать его для аналогичных целей.

                          Если очень хочется, то всегда можно в Go включить HTTP вместо HTTPS, так код реализован на уровне http.Handler

                          Я хочу услышать конструктивную критику.
                          Я не спорю, ведь мы свидетели того, что дыры были стандартах принятых индустрией, стандарты эти рассматривали
                          тысячи специалистов.

                          Вот на хабре тоже ведь есть специалисты по криптографии, которые могут конструктивную критику привести.
                            –1
                            Ладно, я достаточно написал. Вы, похоже, не понимаете, зачем вообще нужны права доступа, и Вам они только мешают.
                              0
                              не надо мне приписывать, что я не говорил, это очень не приятно, я ответил на ваш тезис про папку и права.

                              земной пример.

                              исходные данные

                              хостинг php, cgi-bin, suexec
                              папка cgi-bin
                              пользователь alex

                              пишем в cgi-bin скрипт или исполняемый файл написанный на Go

                              о кошмар этот скрипт запускается от имени alex и обладает теме же правами, что и FTP сервер в папке этого алекса. Может обновлять файлы пхп, сам себя, менять атрибуты файлов и папок!

                              не для слабонервных
                              WordPress умеет сам себя обновлять, как теперь спать? это 74,652,825 сайтов!!! (конечно если пхп под nobody запушен, то не обновить вордпресс, но месяц назад ставил Cpanel и там phpsuexec и WordPress сам обновляется)
                                +1
                                Именно поэтому самый популярный канал взлома php-шных сайтов — заливка php-скриптов под видом данных туда, где они могут исполняться.
                              +1
                              По-моему, это вполне конструктивное замечание. Лучше «допилить» метод деплоя ддля использования в боевых условиях, чем городить шифрование. Разве нет?
                                0
                                я не готов утверждать, но возможно это шифрование, достаточно безопасное

                                по существу тут был один коммент от hellman, который предлагал алгоритм взлома, но после того, как я посмотрел исходник реализации XORKeyStream, то я считаю, что описанную атаку провести нет возможности.

                                  0
                                  > возможно это шифрование, достаточно безопасное

                                  А возможно, позволит взломать сервер. Это кому-нибудь надо? Почему кто то должен полагаться на ваши крипто изыскания?
                                    0
                                    Я не до конца уверен в криптографической стойкости моего алгоритма шифрования


                                    Я достаточно критично отношусь к этому коду, и мне будет очень интересно увидеть то, как его можно взломать.

                                    Если в результате, статьи кто то найдет дыру — то отлично, это классный опыт
                                    Если не найдут, то этот код можно считать чуть надежней

                                    По сути если отбросить не конструктивные комменты, это есть открытая экспертиза, лучшая на, которую я мог только расчитывать.
                    0
                    Для удаленной разработки на слабом устройстве самый удобный вариант — NFS директория. Прошивку для камеры мы именно так разрабатываем.
                      +3
                      Слишком сыро для того, чтобы постить на Хабр. И уж лучше совсем без шифрования, чем с реализованным таким образом.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое