mirror of
https://github.com/esp8266/Arduino.git
synced 2025-04-21 10:26:06 +03:00
Remove dependency on SD/SPIFFS from CertStore (#4760)
Due to popular demand, remove the hardcoded dependency on SPIFFS or SD from the CertStore by factoring out the file interface into a new class (CertStoreFile) that the user will need to implement as a thin wrapper around either a SPIFFS.file or a SD.file Combine the downloaded certificates into a UNIX "ar" archive and parse that on-the-fly to allow easy inspection and creation of the Cert Store database. Examples updated with a new certificate downloader that creates the certs.ar archive and with a single sample that can be built for either SPIFFS or SD with a #define. Users can copy the implementation of the CertStoreFile they need to their own code as it is self-contained. Also move the CertStore to the BearSSL namespace and remove the suffix and separate SPIFFS/SD sources. Remove the "deep+" change from the CI build as well (no special options needed on any PIO or makefile build). We'll revisit the filesystem wrapper for 2.5.0, hopefully having a unified template for both filesystem usage at a global level. For current users, be aware the interface may change (simplify!) in release 2.5.0. Fixes #4740
This commit is contained in:
parent
c0cfe875c2
commit
794630e068
@ -7,7 +7,7 @@
|
|||||||
//
|
//
|
||||||
// Why would you need a CertStore?
|
// Why would you need a CertStore?
|
||||||
//
|
//
|
||||||
// If you know the exact serve being connected to, or you
|
// If you know the exact server being connected to, or you
|
||||||
// are generating your own self-signed certificates and aren't
|
// are generating your own self-signed certificates and aren't
|
||||||
// allowing connections to HTTPS/TLS servers out of your
|
// allowing connections to HTTPS/TLS servers out of your
|
||||||
// control, then you do NOT want a CertStore. Hardcode the
|
// control, then you do NOT want a CertStore. Hardcode the
|
||||||
@ -15,7 +15,7 @@
|
|||||||
//
|
//
|
||||||
// However, if you don't know what specific sites the system
|
// However, if you don't know what specific sites the system
|
||||||
// will be required to connect to and verify, a
|
// will be required to connect to and verify, a
|
||||||
// CertStore{SPIFFS,SD}BearSSL can allow you to select from
|
// CertStore can allow you to select from among
|
||||||
// 10s or 100s of CAs against which you can check the
|
// 10s or 100s of CAs against which you can check the
|
||||||
// target's X.509, without taking any more RAM than a single
|
// target's X.509, without taking any more RAM than a single
|
||||||
// certificate. This is the same way that standard browsers
|
// certificate. This is the same way that standard browsers
|
||||||
@ -31,7 +31,7 @@
|
|||||||
// Released to the public domain
|
// Released to the public domain
|
||||||
|
|
||||||
#include <ESP8266WiFi.h>
|
#include <ESP8266WiFi.h>
|
||||||
#include <CertStoreSPIFFSBearSSL.h>
|
#include <CertStoreBearSSL.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
const char *ssid = "....";
|
const char *ssid = "....";
|
||||||
@ -40,7 +40,87 @@ const char *pass = "....";
|
|||||||
// A single, global CertStore which can be used by all
|
// A single, global CertStore which can be used by all
|
||||||
// connections. Needs to stay live the entire time any of
|
// connections. Needs to stay live the entire time any of
|
||||||
// the WiFiClientBearSSLs are present.
|
// the WiFiClientBearSSLs are present.
|
||||||
CertStoreSPIFFSBearSSL certStore;
|
BearSSL::CertStore certStore;
|
||||||
|
|
||||||
|
// Uncomment below to use the SD card to store the certs
|
||||||
|
// #define USE_SDCARD 1
|
||||||
|
|
||||||
|
// NOTE: The CertStoreFile virtual class may migrate to a templated
|
||||||
|
// model in a future release. Expect some changes to the interface,
|
||||||
|
// no matter what, as the SD and SPIFFS filesystem get unified.
|
||||||
|
|
||||||
|
#ifdef USE_SDCARD
|
||||||
|
|
||||||
|
#include <SD.h>
|
||||||
|
class SDCertStoreFile : public BearSSL::CertStoreFile {
|
||||||
|
public:
|
||||||
|
SDCertStoreFile(const char *name) {
|
||||||
|
_name = name;
|
||||||
|
};
|
||||||
|
virtual ~SDCertStoreFile() override {};
|
||||||
|
|
||||||
|
// The main API
|
||||||
|
virtual bool open(bool write = false) override {
|
||||||
|
_file = SD.open(_name, write ? FILE_WRITE : FILE_READ);
|
||||||
|
return _file;
|
||||||
|
}
|
||||||
|
virtual bool seek(size_t absolute_pos) override {
|
||||||
|
return _file.seek(absolute_pos);
|
||||||
|
}
|
||||||
|
virtual ssize_t read(void *dest, size_t bytes) override {
|
||||||
|
return _file.read(dest, bytes);
|
||||||
|
}
|
||||||
|
virtual ssize_t write(void *dest, size_t bytes) override {
|
||||||
|
return _file.write((const uint8_t*)dest, bytes);
|
||||||
|
}
|
||||||
|
virtual void close() override {
|
||||||
|
_file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
File _file;
|
||||||
|
const char *_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
SDCertStoreFile certs_idx("/certs.idx");
|
||||||
|
SDCertStoreFile certs_ar("/certs.ar");
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <FS.h>
|
||||||
|
class SPIFFSCertStoreFile : public BearSSL::CertStoreFile {
|
||||||
|
public:
|
||||||
|
SPIFFSCertStoreFile(const char *name) {
|
||||||
|
_name = name;
|
||||||
|
};
|
||||||
|
virtual ~SPIFFSCertStoreFile() override {};
|
||||||
|
|
||||||
|
// The main API
|
||||||
|
virtual bool open(bool write = false) override {
|
||||||
|
_file = SPIFFS.open(_name, write ? "w" : "r");
|
||||||
|
return _file;
|
||||||
|
}
|
||||||
|
virtual bool seek(size_t absolute_pos) override {
|
||||||
|
return _file.seek(absolute_pos, SeekSet);
|
||||||
|
}
|
||||||
|
virtual ssize_t read(void *dest, size_t bytes) override {
|
||||||
|
return _file.readBytes((char*)dest, bytes);
|
||||||
|
}
|
||||||
|
virtual ssize_t write(void *dest, size_t bytes) override {
|
||||||
|
return _file.write((uint8_t*)dest, bytes);
|
||||||
|
}
|
||||||
|
virtual void close() override {
|
||||||
|
_file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
File _file;
|
||||||
|
const char *_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
SPIFFSCertStoreFile certs_idx("/certs.idx");
|
||||||
|
SPIFFSCertStoreFile certs_ar("/certs.ar");
|
||||||
|
#endif
|
||||||
|
|
||||||
// Set time via NTP, as required for x.509 validation
|
// Set time via NTP, as required for x.509 validation
|
||||||
void setClock() {
|
void setClock() {
|
||||||
@ -108,6 +188,12 @@ void setup() {
|
|||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.println();
|
Serial.println();
|
||||||
|
|
||||||
|
#ifdef USE_SDCARD
|
||||||
|
SD.begin();
|
||||||
|
#else
|
||||||
|
SPIFFS.begin();
|
||||||
|
#endif
|
||||||
|
|
||||||
// We start by connecting to a WiFi network
|
// We start by connecting to a WiFi network
|
||||||
Serial.print("Connecting to ");
|
Serial.print("Connecting to ");
|
||||||
Serial.println(ssid);
|
Serial.println(ssid);
|
||||||
@ -126,7 +212,7 @@ void setup() {
|
|||||||
|
|
||||||
setClock(); // Required for X.509 validation
|
setClock(); // Required for X.509 validation
|
||||||
|
|
||||||
int numCerts = certStore.initCertStore();
|
int numCerts = certStore.initCertStore(&certs_idx, &certs_ar);
|
||||||
Serial.printf("Number of CA certs read: %d\n", numCerts);
|
Serial.printf("Number of CA certs read: %d\n", numCerts);
|
||||||
if (numCerts == 0) {
|
if (numCerts == 0) {
|
||||||
Serial.printf("No certs found. Did you run certs-from-mozill.py and upload the SPIFFS directory before running?\n");
|
Serial.printf("No certs found. Did you run certs-from-mozill.py and upload the SPIFFS directory before running?\n");
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
# Script by Earle F. Philhower, III. Released to the public domain.
|
# Script by Earle F. Philhower, III. Released to the public domain.
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
from os import mkdir
|
import os
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE, call
|
||||||
import urllib2
|
import urllib2
|
||||||
try:
|
try:
|
||||||
# for Python 2.x
|
# for Python 2.x
|
||||||
@ -40,12 +40,27 @@ try:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
derFiles = []
|
||||||
|
idx = 0
|
||||||
# Process the text PEM using openssl into DER files
|
# Process the text PEM using openssl into DER files
|
||||||
for i in range(0, len(pems)):
|
for i in range(0, len(pems)):
|
||||||
certName = "data/ca_%03d.der" % (i);
|
certName = "data/ca_%03d.der" % (idx);
|
||||||
thisPem = pems[i].replace("'", "")
|
thisPem = pems[i].replace("'", "")
|
||||||
print names[i] + " -> " + certName
|
print names[i] + " -> " + certName
|
||||||
pipe = Popen(['openssl','x509','-inform','PEM','-outform','DER','-out', certName], shell = False, stdin = PIPE).stdin
|
ssl = Popen(['openssl','x509','-inform','PEM','-outform','DER','-out', certName], shell = False, stdin = PIPE)
|
||||||
|
pipe = ssl.stdin
|
||||||
pipe.write(thisPem)
|
pipe.write(thisPem)
|
||||||
pipe.close
|
pipe.close()
|
||||||
|
ssl.wait()
|
||||||
|
if os.path.exists(certName):
|
||||||
|
derFiles.append(certName)
|
||||||
|
idx = idx + 1
|
||||||
|
|
||||||
|
if os.path.exists("data/certs.ar"):
|
||||||
|
os.unlink("data/certs.ar");
|
||||||
|
|
||||||
|
arCmd = ['ar', 'mcs', 'data/certs.ar'] + derFiles;
|
||||||
|
call( arCmd )
|
||||||
|
|
||||||
|
for der in derFiles:
|
||||||
|
os.unlink(der)
|
||||||
|
@ -20,21 +20,18 @@
|
|||||||
#include "CertStoreBearSSL.h"
|
#include "CertStoreBearSSL.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
namespace BearSSL {
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
// Callbacks for the x509 decoder
|
// Callback for the x509 decoder
|
||||||
static void dn_append(void *ctx, const void *buf, size_t len) {
|
static void dn_append(void *ctx, const void *buf, size_t len) {
|
||||||
br_sha256_context *sha1 = (br_sha256_context*)ctx;
|
br_sha256_context *sha1 = (br_sha256_context*)ctx;
|
||||||
br_sha256_update(sha1, buf, len);
|
br_sha256_update(sha1, buf, len);
|
||||||
}
|
}
|
||||||
static void dn_append_null(void *ctx, const void *buf, size_t len) {
|
|
||||||
(void) ctx;
|
|
||||||
(void) buf;
|
|
||||||
(void) len;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CertStoreBearSSL::CertInfo CertStoreBearSSL::preprocessCert(const char *fname, const void *raw, size_t sz) {
|
CertStore::CertInfo CertStore::_preprocessCert(uint32_t length, uint32_t offset, const void *raw) {
|
||||||
CertStoreBearSSL::CertInfo ci;
|
CertStore::CertInfo ci;
|
||||||
|
|
||||||
// Clear the CertInfo
|
// Clear the CertInfo
|
||||||
memset(&ci, 0, sizeof(ci));
|
memset(&ci, 0, sizeof(ci));
|
||||||
@ -44,11 +41,12 @@ CertStoreBearSSL::CertInfo CertStoreBearSSL::preprocessCert(const char *fname, c
|
|||||||
br_sha256_context *sha256 = new br_sha256_context;
|
br_sha256_context *sha256 = new br_sha256_context;
|
||||||
br_sha256_init(sha256);
|
br_sha256_init(sha256);
|
||||||
br_x509_decoder_init(ctx, dn_append, sha256, nullptr, nullptr);
|
br_x509_decoder_init(ctx, dn_append, sha256, nullptr, nullptr);
|
||||||
br_x509_decoder_push(ctx, (const void*)raw, sz);
|
br_x509_decoder_push(ctx, (const void*)raw, length);
|
||||||
|
|
||||||
// Copy result to structure
|
// Copy result to structure
|
||||||
br_sha256_out(sha256, &ci.sha256);
|
br_sha256_out(sha256, &ci.sha256);
|
||||||
strcpy(ci.fname, fname);
|
ci.length = length;
|
||||||
|
ci.offset = offset;
|
||||||
|
|
||||||
// Clean up allocated memory
|
// Clean up allocated memory
|
||||||
delete sha256;
|
delete sha256;
|
||||||
@ -58,84 +56,139 @@ CertStoreBearSSL::CertInfo CertStoreBearSSL::preprocessCert(const char *fname, c
|
|||||||
return ci;
|
return ci;
|
||||||
}
|
}
|
||||||
|
|
||||||
br_x509_trust_anchor *CertStoreBearSSL::makeTrustAnchor(const void *der, size_t der_len, const CertInfo *ci) {
|
// The certs.ar file is a UNIX ar format file, concatenating all the
|
||||||
// std::unique_ptr will free dc when we exit scope, automatically
|
// individual certificates into a single blob in a space-efficient way.
|
||||||
std::unique_ptr<br_x509_decoder_context> dc(new br_x509_decoder_context);
|
int CertStore::initCertStore(CertStoreFile *index, CertStoreFile *data) {
|
||||||
br_x509_decoder_init(dc.get(), dn_append_null, nullptr, nullptr, nullptr);
|
int count = 0;
|
||||||
br_x509_decoder_push(dc.get(), der, der_len);
|
uint32_t offset = 0;
|
||||||
br_x509_pkey *pk = br_x509_decoder_get_pkey(dc.get());
|
|
||||||
if (!pk) {
|
_index = index;
|
||||||
return nullptr;
|
_data = data;
|
||||||
|
|
||||||
|
if (!_index || !data) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
br_x509_trust_anchor *ta = (br_x509_trust_anchor*)malloc(sizeof(br_x509_trust_anchor));
|
if (!_index->open(true)) {
|
||||||
if (!ta) {
|
return 0;
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
memset(ta, 0, sizeof(*ta));
|
|
||||||
ta->dn.data = (uint8_t*)malloc(sizeof(ci->sha256));
|
|
||||||
if (!ta->dn.data) {
|
|
||||||
free(ta);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
memcpy(ta->dn.data, ci->sha256, sizeof(ci->sha256));
|
|
||||||
ta->dn.len = sizeof(ci->sha256);
|
|
||||||
|
|
||||||
ta->flags = 0;
|
|
||||||
if (br_x509_decoder_isCA(dc.get())) {
|
|
||||||
ta->flags |= BR_X509_TA_CA;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (pk->key_type) {
|
if (!_data->open(false)) {
|
||||||
case BR_KEYTYPE_RSA:
|
_index->close();
|
||||||
ta->pkey.key_type = BR_KEYTYPE_RSA;
|
return 0;
|
||||||
ta->pkey.key.rsa.n = (uint8_t*)malloc(pk->key.rsa.nlen);
|
|
||||||
if (!ta->pkey.key.rsa.n) {
|
|
||||||
free(ta->dn.data);
|
|
||||||
free(ta);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
memcpy(ta->pkey.key.rsa.n, pk->key.rsa.n, pk->key.rsa.nlen);
|
|
||||||
ta->pkey.key.rsa.nlen = pk->key.rsa.nlen;
|
|
||||||
ta->pkey.key.rsa.e = (uint8_t*)malloc(pk->key.rsa.elen);
|
|
||||||
if (!ta->pkey.key.rsa.e) {
|
|
||||||
free(ta->pkey.key.rsa.n);
|
|
||||||
free(ta->dn.data);
|
|
||||||
free(ta);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
memcpy(ta->pkey.key.rsa.e, pk->key.rsa.e, pk->key.rsa.elen);
|
|
||||||
ta->pkey.key.rsa.elen = pk->key.rsa.elen;
|
|
||||||
return ta;
|
|
||||||
case BR_KEYTYPE_EC:
|
|
||||||
ta->pkey.key_type = BR_KEYTYPE_EC;
|
|
||||||
ta->pkey.key.ec.curve = pk->key.ec.curve;
|
|
||||||
ta->pkey.key.ec.q = (uint8_t*)malloc(pk->key.ec.qlen);
|
|
||||||
if (!ta->pkey.key.ec.q) {
|
|
||||||
free(ta->dn.data);
|
|
||||||
free(ta);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
memcpy(ta->pkey.key.ec.q, pk->key.ec.q, pk->key.ec.qlen);
|
|
||||||
ta->pkey.key.ec.qlen = pk->key.ec.qlen;
|
|
||||||
return ta;
|
|
||||||
default:
|
|
||||||
free(ta->dn.data);
|
|
||||||
free(ta);
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char magic[8];
|
||||||
|
if (_data->read(magic, sizeof(magic)) != sizeof(magic) ||
|
||||||
|
memcmp(magic, "!<arch>\n", sizeof(magic)) ) {
|
||||||
|
_data->close();
|
||||||
|
_index->close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
offset += sizeof(magic);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
char fileHeader[60];
|
||||||
|
// 0..15 = filename in ASCII
|
||||||
|
// 48...57 = length in decimal ASCII
|
||||||
|
uint32_t length;
|
||||||
|
if (data->read(fileHeader, sizeof(fileHeader)) != sizeof(fileHeader)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += sizeof(fileHeader);
|
||||||
|
fileHeader[58] = 0;
|
||||||
|
if (1 != sscanf(fileHeader + 48, "%d", &length) || !length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *raw = malloc(length);
|
||||||
|
if (!raw) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (_data->read(raw, length) != (ssize_t)length) {
|
||||||
|
free(raw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the filename starts with "//" then this is a rename file, skip it
|
||||||
|
if (fileHeader[0] != '/' || fileHeader[1] != '/') {
|
||||||
|
CertStore::CertInfo ci = _preprocessCert(length, offset, raw);
|
||||||
|
if (_index->write(&ci, sizeof(ci)) != (ssize_t)sizeof(ci)) {
|
||||||
|
free(raw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += length;
|
||||||
|
free(raw);
|
||||||
|
if (offset & 1) {
|
||||||
|
char x;
|
||||||
|
_data->read(&x, 1);
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_data->close();
|
||||||
|
_index->close();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CertStore::installCertStore(br_x509_minimal_context *ctx) {
|
||||||
|
br_x509_minimal_set_dynamic(ctx, (void*)this, findHashedTA, freeHashedTA);
|
||||||
|
}
|
||||||
|
|
||||||
|
const br_x509_trust_anchor *CertStore::findHashedTA(void *ctx, void *hashed_dn, size_t len) {
|
||||||
|
CertStore *cs = static_cast<CertStore*>(ctx);
|
||||||
|
CertStore::CertInfo ci;
|
||||||
|
|
||||||
|
if (!cs || len != sizeof(ci.sha256) || !cs->_index || !cs->_data) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cs->_index->open(false)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cs->_index->read(&ci, sizeof(ci)) == sizeof(ci)) {
|
||||||
|
if (!memcmp(ci.sha256, hashed_dn, sizeof(ci.sha256))) {
|
||||||
|
cs->_index->close();
|
||||||
|
uint8_t *der = (uint8_t*)malloc(ci.length);
|
||||||
|
if (!der) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!cs->_data->open(false)) {
|
||||||
|
free(der);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!cs->_data->seek(ci.offset)) {
|
||||||
|
cs->_data->close();
|
||||||
|
free(der);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (cs->_data->read(der, ci.length) != (ssize_t)ci.length) {
|
||||||
|
free(der);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
cs->_data->close();
|
||||||
|
cs->_x509 = new BearSSLX509List(der, ci.length);
|
||||||
|
free(der);
|
||||||
|
|
||||||
|
br_x509_trust_anchor *ta = (br_x509_trust_anchor*)cs->_x509->getTrustAnchors();
|
||||||
|
memcpy(ta->dn.data, ci.sha256, sizeof(ci.sha256));
|
||||||
|
ta->dn.len = sizeof(ci.sha256);
|
||||||
|
|
||||||
|
return ta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cs->_index->close();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CertStore::freeHashedTA(void *ctx, const br_x509_trust_anchor *ta) {
|
||||||
|
CertStore *cs = static_cast<CertStore*>(ctx);
|
||||||
|
(void) ta; // Unused
|
||||||
|
delete cs->_x509;
|
||||||
|
cs->_x509 = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CertStoreBearSSL::freeTrustAnchor(const br_x509_trust_anchor *ta) {
|
|
||||||
switch (ta->pkey.key_type) {
|
|
||||||
case BR_KEYTYPE_RSA:
|
|
||||||
free(ta->pkey.key.rsa.e);
|
|
||||||
free(ta->pkey.key.rsa.n);
|
|
||||||
break;
|
|
||||||
case BR_KEYTYPE_EC:
|
|
||||||
free(ta->pkey.key.ec.q);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
free(ta->dn.data);
|
|
||||||
free((void*)ta);
|
|
||||||
}
|
}
|
||||||
|
@ -21,40 +21,69 @@
|
|||||||
#define _CERTSTORE_BEARSSL_H
|
#define _CERTSTORE_BEARSSL_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <BearSSLHelpers.h>
|
||||||
#include <bearssl/bearssl.h>
|
#include <bearssl/bearssl.h>
|
||||||
|
|
||||||
// Virtual base class for the certificate stores, which allow use
|
// Base class for the certificate stores, which allow use
|
||||||
// of a large set of certificates stored on SPIFFS of SD card to
|
// of a large set of certificates stored on SPIFFS of SD card to
|
||||||
// be dynamically used when validating a X509 certificate
|
// be dynamically used when validating a X509 certificate
|
||||||
|
|
||||||
// Templates for child classes not possible due to the difference in SD
|
namespace BearSSL {
|
||||||
// and FS in terms of directory parsing and interating. Dir doesn't
|
|
||||||
// exist in SD, everything is a file (which might support get-next-entry()
|
|
||||||
// or not).
|
|
||||||
|
|
||||||
// This class should not be instantiated directly, only via its children.
|
// Subclass this and provide virtual functions appropriate for your storage.
|
||||||
class CertStoreBearSSL {
|
// Required because there are conflicting definitions for a "File" in the
|
||||||
|
// Arduino setup, and there is no simple way to work around the minor
|
||||||
|
// differences.
|
||||||
|
// See the examples for implementations to use in your own code.
|
||||||
|
//
|
||||||
|
// NOTE: This virtual class may migrate to a templated model in a future
|
||||||
|
// release. Expect some changes to the interface, no matter what, as the
|
||||||
|
// SD and SPIFFS filesystem get unified.
|
||||||
|
class CertStoreFile {
|
||||||
public:
|
public:
|
||||||
CertStoreBearSSL() {}
|
CertStoreFile() {};
|
||||||
virtual ~CertStoreBearSSL() {}
|
virtual ~CertStoreFile() {};
|
||||||
|
|
||||||
// Preprocess the certs from the flash, returns number parsed
|
// The main API
|
||||||
virtual int initCertStore(const char *dir) = 0;
|
virtual bool open(bool write=false) = 0;
|
||||||
|
virtual bool seek(size_t absolute_pos) = 0;
|
||||||
|
virtual ssize_t read(void *dest, size_t bytes) = 0;
|
||||||
|
virtual ssize_t write(void *dest, size_t bytes) = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class CertStore {
|
||||||
|
public:
|
||||||
|
CertStore() { };
|
||||||
|
~CertStore() { };
|
||||||
|
|
||||||
|
// Set the file interface instances, do preprocessing
|
||||||
|
int initCertStore(CertStoreFile *index, CertStoreFile *data);
|
||||||
|
|
||||||
// Installs the cert store into the X509 decoder (normally via static function callbacks)
|
// Installs the cert store into the X509 decoder (normally via static function callbacks)
|
||||||
virtual void installCertStore(br_x509_minimal_context *ctx) = 0;
|
void installCertStore(br_x509_minimal_context *ctx);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// The binary format of the pre-computed file
|
CertStoreFile *_index = nullptr;
|
||||||
|
CertStoreFile *_data = nullptr;
|
||||||
|
BearSSLX509List *_x509 = nullptr;
|
||||||
|
|
||||||
|
// These need to be static as they are callbacks from BearSSL C code
|
||||||
|
static const br_x509_trust_anchor *findHashedTA(void *ctx, void *hashed_dn, size_t len);
|
||||||
|
static void freeHashedTA(void *ctx, const br_x509_trust_anchor *ta);
|
||||||
|
|
||||||
|
// The binary format of the index file
|
||||||
class CertInfo {
|
class CertInfo {
|
||||||
public:
|
public:
|
||||||
uint8_t sha256[32];
|
uint8_t sha256[32];
|
||||||
char fname[64];
|
uint32_t offset;
|
||||||
|
uint32_t length;
|
||||||
};
|
};
|
||||||
|
static CertInfo _preprocessCert(uint32_t length, uint32_t offset, const void *raw);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
CertInfo preprocessCert(const char *fname, const void *raw, size_t sz);
|
|
||||||
static br_x509_trust_anchor *makeTrustAnchor(const void *der, size_t der_len, const CertInfo *ci);
|
|
||||||
static void freeTrustAnchor(const br_x509_trust_anchor *ta);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
/*
|
|
||||||
CertStoreSDBearSSL.cpp - Library for Arduino ESP8266
|
|
||||||
Copyright (c) 2018 Earle F. Philhower, III
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SD.h>
|
|
||||||
#include "CertStoreSDBearSSL.h"
|
|
||||||
|
|
||||||
CertStoreSDBearSSL::CertStoreSDBearSSL() : CertStoreBearSSL() {
|
|
||||||
path = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
CertStoreSDBearSSL::~CertStoreSDBearSSL() {
|
|
||||||
}
|
|
||||||
|
|
||||||
CertStoreBearSSL::CertInfo CertStoreSDBearSSL::preprocessCert(File *f) {
|
|
||||||
CertStoreBearSSL::CertInfo ci;
|
|
||||||
memset(&ci, 0, sizeof(ci));
|
|
||||||
|
|
||||||
// Load the DER into RAM temporarially
|
|
||||||
if (!f) {
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sz = f->size();
|
|
||||||
uint8_t *buf = new uint8_t[sz];
|
|
||||||
if (!buf) {
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
f->read(buf, sz);
|
|
||||||
|
|
||||||
ci = CertStoreBearSSL::preprocessCert(f->name(), buf, sz);
|
|
||||||
|
|
||||||
delete buf;
|
|
||||||
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
|
|
||||||
int CertStoreSDBearSSL::initCertStore(const char *subdir) {
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
// We want path to have a leading slash and a trailing one
|
|
||||||
path = subdir;
|
|
||||||
if (path[0] != '/') {
|
|
||||||
path = "/" + path;
|
|
||||||
}
|
|
||||||
if (!path.endsWith("/")) {
|
|
||||||
path += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
String tblName = path + "ca_tbl.bin";
|
|
||||||
|
|
||||||
File tbl = SD.open(tblName, FILE_WRITE);
|
|
||||||
if (!tbl) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
File d = SD.open(path);
|
|
||||||
while (true) {
|
|
||||||
File nextFile = d.openNextFile();
|
|
||||||
if (!nextFile) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!strstr(nextFile.name(), ".der")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CertStoreBearSSL::CertInfo ci = preprocessCert(&nextFile);
|
|
||||||
nextFile.close();
|
|
||||||
tbl.write((uint8_t*)&ci, sizeof(ci));
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
tbl.close();
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CertStoreSDBearSSL::installCertStore(br_x509_minimal_context *ctx) {
|
|
||||||
br_x509_minimal_set_dynamic(ctx, (void*)this, findHashedTA, freeHashedTA);
|
|
||||||
}
|
|
||||||
|
|
||||||
const br_x509_trust_anchor *CertStoreSDBearSSL::findHashedTA(void *ctx, void *hashed_dn, size_t len) {
|
|
||||||
CertStoreSDBearSSL *cs = static_cast<CertStoreSDBearSSL*>(ctx);
|
|
||||||
CertInfo ci;
|
|
||||||
|
|
||||||
String tblName = cs->path + "ca_tbl.bin";
|
|
||||||
|
|
||||||
if (len != sizeof(ci.sha256) || !SD.exists(tblName)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
File f = SD.open(tblName, FILE_READ);
|
|
||||||
if (!f) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
while (f.read((uint8_t*)&ci, sizeof(ci)) == sizeof(ci)) {
|
|
||||||
if (!memcmp(ci.sha256, hashed_dn, sizeof(ci.sha256))) {
|
|
||||||
// This could be the one!
|
|
||||||
f.close();
|
|
||||||
File d = SD.open(ci.fname, FILE_READ);
|
|
||||||
if (!d) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
size_t der_len = d.size();
|
|
||||||
uint8_t *der = (uint8_t*)malloc(der_len);
|
|
||||||
if (!der) {
|
|
||||||
d.close();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (d.read(der, der_len) != (int)der_len) {
|
|
||||||
d.close();
|
|
||||||
free(der);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
d.close();
|
|
||||||
|
|
||||||
br_x509_trust_anchor *ta = CertStoreBearSSL::makeTrustAnchor(der, der_len, &ci);
|
|
||||||
free(der);
|
|
||||||
|
|
||||||
return ta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CertStoreSDBearSSL::freeHashedTA(void *ctx, const br_x509_trust_anchor *ta) {
|
|
||||||
(void) ctx; // not needed
|
|
||||||
CertStoreBearSSL::freeTrustAnchor(ta);
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
CertStoreSDBearSSL.h - Library for Arduino ESP8266
|
|
||||||
Copyright (c) 2018 Earle F. Philhower, III
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _CERTSTORESD_BEARSSL_H
|
|
||||||
#define _CERTSTORESD_BEARSSL_H
|
|
||||||
|
|
||||||
#include "CertStoreBearSSL.h"
|
|
||||||
|
|
||||||
class File;
|
|
||||||
|
|
||||||
// SD cert store can be in a subdirectory as there are fewer limits
|
|
||||||
// Note that SD.begin() MUST be called before doing initCertStore because
|
|
||||||
// there are different options for the CS and other pins you need to
|
|
||||||
// specify it in your own code.
|
|
||||||
class CertStoreSDBearSSL : public CertStoreBearSSL {
|
|
||||||
public:
|
|
||||||
CertStoreSDBearSSL();
|
|
||||||
virtual ~CertStoreSDBearSSL();
|
|
||||||
|
|
||||||
virtual int initCertStore(const char *dir = "/") override;
|
|
||||||
virtual void installCertStore(br_x509_minimal_context *ctx) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
String path;
|
|
||||||
CertInfo preprocessCert(File *f);
|
|
||||||
// These need to be static as they are callbacks from BearSSL C code
|
|
||||||
static const br_x509_trust_anchor *findHashedTA(void *ctx, void *hashed_dn, size_t len);
|
|
||||||
static void freeHashedTA(void *ctx, const br_x509_trust_anchor *ta);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
CertStoreSPIFFSBearSSL.cpp - Library for Arduino ESP8266
|
|
||||||
Copyright (c) 2018 Earle F. Philhower, III
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "CertStoreSPIFFSBearSSL.h"
|
|
||||||
#include <FS.h>
|
|
||||||
|
|
||||||
CertStoreSPIFFSBearSSL::CertStoreSPIFFSBearSSL() : CertStoreBearSSL() {
|
|
||||||
}
|
|
||||||
|
|
||||||
CertStoreSPIFFSBearSSL::~CertStoreSPIFFSBearSSL() {
|
|
||||||
}
|
|
||||||
|
|
||||||
CertStoreBearSSL::CertInfo CertStoreSPIFFSBearSSL::preprocessCert(const char *fname) {
|
|
||||||
CertStoreBearSSL::CertInfo ci;
|
|
||||||
memset(&ci, 0, sizeof(ci));
|
|
||||||
|
|
||||||
// Load the DER into RAM temporarially
|
|
||||||
File f = SPIFFS.open(fname, "r");
|
|
||||||
if (!f) {
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
int sz = f.size();
|
|
||||||
uint8_t *buf = new uint8_t[sz];
|
|
||||||
if (!buf) {
|
|
||||||
f.close();
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
f.read(buf, sz);
|
|
||||||
f.close();
|
|
||||||
|
|
||||||
ci = CertStoreBearSSL::preprocessCert(fname, buf, sz);
|
|
||||||
|
|
||||||
delete[] buf;
|
|
||||||
|
|
||||||
return ci;
|
|
||||||
}
|
|
||||||
|
|
||||||
int CertStoreSPIFFSBearSSL::initCertStore(const char *subdir) {
|
|
||||||
(void) subdir; // ignored prefix, not enough space in filenames
|
|
||||||
int count = 0;
|
|
||||||
SPIFFS.begin();
|
|
||||||
File tbl = SPIFFS.open("/ca_tbl.bin", "w");
|
|
||||||
if (!tbl) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Dir d = SPIFFS.openDir("");
|
|
||||||
while (d.next()) {
|
|
||||||
if (!strstr(d.fileName().c_str(), ".der")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CertStoreBearSSL::CertInfo ci = preprocessCert(d.fileName().c_str());
|
|
||||||
tbl.write((uint8_t*)&ci, sizeof(ci));
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
tbl.close();
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CertStoreSPIFFSBearSSL::installCertStore(br_x509_minimal_context *ctx) {
|
|
||||||
br_x509_minimal_set_dynamic(ctx, /* no context needed */nullptr, findHashedTA, freeHashedTA);
|
|
||||||
}
|
|
||||||
|
|
||||||
const br_x509_trust_anchor *CertStoreSPIFFSBearSSL::findHashedTA(void *ctx, void *hashed_dn, size_t len) {
|
|
||||||
(void) ctx; // not needed
|
|
||||||
CertInfo ci;
|
|
||||||
|
|
||||||
if (len != sizeof(ci.sha256) || !SPIFFS.exists("/ca_tbl.bin")) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
File f = SPIFFS.open("/ca_tbl.bin", "r");
|
|
||||||
if (!f) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
while (f.read((uint8_t*)&ci, sizeof(ci)) == sizeof(ci)) {
|
|
||||||
if (!memcmp(ci.sha256, hashed_dn, sizeof(ci.sha256))) {
|
|
||||||
// This could be the one!
|
|
||||||
f.close();
|
|
||||||
File d = SPIFFS.open(ci.fname, "r");
|
|
||||||
if (!d) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
size_t der_len = d.size();
|
|
||||||
uint8_t *der = (uint8_t*)malloc(der_len);
|
|
||||||
if (!der) {
|
|
||||||
d.close();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (d.read(der, der_len) != der_len) {
|
|
||||||
d.close();
|
|
||||||
free(der);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
d.close();
|
|
||||||
|
|
||||||
br_x509_trust_anchor *ta = CertStoreBearSSL::makeTrustAnchor(der, der_len, &ci);
|
|
||||||
free(der);
|
|
||||||
|
|
||||||
return ta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CertStoreSPIFFSBearSSL::freeHashedTA(void *ctx, const br_x509_trust_anchor *ta) {
|
|
||||||
(void) ctx; // not needed
|
|
||||||
CertStoreBearSSL::freeTrustAnchor(ta);
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
CertStoreSPIFFSBearSSL.h - Library for Arduino ESP8266
|
|
||||||
Copyright (c) 2018 Earle F. Philhower, III
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _CERTSTORESPIFFS_BEARSSL_H
|
|
||||||
#define _CERTSTORESPIFFS_BEARSSL_H
|
|
||||||
|
|
||||||
#include "CertStoreBearSSL.h"
|
|
||||||
#include <FS.h>
|
|
||||||
|
|
||||||
// SPIFFS cert stores stored in root directory due to filename length limits
|
|
||||||
class CertStoreSPIFFSBearSSL : public CertStoreBearSSL {
|
|
||||||
public:
|
|
||||||
CertStoreSPIFFSBearSSL();
|
|
||||||
virtual ~CertStoreSPIFFSBearSSL();
|
|
||||||
|
|
||||||
virtual int initCertStore(const char *dir = "") override; // ignores dir
|
|
||||||
virtual void installCertStore(br_x509_minimal_context *ctx) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
CertInfo preprocessCert(const char *fname);
|
|
||||||
// These need to be static as they are callbacks from BearSSL C code
|
|
||||||
static const br_x509_trust_anchor *findHashedTA(void *ctx, void *hashed_dn, size_t len);
|
|
||||||
static void freeHashedTA(void *ctx, const br_x509_trust_anchor *ta);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -95,7 +95,7 @@ class WiFiClientSecure : public WiFiClient {
|
|||||||
int getLastSSLError(char *dest = NULL, size_t len = 0);
|
int getLastSSLError(char *dest = NULL, size_t len = 0);
|
||||||
|
|
||||||
// Attach a preconfigured certificate store
|
// Attach a preconfigured certificate store
|
||||||
void setCertStore(CertStoreBearSSL *certStore) {
|
void setCertStore(CertStore *certStore) {
|
||||||
_certStore = certStore;
|
_certStore = certStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ class WiFiClientSecure : public WiFiClient {
|
|||||||
std::shared_ptr<unsigned char> _iobuf_out;
|
std::shared_ptr<unsigned char> _iobuf_out;
|
||||||
time_t _now;
|
time_t _now;
|
||||||
const BearSSLX509List *_ta;
|
const BearSSLX509List *_ta;
|
||||||
CertStoreBearSSL *_certStore;
|
CertStore *_certStore;
|
||||||
int _iobuf_in_size;
|
int _iobuf_in_size;
|
||||||
int _iobuf_out_size;
|
int _iobuf_out_size;
|
||||||
bool _handshake_done;
|
bool _handshake_done;
|
||||||
|
@ -259,7 +259,7 @@ elif [ "$BUILD_TYPE" = "build_odd" ]; then
|
|||||||
elif [ "$BUILD_TYPE" = "platformio" ]; then
|
elif [ "$BUILD_TYPE" = "platformio" ]; then
|
||||||
# PlatformIO
|
# PlatformIO
|
||||||
install_platformio
|
install_platformio
|
||||||
build_sketches_with_platformio $TRAVIS_BUILD_DIR/libraries "--board nodemcuv2 --project-option=lib_ldf_mode=deep+ --verbose"
|
build_sketches_with_platformio $TRAVIS_BUILD_DIR/libraries "--board nodemcuv2 --verbose"
|
||||||
elif [ "$BUILD_TYPE" = "docs" ]; then
|
elif [ "$BUILD_TYPE" = "docs" ]; then
|
||||||
# Build documentation using Sphinx
|
# Build documentation using Sphinx
|
||||||
cd $TRAVIS_BUILD_DIR/doc
|
cd $TRAVIS_BUILD_DIR/doc
|
||||||
|
Loading…
x
Reference in New Issue
Block a user